From 6469528af1355117b9083e7949edc0bc4d12e99e Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Wed, 20 Nov 2024 17:20:29 +1100 Subject: [PATCH 001/138] feat: create logger, performance tracker and time keeper --- log/__init__.py | 56 ++++++++++++++++++++++ log/perf.py | 60 ++++++++++++++++++++++++ log/time_keeper.py | 114 +++++++++++++++++++++++++++++++++++++++++++++ log/util.py | 52 +++++++++++++++++++++ 4 files changed, 282 insertions(+) create mode 100644 log/__init__.py create mode 100644 log/perf.py create mode 100644 log/time_keeper.py create mode 100644 log/util.py diff --git a/log/__init__.py b/log/__init__.py new file mode 100644 index 0000000..9554185 --- /dev/null +++ b/log/__init__.py @@ -0,0 +1,56 @@ +import logging + +from log.perf import PerformanceLogger +from log.util import add_logging_level + +CUSTOM_LOG_LEVELS = {"SILLY": 1, "PERF": 69} +for label, lvl in CUSTOM_LOG_LEVELS.items(): + add_logging_level(label, lvl) + + +class CustomFormatter(logging.Formatter): + + green = "\x1b[32;20m" + blue = "\x1b[36;20m" + grey = "\x1b[38;20m" + yellow = "\x1b[33;20m" + red = "\x1b[31;20m" + bold_red = "\x1b[31;1m" + purple = "\x1b[35;1m" + reset = "\x1b[0m" + format = "%(asctime)s | %(name)s | %(levelname)s | %(message)s (%(filename)s:%(lineno)d)" + + FORMATS = { + CUSTOM_LOG_LEVELS["SILLY"]: green + format + reset, + logging.DEBUG: blue + format + reset, + logging.INFO: grey + format + reset, + logging.WARNING: yellow + format + reset, + logging.ERROR: red + format + reset, + logging.CRITICAL: bold_red + format + reset, + 69: purple + format + reset, + } + + def format(self, record): + log_fmt = self.FORMATS.get(record.levelno) + formatter = logging.Formatter(log_fmt) + return formatter.format(record) + + +class Log: + def __init__(self, name, initial_level=CUSTOM_LOG_LEVELS["SILLY"], enable_perf=False): + self.name = name + self.logger = logging.getLogger(name) + self.logger.setLevel(initial_level) + self.logger.propagate = False + + ch = logging.StreamHandler() + ch.setLevel(initial_level) + ch.setFormatter(CustomFormatter()) + self.logger.addHandler(ch) + + self.logger.perf = PerformanceLogger(self.logger.perf, enable_perf) + + def set_level(self, level: str) -> None: + named_level = logging.getLevelName(level) + self.logger.info("Setting log level to {}".format(named_level)) + self.logger.setLevel(level) diff --git a/log/perf.py b/log/perf.py new file mode 100644 index 0000000..ab3dc78 --- /dev/null +++ b/log/perf.py @@ -0,0 +1,60 @@ +from logging import info +import time + + +class PerformanceLogger: + def __init__(self, logger: info = None, enabled=True): + if enabled: + self.logger = logger + self.start = self._start_enabled + self.end = self.end_enabled + self.times = {} + self.cpu_times = {} + self.orphaned_event_age_seconds = 3600 # 1 hour + self.check_for_orphans_interval = 3600 # 1 hour + self.last_orphan_prune = 0 + else: + self.start = self._noop + self.end = self._noop + + def _start_enabled(self, label): + self.times[label] = time.perf_counter_ns() + self.cpu_times[label] = time.process_time_ns() + + def end_enabled(self, label): + elapsed_ms, elapsed_cpu_ms = self.end_timer(label) + self._log_end(label, elapsed_ms, elapsed_cpu_ms) + + def end_timer(self, label): + start_time = self.times.pop(label, None) + start_time_cpu = self.cpu_times.pop(label, None) + + self._cleanup_orphans() + + if start_time is not None and start_time_cpu is not None: + elapsed_ms = (time.perf_counter_ns() - start_time) / 1e6 + elapsed_cpu_ms = (time.process_time_ns() - start_time_cpu) / 1e6 + return elapsed_ms, elapsed_cpu_ms + return None, None + + def _log_end(self, label, elapsed_ms, elapsed_cpu_ms): + if self.logger is None: + return + + if elapsed_ms is not None and elapsed_cpu_ms is not None: + self.logger( + f"Elapsed time for '{label}': {elapsed_ms:.6f} ms ({elapsed_cpu_ms:.6f} cpu ms)" + ) + else: + self.logger(f"No start time recorded for label '{label}'") + + def _cleanup_orphans(self): + now = time.time() + if len(self.times) > 0 and now - self.last_orphan_prune > self.check_for_orphans_interval: + self.last_orphan_prune = now + for label, start in self.times.items(): + if now - start > self.orphaned_event_age_seconds: + self.times.pop(label) + + def _noop(self, *args, **kwargs): + pass diff --git a/log/time_keeper.py b/log/time_keeper.py new file mode 100644 index 0000000..9f50c15 --- /dev/null +++ b/log/time_keeper.py @@ -0,0 +1,114 @@ +import logging +import statistics +import time + +from log import PerformanceLogger +from util import format_seconds, format_ms + + +class TimeKeeper: + def __init__(self, logger: logging, perf=False, max_events=10_000): + self.max_events = max_events + assert self.max_events > 100, "max_events must be greater than 100 to be meaningful" + assert self.max_events < 10e6, "max_events must be less than 10e6 to avoid memory issues" + + self.logger = logger + + self.time_keeper_app_alive = time.time() + self.exec_timestamps = {} + + self.exec_durations = {} + self.exec_cpu_durations = {} + + if perf: + self.perf = PerformanceLogger(enabled=True) + + def add(self, name: str): + self.exec_timestamps.setdefault(name, []).append(time.time()) + self.perf.start(name) + + def end(self, name: str): + elapsed_ms, elapsed_cpu_ms = self.perf.end_timer(name) + self.exec_durations.setdefault(name, []).append(elapsed_ms) + self.exec_cpu_durations.setdefault(name, []).append(elapsed_cpu_ms) + + def get(self, name: str): + return ( + self.exec_timestamps[name], + self.exec_durations[name], + self.exec_cpu_durations[name], + ) + + def log_time_keeper(self): + self.logger.info( + "Time keeper: app alive for {} seconds".format( + format_seconds(time.time() - self.time_keeper_app_alive, 0) + ) + ) + for task, timestamps in self.exec_timestamps.items(): + deltas = [] + for i in range(len(timestamps)): + if i == 0: + continue + deltas.append(timestamps[i] - timestamps[i - 1]) + + n_deltas = len(deltas) + + self.logger.info("Time keeper: task '{}', n: {}".format(task, len(timestamps))) + if n_deltas > 0: + self.logger.info( + "- Intervals: min: {}s, max: {}s, avg: {}s, median: {}s, stddev: {}s".format( + format_seconds(min(deltas)), + format_seconds(max(deltas)), + format_seconds(sum(deltas) / n_deltas), + format_seconds(statistics.median(deltas)) if n_deltas > 1 else "N/A", + format_seconds(statistics.stdev(deltas)) if n_deltas > 1 else "N/A", + ) + ) + else: + self.logger.info("- More than one timestamp is required for interval stats") + + durations = self.exec_durations[task] + n_durations = len(durations) + if n_durations > 0: + self.logger.info( + "- Durations: min: {}ms, max: {}ms, avg: {}ms, median: {}ms, stddev: {}ms".format( + format_ms(min(durations)), + format_ms(max(durations)), + format_ms(sum(durations) / n_durations), + format_ms(statistics.median(durations)) if n_durations > 1 else "N/A", + format_ms(statistics.stdev(durations)) if n_durations > 1 else "N/A", + ) + ) + else: + self.logger.info("- More than one duration is required for duration stats") + + cpu_durations = self.exec_cpu_durations[task] + n_cpu_durations = len(cpu_durations) + if len(cpu_durations) > 0: + self.logger.info( + "- CPU Durations: min: {}ms, max: {}ms, avg: {}ms, median: {}ms, stddev: {}ms".format( + format_ms(min(cpu_durations)), + format_ms(max(cpu_durations)), + format_ms(sum(cpu_durations) / n_cpu_durations), + format_ms(statistics.median(cpu_durations)) if n_durations > 1 else "N/A", + ( + format_ms(statistics.stdev(cpu_durations)) + if n_cpu_durations > 1 + else "N/A" + ), + ) + ) + else: + self.logger.info("- More than one cpu duration is required for cpu duration stats") + + self.cleanup() + + def cleanup(self): + # NOTE: each array can vary in length as if an error occurs, exec_timestamps will be longer than durations + if len(self.exec_timestamps) > self.max_events: + self.exec_timestamps = self.exec_timestamps[-self.max_events :] + if len(self.exec_durations) > self.max_events: + self.exec_durations = self.exec_durations[-self.max_events :] + if len(self.exec_cpu_durations) > self.max_events: + self.exec_cpu_durations = self.exec_cpu_durations[-self.max_events :] diff --git a/log/util.py b/log/util.py new file mode 100644 index 0000000..0614a8f --- /dev/null +++ b/log/util.py @@ -0,0 +1,52 @@ +import logging + + +def add_logging_level(level_name: str, level_num: int, method_name=None): + """ + Comprehensively adds a new logging level to the `logging` module and the + currently configured logging class. + + `levelName` becomes an attribute of the `logging` module with the value + `levelNum`. `methodName` becomes a convenience method for both `logging` + itself and the class returned by `logging.getLoggerClass()` (usually just + `logging.Logger`). If `methodName` is not specified, `levelName.lower()` is + used. + + To avoid accidental clobberings of existing attributes, this method will + raise an `AttributeError` if the level name is already an attribute of the + `logging` module or if the method name is already present + + Example + ------- + >>> addLoggingLevel('TRACE', logging.DEBUG - 5) + >>> logging.getLogger(__name__).setLevel("TRACE") + >>> logging.getLogger(__name__).trace('that worked') + >>> logging.trace('so did this') + >>> logging.TRACE + 5 + + """ + if not method_name: + method_name = level_name.lower() + + if hasattr(logging, level_name): + raise AttributeError("{} already defined in logging module".format(level_name)) + if hasattr(logging, method_name): + raise AttributeError("{} already defined in logging module".format(method_name)) + if hasattr(logging.getLoggerClass(), method_name): + raise AttributeError("{} already defined in logger class".format(method_name)) + + # This method was inspired by the answers to Stack Overflow post + # http://stackoverflow.com/q/2183233/2988730, especially + # http://stackoverflow.com/a/13638084/2988730 + def logForLevel(self, message, *args, **kwargs): + if self.isEnabledFor(level_num): + self._log(level_num, message, args, **kwargs) + + def logToRoot(message, *args, **kwargs): + logging.log(level_num, message, *args, **kwargs) + + logging.addLevelName(level_num, level_name) + setattr(logging, level_name, level_num) + setattr(logging.getLoggerClass(), method_name, logForLevel) + setattr(logging, method_name, logToRoot) From b002be36f5de70e5912e3997ca04994896129266 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Wed, 20 Nov 2024 17:21:20 +1100 Subject: [PATCH 002/138] feat: web3 client and contract function --- abi_manager.py | 36 -- .../service_node_contribution_factory.py | 59 -- web3client/__init__.py | 0 web3client/abi_manager.py | 83 +++ {abis => web3client/abis}/RewardRatePool.json | 0 web3client/abis/SENT.json | 545 ++++++++++++++++++ .../abis}/ServiceNodeContribution.json | 0 .../abis}/ServiceNodeContributionFactory.json | 0 .../abis}/ServiceNodeRewards.json | 0 web3client/client.py | 76 +++ web3client/contracts/__init__.py | 0 web3client/contracts/contract.py | 29 + .../contracts}/reward_rate_pool.py | 32 +- web3client/contracts/sent.py | 27 + .../contracts}/service_node_contribution.py | 83 +-- .../service_node_contribution_factory.py | 21 + .../contracts}/service_node_rewards.py | 70 ++- 17 files changed, 861 insertions(+), 200 deletions(-) delete mode 100644 abi_manager.py delete mode 100644 contracts/service_node_contribution_factory.py create mode 100644 web3client/__init__.py create mode 100644 web3client/abi_manager.py rename {abis => web3client/abis}/RewardRatePool.json (100%) create mode 100644 web3client/abis/SENT.json rename {abis => web3client/abis}/ServiceNodeContribution.json (100%) rename {abis => web3client/abis}/ServiceNodeContributionFactory.json (100%) rename {abis => web3client/abis}/ServiceNodeRewards.json (100%) create mode 100644 web3client/client.py create mode 100644 web3client/contracts/__init__.py create mode 100644 web3client/contracts/contract.py rename {contracts => web3client/contracts}/reward_rate_pool.py (53%) create mode 100644 web3client/contracts/sent.py rename {contracts => web3client/contracts}/service_node_contribution.py (53%) create mode 100644 web3client/contracts/service_node_contribution_factory.py rename {contracts => web3client/contracts}/service_node_rewards.py (52%) diff --git a/abi_manager.py b/abi_manager.py deleted file mode 100644 index aa679a4..0000000 --- a/abi_manager.py +++ /dev/null @@ -1,36 +0,0 @@ -import json -import os - -class ABIManager: - def __init__(self, abi_dir='abis'): - """ - Initializes the ABIManager with the directory containing ABI JSON files. - - :param abi_dir: The directory where ABI files are stored. Default is 'abis'. - """ - self.abi_dir = abi_dir - - def load_abi(self, artifact_name): - """ - Loads the ABI from a specified artifact JSON file. - - :param artifact_name: The name of the artifact file (without .json extension). - :return: The ABI extracted from the specified artifact JSON file. - :raises FileNotFoundError: If the specified file does not exist. - :raises KeyError: If the 'abi' key is not found in the JSON data. - """ - file_path = os.path.join(self.abi_dir, f"{artifact_name}.json") - if not os.path.exists(file_path): - raise FileNotFoundError(f"No such file: {file_path}") - - with open(file_path, 'r') as file: - data = json.load(file) - if 'abi' not in data: - raise KeyError("Missing 'abi' key in the JSON file.") - return data['abi'] - -# Example usage: -# manager = ABIManager() -# abi = manager.load_abi('MyContract') -# This `abi` can now be used with Web3 library to interact with a smart contract. - diff --git a/contracts/service_node_contribution_factory.py b/contracts/service_node_contribution_factory.py deleted file mode 100644 index f384a27..0000000 --- a/contracts/service_node_contribution_factory.py +++ /dev/null @@ -1,59 +0,0 @@ -from web3 import Web3 -from abi_manager import ABIManager - -class ServiceNodeContributionFactory: - def __init__(self, provider_url, contract_address): - """ - Initialize the connection to the ServiceNodeContributionFactory contract. - - :param provider_url: URL of the Ethereum node to connect to. - :param contract_address: Address of the deployed ServiceNodeContributionFactory contract. - """ - self.web3 = Web3(Web3.HTTPProvider(provider_url)) - self.contract_address = Web3.to_checksum_address(contract_address) - manager = ABIManager() - abi = manager.load_abi('ServiceNodeContributionFactory') - self.contract = self.web3.eth.contract(address=self.contract_address, abi=abi) - self.last_contribution_event_height = 0 - - def max_contributors(self): - """ - Calls the view function to get the maximum number of contributors. - - :return: Maximum number of contributors as integer. - """ - return self.contract.functions.maxContributors().call() - - def designated_token(self): - """ - Calls the view function to get the designated token address. - - :return: Address of the designated token as string. - """ - return self.contract.functions.SENT().call() - - def get_new_contribution_contract_events(self, from_block='latest'): - """ - Retrieves the events of new contribution contracts deployed. - - :param from_block: The block number to start looking for events. - :return: List of events. - """ - return self.contract.events.NewServiceNodeContributionContract.get_logs(from_block=from_block) - - def get_latest_contribution_contract_events(self): - """ - Retrieves the latest events of new contribution contracts deployed. keeping track of when last called - - :return: List of events. - """ - events = self.get_new_contribution_contract_events(self.last_contribution_event_height) - self.last_contribution_event_height = self.web3.eth.block_number - return events - -# Example usage: -# factory_interface = ServiceNodeContributionFactory('http://127.0.0.1:8545', '0x...') -# max_contributors = factory_interface.max_contributors() -# designated_token = factory_interface.designated_token() -# new_contribution_contracts = factory_interface.get_new_contribution_contract_events() - diff --git a/web3client/__init__.py b/web3client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web3client/abi_manager.py b/web3client/abi_manager.py new file mode 100644 index 0000000..8a55fca --- /dev/null +++ b/web3client/abi_manager.py @@ -0,0 +1,83 @@ +import json +import os +from dataclasses import dataclass + + +@dataclass +class ABIData: + name: str + abi: dict + bytecode: bytes + deployed_bytecode: bytes + + +class ABIManager: + + cache = {} + + def __init__(self, db_writer=None, abi_dir="web3client/abis"): + """ + Initializes the ABIManager with the directory containing ABI JSON files. + + :param db_writer: The db writer to use for interacting with the db. + :param abi_dir: The directory where ABI files are stored. Default is 'abis'. + """ + self.abi_dir = abi_dir + if db_writer is not None: + abis = self.load_all_abis() + db_writer.write_smart_contract_abis_to_db(abis) + + def get_abi(self, contract_name): + """ + Gets the ABI for a contract. + """ + if contract_name in self.cache: + return self.cache[contract_name] + + return self.load_abi(contract_name) + + def load_abi(self, file_name): + """ + Loads the ABI from a specified artifact JSON file. + + :param file_name: The name of the artifact file (without .json extension). + :return: The ABI extracted from the specified artifact JSON file. + :raises FileNotFoundError: If the specified file does not exist. + :raises KeyError: If the 'abi' key is not found in the JSON data. + """ + if file_name in self.cache: + return self.cache[file_name] + + file_path = os.path.join(self.abi_dir, "{}.json".format(file_name)) + if not os.path.exists(file_path): + raise FileNotFoundError("No such file: {}".format(file_path)) + + with open(file_path, "r") as file: + data = json.load(file) + if "abi" not in data: + raise KeyError("Missing 'abi' key in the JSON file.") + abi = data["abi"] + name = data["contractName"] + bytecode_bytes = data["bytecode"] + deployed_bytecode_bytes = data["deployedBytecode"] + self.cache[file_name] = abi + return ABIData(name, abi, bytecode_bytes, deployed_bytecode_bytes) + + def load_all_abis(self): + """ + Loads all ABIs from the specified directory. + + :return: A dictionary where the keys are the artifact names and the values are the ABIs. + """ + abis = [] + for file in os.listdir(self.abi_dir): + if file.endswith(".json"): + name = file[:-5] + abis.append(self.load_abi(name)) + return abis + + +# Example usage: +# manager = ABIManager() +# abi = manager.load_abi('MyContract') +# This `abi` can now be used with Web3 library to interact with a smart contract. diff --git a/abis/RewardRatePool.json b/web3client/abis/RewardRatePool.json similarity index 100% rename from abis/RewardRatePool.json rename to web3client/abis/RewardRatePool.json diff --git a/web3client/abis/SENT.json b/web3client/abis/SENT.json new file mode 100644 index 0000000..d2646c2 --- /dev/null +++ b/web3client/abis/SENT.json @@ -0,0 +1,545 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "SENT", + "sourceName": "contracts/SENT.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "uint256", + "name": "totalSupply_", + "type": "uint256" + }, + { + "internalType": "address", + "name": "receiverGenesisAddress", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "ECDSAInvalidSignature", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "name": "ECDSAInvalidSignatureLength", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "ECDSAInvalidSignatureS", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "allowance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "ERC20InsufficientAllowance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "ERC20InsufficientBalance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "approver", + "type": "address" + } + ], + "name": "ERC20InvalidApprover", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "ERC20InvalidReceiver", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "ERC20InvalidSender", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "ERC20InvalidSpender", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "ERC2612ExpiredSignature", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "signer", + "type": "address" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "ERC2612InvalidSigner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "currentNonce", + "type": "uint256" + } + ], + "name": "InvalidAccountNonce", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidShortString", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "str", + "type": "string" + } + ], + "name": "StringTooLong", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [], + "name": "EIP712DomainChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "eip712Domain", + "outputs": [ + { + "internalType": "bytes1", + "name": "fields", + "type": "bytes1" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "version", + "type": "string" + }, + { + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "verifyingContract", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + }, + { + "internalType": "uint256[]", + "name": "extensions", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "nonces", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "permit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x61016060405234801561001157600080fd5b5060405161155c38038061155c8339810160408190526100309161041d565b6040518060400160405280600781526020016629b2b9b9b4b7b760c91b81525080604051806040016040528060018152602001603160f81b8152506040518060400160405280600781526020016629b2b9b9b4b7b760c91b8152506040518060400160405280600481526020016314d1539560e21b81525081600390816100b791906104f9565b5060046100c482826104f9565b506100d491508390506005610248565b610120526100e3816006610248565b61014052815160208084019190912060e052815190820120610100524660a05261017060e05161010051604080517f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f60208201529081019290925260608201524660808201523060a082015260009060c00160405160208183030381529060405280519060200120905090565b60805250503060c05250806001600160a01b0381166101e45760405162461bcd60e51b815260206004820152602560248201527f5368617265643a205a65726f2d61646472657373206973206e6f74207065726d6044820152641a5d1d195960da1b60648201526084015b60405180910390fd5b82806000036102355760405162461bcd60e51b815260206004820152601b60248201527f5368617265643a2075696e7420696e70757420697320656d707479000000000060448201526064016101db565b61023f838561027b565b5050505061064a565b60006020835110156102645761025d836102b5565b9050610275565b8161026f84826104f9565b5060ff90505b92915050565b6001600160a01b0382166102a55760405163ec442f0560e01b8152600060048201526024016101db565b6102b1600083836102f3565b5050565b600080829050601f815111156102e0578260405163305a27a960e01b81526004016101db91906105b7565b80516102eb82610605565b179392505050565b6001600160a01b03831661031e5780600260008282546103139190610629565b909155506103909050565b6001600160a01b038316600090815260208190526040902054818110156103715760405163391434e360e21b81526001600160a01b038516600482015260248101829052604481018390526064016101db565b6001600160a01b03841660009081526020819052604090209082900390555b6001600160a01b0382166103ac576002805482900390556103cb565b6001600160a01b03821660009081526020819052604090208054820190555b816001600160a01b0316836001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8360405161041091815260200190565b60405180910390a3505050565b6000806040838503121561043057600080fd5b825160208401519092506001600160a01b038116811461044f57600080fd5b809150509250929050565b634e487b7160e01b600052604160045260246000fd5b600181811c9082168061048457607f821691505b6020821081036104a457634e487b7160e01b600052602260045260246000fd5b50919050565b601f8211156104f457806000526020600020601f840160051c810160208510156104d15750805b601f840160051c820191505b818110156104f157600081556001016104dd565b50505b505050565b81516001600160401b038111156105125761051261045a565b610526816105208454610470565b846104aa565b6020601f82116001811461055a57600083156105425750848201515b600019600385901b1c1916600184901b1784556104f1565b600084815260208120601f198516915b8281101561058a578785015182556020948501946001909201910161056a565b50848210156105a85786840151600019600387901b60f8161c191681555b50505050600190811b01905550565b602081526000825180602084015260005b818110156105e557602081860181015160408684010152016105c8565b506000604082850101526040601f19601f83011684010191505092915050565b805160208083015191908110156104a45760001960209190910360031b1b16919050565b8082018082111561027557634e487b7160e01b600052601160045260246000fd5b60805160a05160c05160e051610100516101205161014051610eb86106a460003960006106c80152600061069b015260006106430152600061061b01526000610576015260006105a0015260006105ca0152610eb86000f3fe608060405234801561001057600080fd5b50600436106100bf5760003560e01c806370a082311161007c57806370a08231146101415780637ecebe001461016a57806384b0196e1461017d57806395d89b4114610198578063a9059cbb146101a0578063d505accf146101b3578063dd62ed3e146101c857600080fd5b806306fdde03146100c4578063095ea7b3146100e257806318160ddd1461010557806323b872dd14610117578063313ce5671461012a5780633644e51514610139575b600080fd5b6100cc6101db565b6040516100d99190610c0f565b60405180910390f35b6100f56100f0366004610c45565b61026d565b60405190151581526020016100d9565b6002545b6040519081526020016100d9565b6100f5610125366004610c6f565b610287565b604051600981526020016100d9565b6101096102ab565b61010961014f366004610cac565b6001600160a01b031660009081526020819052604090205490565b610109610178366004610cac565b6102ba565b6101856102d8565b6040516100d99796959493929190610cc7565b6100cc61031e565b6100f56101ae366004610c45565b61032d565b6101c66101c1366004610d5f565b61033b565b005b6101096101d6366004610dd2565b61047a565b6060600380546101ea90610e05565b80601f016020809104026020016040519081016040528092919081815260200182805461021690610e05565b80156102635780601f1061023857610100808354040283529160200191610263565b820191906000526020600020905b81548152906001019060200180831161024657829003601f168201915b5050505050905090565b60003361027b8185856104a5565b60019150505b92915050565b6000336102958582856104b7565b6102a085858561050a565b506001949350505050565b60006102b5610569565b905090565b6001600160a01b038116600090815260076020526040812054610281565b6000606080600080600060606102ec610694565b6102f46106c1565b60408051600080825260208201909252600f60f81b9b939a50919850469750309650945092509050565b6060600480546101ea90610e05565b60003361027b81858561050a565b834211156103645760405163313c898160e11b8152600481018590526024015b60405180910390fd5b60007f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c98888886103b18c6001600160a01b0316600090815260076020526040902080546001810190915590565b6040805160208101969096526001600160a01b0394851690860152929091166060840152608083015260a082015260c0810186905260e001604051602081830303815290604052805190602001209050600061040c826106ee565b9050600061041c8287878761071b565b9050896001600160a01b0316816001600160a01b031614610463576040516325c0072360e11b81526001600160a01b0380831660048301528b16602482015260440161035b565b61046e8a8a8a6104a5565b50505050505050505050565b6001600160a01b03918216600090815260016020908152604080832093909416825291909152205490565b6104b28383836001610749565b505050565b60006104c3848461047a565b9050600019811461050457818110156104f557828183604051637dc7a0d960e11b815260040161035b93929190610e3f565b61050484848484036000610749565b50505050565b6001600160a01b038316610534576000604051634b637e8f60e11b815260040161035b9190610e60565b6001600160a01b03821661055e57600060405163ec442f0560e01b815260040161035b9190610e60565b6104b283838361081e565b6000306001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000161480156105c257507f000000000000000000000000000000000000000000000000000000000000000046145b156105ec57507f000000000000000000000000000000000000000000000000000000000000000090565b6102b5604080517f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f60208201527f0000000000000000000000000000000000000000000000000000000000000000918101919091527f000000000000000000000000000000000000000000000000000000000000000060608201524660808201523060a082015260009060c00160405160208183030381529060405280519060200120905090565b60606102b57f00000000000000000000000000000000000000000000000000000000000000006005610935565b60606102b57f00000000000000000000000000000000000000000000000000000000000000006006610935565b60006102816106fb610569565b8360405161190160f01b8152600281019290925260228201526042902090565b60008060008061072d888888886109e0565b92509250925061073d8282610aa5565b50909695505050505050565b6001600160a01b03841661077357600060405163e602df0560e01b815260040161035b9190610e60565b6001600160a01b03831661079d576000604051634a1406b160e11b815260040161035b9190610e60565b6001600160a01b038085166000908152600160209081526040808320938716835292905220829055801561050457826001600160a01b0316846001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9258460405161081091815260200190565b60405180910390a350505050565b6001600160a01b03831661084957806002600082825461083e9190610e74565b909155506108a89050565b6001600160a01b038316600090815260208190526040902054818110156108895783818360405163391434e360e21b815260040161035b93929190610e3f565b6001600160a01b03841660009081526020819052604090209082900390555b6001600160a01b0382166108c4576002805482900390556108e3565b6001600160a01b03821660009081526020819052604090208054820190555b816001600160a01b0316836001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8360405161092891815260200190565b60405180910390a3505050565b606060ff831461094f5761094883610b62565b9050610281565b81805461095b90610e05565b80601f016020809104026020016040519081016040528092919081815260200182805461098790610e05565b80156109d45780601f106109a9576101008083540402835291602001916109d4565b820191906000526020600020905b8154815290600101906020018083116109b757829003601f168201915b50505050509050610281565b600080806fa2a8918ca85bafe22016d0b997e4df60600160ff1b03841115610a115750600091506003905082610a9b565b604080516000808252602082018084528a905260ff891692820192909252606081018790526080810186905260019060a0016020604051602081039080840390855afa158015610a65573d6000803e3d6000fd5b5050604051601f1901519150506001600160a01b038116610a9157506000925060019150829050610a9b565b9250600091508190505b9450945094915050565b6000826003811115610ab957610ab9610e95565b03610ac2575050565b6001826003811115610ad657610ad6610e95565b03610af45760405163f645eedf60e01b815260040160405180910390fd5b6002826003811115610b0857610b08610e95565b03610b295760405163fce698f760e01b81526004810182905260240161035b565b6003826003811115610b3d57610b3d610e95565b03610b5e576040516335e2f38360e21b81526004810182905260240161035b565b5050565b60606000610b6f83610ba1565b604080516020808252818301909252919250600091906020820181803683375050509182525060208101929092525090565b600060ff8216601f81111561028157604051632cd44ac360e21b815260040160405180910390fd5b6000815180845260005b81811015610bef57602081850181015186830182015201610bd3565b506000602082860101526020601f19601f83011685010191505092915050565b602081526000610c226020830184610bc9565b9392505050565b80356001600160a01b0381168114610c4057600080fd5b919050565b60008060408385031215610c5857600080fd5b610c6183610c29565b946020939093013593505050565b600080600060608486031215610c8457600080fd5b610c8d84610c29565b9250610c9b60208501610c29565b929592945050506040919091013590565b600060208284031215610cbe57600080fd5b610c2282610c29565b60ff60f81b8816815260e060208201526000610ce660e0830189610bc9565b8281036040840152610cf88189610bc9565b606084018890526001600160a01b038716608085015260a0840186905283810360c08501528451808252602080870193509091019060005b81811015610d4e578351835260209384019390920191600101610d30565b50909b9a5050505050505050505050565b600080600080600080600060e0888a031215610d7a57600080fd5b610d8388610c29565b9650610d9160208901610c29565b95506040880135945060608801359350608088013560ff81168114610db557600080fd5b9699959850939692959460a0840135945060c09093013592915050565b60008060408385031215610de557600080fd5b610dee83610c29565b9150610dfc60208401610c29565b90509250929050565b600181811c90821680610e1957607f821691505b602082108103610e3957634e487b7160e01b600052602260045260246000fd5b50919050565b6001600160a01b039390931683526020830191909152604082015260600190565b6001600160a01b0391909116815260200190565b8082018082111561028157634e487b7160e01b600052601160045260246000fd5b634e487b7160e01b600052602160045260246000fdfea164736f6c634300081a000a", + "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100bf5760003560e01c806370a082311161007c57806370a08231146101415780637ecebe001461016a57806384b0196e1461017d57806395d89b4114610198578063a9059cbb146101a0578063d505accf146101b3578063dd62ed3e146101c857600080fd5b806306fdde03146100c4578063095ea7b3146100e257806318160ddd1461010557806323b872dd14610117578063313ce5671461012a5780633644e51514610139575b600080fd5b6100cc6101db565b6040516100d99190610c0f565b60405180910390f35b6100f56100f0366004610c45565b61026d565b60405190151581526020016100d9565b6002545b6040519081526020016100d9565b6100f5610125366004610c6f565b610287565b604051600981526020016100d9565b6101096102ab565b61010961014f366004610cac565b6001600160a01b031660009081526020819052604090205490565b610109610178366004610cac565b6102ba565b6101856102d8565b6040516100d99796959493929190610cc7565b6100cc61031e565b6100f56101ae366004610c45565b61032d565b6101c66101c1366004610d5f565b61033b565b005b6101096101d6366004610dd2565b61047a565b6060600380546101ea90610e05565b80601f016020809104026020016040519081016040528092919081815260200182805461021690610e05565b80156102635780601f1061023857610100808354040283529160200191610263565b820191906000526020600020905b81548152906001019060200180831161024657829003601f168201915b5050505050905090565b60003361027b8185856104a5565b60019150505b92915050565b6000336102958582856104b7565b6102a085858561050a565b506001949350505050565b60006102b5610569565b905090565b6001600160a01b038116600090815260076020526040812054610281565b6000606080600080600060606102ec610694565b6102f46106c1565b60408051600080825260208201909252600f60f81b9b939a50919850469750309650945092509050565b6060600480546101ea90610e05565b60003361027b81858561050a565b834211156103645760405163313c898160e11b8152600481018590526024015b60405180910390fd5b60007f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c98888886103b18c6001600160a01b0316600090815260076020526040902080546001810190915590565b6040805160208101969096526001600160a01b0394851690860152929091166060840152608083015260a082015260c0810186905260e001604051602081830303815290604052805190602001209050600061040c826106ee565b9050600061041c8287878761071b565b9050896001600160a01b0316816001600160a01b031614610463576040516325c0072360e11b81526001600160a01b0380831660048301528b16602482015260440161035b565b61046e8a8a8a6104a5565b50505050505050505050565b6001600160a01b03918216600090815260016020908152604080832093909416825291909152205490565b6104b28383836001610749565b505050565b60006104c3848461047a565b9050600019811461050457818110156104f557828183604051637dc7a0d960e11b815260040161035b93929190610e3f565b61050484848484036000610749565b50505050565b6001600160a01b038316610534576000604051634b637e8f60e11b815260040161035b9190610e60565b6001600160a01b03821661055e57600060405163ec442f0560e01b815260040161035b9190610e60565b6104b283838361081e565b6000306001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000161480156105c257507f000000000000000000000000000000000000000000000000000000000000000046145b156105ec57507f000000000000000000000000000000000000000000000000000000000000000090565b6102b5604080517f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f60208201527f0000000000000000000000000000000000000000000000000000000000000000918101919091527f000000000000000000000000000000000000000000000000000000000000000060608201524660808201523060a082015260009060c00160405160208183030381529060405280519060200120905090565b60606102b57f00000000000000000000000000000000000000000000000000000000000000006005610935565b60606102b57f00000000000000000000000000000000000000000000000000000000000000006006610935565b60006102816106fb610569565b8360405161190160f01b8152600281019290925260228201526042902090565b60008060008061072d888888886109e0565b92509250925061073d8282610aa5565b50909695505050505050565b6001600160a01b03841661077357600060405163e602df0560e01b815260040161035b9190610e60565b6001600160a01b03831661079d576000604051634a1406b160e11b815260040161035b9190610e60565b6001600160a01b038085166000908152600160209081526040808320938716835292905220829055801561050457826001600160a01b0316846001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9258460405161081091815260200190565b60405180910390a350505050565b6001600160a01b03831661084957806002600082825461083e9190610e74565b909155506108a89050565b6001600160a01b038316600090815260208190526040902054818110156108895783818360405163391434e360e21b815260040161035b93929190610e3f565b6001600160a01b03841660009081526020819052604090209082900390555b6001600160a01b0382166108c4576002805482900390556108e3565b6001600160a01b03821660009081526020819052604090208054820190555b816001600160a01b0316836001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8360405161092891815260200190565b60405180910390a3505050565b606060ff831461094f5761094883610b62565b9050610281565b81805461095b90610e05565b80601f016020809104026020016040519081016040528092919081815260200182805461098790610e05565b80156109d45780601f106109a9576101008083540402835291602001916109d4565b820191906000526020600020905b8154815290600101906020018083116109b757829003601f168201915b50505050509050610281565b600080806fa2a8918ca85bafe22016d0b997e4df60600160ff1b03841115610a115750600091506003905082610a9b565b604080516000808252602082018084528a905260ff891692820192909252606081018790526080810186905260019060a0016020604051602081039080840390855afa158015610a65573d6000803e3d6000fd5b5050604051601f1901519150506001600160a01b038116610a9157506000925060019150829050610a9b565b9250600091508190505b9450945094915050565b6000826003811115610ab957610ab9610e95565b03610ac2575050565b6001826003811115610ad657610ad6610e95565b03610af45760405163f645eedf60e01b815260040160405180910390fd5b6002826003811115610b0857610b08610e95565b03610b295760405163fce698f760e01b81526004810182905260240161035b565b6003826003811115610b3d57610b3d610e95565b03610b5e576040516335e2f38360e21b81526004810182905260240161035b565b5050565b60606000610b6f83610ba1565b604080516020808252818301909252919250600091906020820181803683375050509182525060208101929092525090565b600060ff8216601f81111561028157604051632cd44ac360e21b815260040160405180910390fd5b6000815180845260005b81811015610bef57602081850181015186830182015201610bd3565b506000602082860101526020601f19601f83011685010191505092915050565b602081526000610c226020830184610bc9565b9392505050565b80356001600160a01b0381168114610c4057600080fd5b919050565b60008060408385031215610c5857600080fd5b610c6183610c29565b946020939093013593505050565b600080600060608486031215610c8457600080fd5b610c8d84610c29565b9250610c9b60208501610c29565b929592945050506040919091013590565b600060208284031215610cbe57600080fd5b610c2282610c29565b60ff60f81b8816815260e060208201526000610ce660e0830189610bc9565b8281036040840152610cf88189610bc9565b606084018890526001600160a01b038716608085015260a0840186905283810360c08501528451808252602080870193509091019060005b81811015610d4e578351835260209384019390920191600101610d30565b50909b9a5050505050505050505050565b600080600080600080600060e0888a031215610d7a57600080fd5b610d8388610c29565b9650610d9160208901610c29565b95506040880135945060608801359350608088013560ff81168114610db557600080fd5b9699959850939692959460a0840135945060c09093013592915050565b60008060408385031215610de557600080fd5b610dee83610c29565b9150610dfc60208401610c29565b90509250929050565b600181811c90821680610e1957607f821691505b602082108103610e3957634e487b7160e01b600052602260045260246000fd5b50919050565b6001600160a01b039390931683526020830191909152604082015260600190565b6001600160a01b0391909116815260200190565b8082018082111561028157634e487b7160e01b600052601160045260246000fd5b634e487b7160e01b600052602160045260246000fdfea164736f6c634300081a000a", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/abis/ServiceNodeContribution.json b/web3client/abis/ServiceNodeContribution.json similarity index 100% rename from abis/ServiceNodeContribution.json rename to web3client/abis/ServiceNodeContribution.json diff --git a/abis/ServiceNodeContributionFactory.json b/web3client/abis/ServiceNodeContributionFactory.json similarity index 100% rename from abis/ServiceNodeContributionFactory.json rename to web3client/abis/ServiceNodeContributionFactory.json diff --git a/abis/ServiceNodeRewards.json b/web3client/abis/ServiceNodeRewards.json similarity index 100% rename from abis/ServiceNodeRewards.json rename to web3client/abis/ServiceNodeRewards.json diff --git a/web3client/client.py b/web3client/client.py new file mode 100644 index 0000000..248027d --- /dev/null +++ b/web3client/client.py @@ -0,0 +1,76 @@ +import logging + +import eth_utils +from web3 import Web3 +from web3.contract.contract import ContractFunction +from web3client.abi_manager import ABIManager + + +class Web3Client: + def __init__( + self, + provider_url: str, + caller_address: str | None, + private_key: str | None, + logger: logging, + abi_manager: ABIManager = ABIManager(), + ): + """ + Initialize the web3 client. + + :param provider_url: URL of the Ethereum node to connect to. + :param caller_address: Address of the caller. + :param private_key: Private key of the caller. + """ + self.web3 = Web3(Web3.HTTPProvider(provider_url)) + self.abi_manager = abi_manager + self.chain_id = self.web3.eth.chain_id + + if provider_url is None: + raise ValueError("Provider URL is None") + self.provider_url = provider_url + + self.private_key = private_key + if private_key is None: + logger.warning("private_key is None, contract writes will be disabled") + + if caller_address is not None: + if not eth_utils.is_checksum_address(caller_address): + raise ValueError("Caller address is not a checksum address") + check_address = self.web3.eth.account.from_key(private_key).address + if check_address != caller_address: + raise ValueError("Private key address does not match the caller address") + self.caller = eth_utils.to_checksum_address(caller_address) + else: + logger.warning("caller_address is None, contract writes will be disabled") + self.caller = None + + def get_nonce(self): + return self.web3.eth.get_transaction_count(self.caller) + + def contract_write(self, contract_function: ContractFunction, args: tuple): + if self.caller is None: + logging.warning("No contract caller, write is disabled") + return None + try: + # Call your function + address_to, amount = args + call_function = contract_function(address_to, amount).build_transaction( + {"chainId": self.chain_id, "from": self.caller, "nonce": self.get_nonce()} + ) + + # Sign transaction + signed_tx = self.web3.eth.account.sign_transaction( + call_function, private_key=self.private_key + ) + + # Send transaction + send_tx = self.web3.eth.send_raw_transaction(signed_tx.rawTransaction) + + # Wait for transaction receipt + tx_hash = self.web3.eth.wait_for_transaction_receipt(send_tx).get("transactionHash") + + return tx_hash.hex() + except Exception as e: + logging.error("Error: {}".format(e)) + return None diff --git a/web3client/contracts/__init__.py b/web3client/contracts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web3client/contracts/contract.py b/web3client/contracts/contract.py new file mode 100644 index 0000000..af92944 --- /dev/null +++ b/web3client/contracts/contract.py @@ -0,0 +1,29 @@ +from web3 import Web3 + +from log import Log +from web3client.abi_manager import ABIManager +from web3client.client import Web3Client + + +class ContractInterface: + def __init__( + self, + web3_client: Web3Client, + contract_address: str, + contract_abi_name: str, + ): + """ + Initialize the connection to a contract. + + :param web3_client: The web3 client to use for interacting with the contract. + :param contract_address: Address of the deployed contract. + :param contract_abi_name: Path to the ABI file for the contract. + """ + # Initialize address nonce + + self.web3_client = web3_client + self.contract_address = Web3.to_checksum_address(contract_address) + abi = self.web3_client.abi_manager.get_abi(contract_abi_name) + # TODO: This call takes a while (~5ms) so we should see if we can cache it and reuse it for other instances with the same abi but different addresses + self.contract = web3_client.web3.eth.contract(address=self.contract_address, abi=abi) + self.address_map = {} diff --git a/contracts/reward_rate_pool.py b/web3client/contracts/reward_rate_pool.py similarity index 53% rename from contracts/reward_rate_pool.py rename to web3client/contracts/reward_rate_pool.py index 0899702..33244b6 100644 --- a/contracts/reward_rate_pool.py +++ b/web3client/contracts/reward_rate_pool.py @@ -1,17 +1,12 @@ -from web3 import Web3 -from abi_manager import ABIManager +from web3client.client import Web3Client +from web3client.contracts.contract import ContractInterface -class RewardRatePoolInterface: - def __init__(self, provider_url, contract_address): - """ - Initialize the connection to the Ethereum provider and set up the contract. - :param provider_url: URL of the Ethereum node to connect to. - :param contract_address: Address of the RewardRatePool smart contract. - """ - self.web3 = Web3(Web3.HTTPProvider(provider_url)) - manager = ABIManager() - abi = manager.load_abi('RewardRatePool') - self.contract = self.web3.eth.contract(address=Web3.to_checksum_address(contract_address), abi=abi) + +class RewardRatePoolInterface(ContractInterface): + abi_name = "RewardRatePool" + + def __init__(self, web3_client: Web3Client, contract_address: str): + super().__init__(web3_client, contract_address, RewardRatePoolInterface.abi_name) def calculate_total_deposited(self): """ @@ -40,14 +35,3 @@ def reward_rate(self, timestamp): :param timestamp: The timestamp for which to calculate the reward rate. """ return self.contract.functions.rewardRate(timestamp).call() - -# Example usage: -# provider_url = 'http://127.0.0.1:8545' -# contract_address = '0x...' - -# reward_rate_pool = RewardRatePoolInterface(provider_url, contract_address) -# total_deposited = reward_rate_pool.calculate_total_deposited() -# print("Total Deposited:", total_deposited) -# reward_rate = reward_rate_pool.reward_rate(Web3.toInt(text="latest")) -# print("Reward Rate at Latest:", reward_rate) - diff --git a/web3client/contracts/sent.py b/web3client/contracts/sent.py new file mode 100644 index 0000000..d1ea404 --- /dev/null +++ b/web3client/contracts/sent.py @@ -0,0 +1,27 @@ +from web3client.client import Web3Client +from web3client.contracts.contract import ContractInterface + + +class SENTInterface(ContractInterface): + abi_name = "SENT" + + def __init__(self, web3_client: Web3Client, contract_address: str): + super().__init__(web3_client, contract_address, SENTInterface.abi_name) + + def transfer(self, amount: int, address_to: str): + """ + Calls the transfer function on the ERC20 token contract + + :return: receipt + """ + return self.web3_client.contract_write( + self.contract.functions.transfer, (address_to, amount) + ) + + def balance_of(self, address: str): + """ + Calls the balanceOf function on the ERC20 token contract + + :return: balance + """ + return self.contract.functions.balanceOf(address).call() diff --git a/contracts/service_node_contribution.py b/web3client/contracts/service_node_contribution.py similarity index 53% rename from contracts/service_node_contribution.py rename to web3client/contracts/service_node_contribution.py index 90badbf..f91b0f8 100644 --- a/contracts/service_node_contribution.py +++ b/web3client/contracts/service_node_contribution.py @@ -1,36 +1,16 @@ +import time +from dataclasses import dataclass + from web3 import Web3 -from abi_manager import ABIManager +from web3client.client import Web3Client +from web3client.contracts.contract import ContractInterface -class ContributorContractInterface: - """ Parent class to handle Web3 connection and load ABI for contracts. """ - def __init__(self, provider_url): - """ - Initialize the connection to the Ethereum provider. - :param provider_url: URL of the Ethereum node to connect to. - """ - self.web3 = Web3(Web3.HTTPProvider(provider_url)) - manager = ABIManager() - self.abi = manager.load_abi('ServiceNodeContribution') +class ServiceNodeContributionInterface(ContractInterface): + abi_name = "ServiceNodeContribution" - def get_contract_instance(self, contract_address): - """ - Create an instance of a contract at a given address. - :param contract_address: Address of the contract to interact with. - :return: Web3 Contract object. - """ - contract = self.web3.eth.contract(address=Web3.to_checksum_address(contract_address), abi=self.abi) - return ServiceNodeContribution(contract) - -class ServiceNodeContribution: - """ Child class to interact with specific Service Node Contribution contracts. """ - - def __init__(self, contract): - """ - Initialize the contract interaction class with the contract. - :param contract: Web3 Contract object. - """ - self.contract = contract + def __init__(self, web3_client: Web3Client, contract_address: str): + super().__init__(web3_client, contract_address, ServiceNodeContributionInterface.abi_name) def get_contributor_contribution(self, contributor_address): """ @@ -38,7 +18,9 @@ def get_contributor_contribution(self, contributor_address): :param contributor_address: Address of the contributor. :return: Contribution amount of the specified contributor. """ - return self.contract.functions.contributions(Web3.to_checksum_address(contributor_address)).call() + return self.contract.functions.contributions( + Web3.to_checksum_address(contributor_address) + ).call() def status(self): """ @@ -90,9 +72,9 @@ def get_service_node_params(self): """ params = self.contract.functions.serviceNodeParams().call() return { - 'serviceNodePubkey': f"{params[0]:032x}", - 'serviceNodeSignature': f"{params[1]:032x}{params[2]:032x}", - 'fee': params[3] + "serviceNodePubkey": f"{params[0]:032x}", + "serviceNodeSignature": f"{params[1]:032x}{params[2]:032x}", + "fee": params[3], } def get_operator(self): @@ -102,31 +84,30 @@ def get_operator(self): return self.contract.functions.operator().call() def get_contributions(self): - contributions = self.contract.functions.getContributions().call() # (address[] memory addrs, address[] memory beneficiaries, uint256[] memory contribs) + contributions = self.contract.functions.getContributions().call() addresses = contributions[0] beneficiaries = contributions[1] contributions = contributions[2] contributions_list = [] for i in range(len(addresses)): - contributions_list.append({ - "address": addresses[i], - "amount": contributions[i], - "beneficiary": beneficiaries[i] - }) + contributions_list.append( + { + "address": addresses[i], + "amount": contributions[i], + "beneficiary": beneficiaries[i], + } + ) return contributions_list + @staticmethod + def add_details_fetch_to_batch_added_batches(): + return 5 -# Example usage: -# provider_url = 'http://127.0.0.1:8545' -# contract_address = '0x...' - -# contract_interface = ContributorContractInterface(provider_url) -# service_node = contract_interface.get_contract_instance(contract_address) - -# Fetch and display data from the contract -# print("Total Contribution:", service_node.total_contribution()) -# print("Is Finalized:", service_node.is_finalized()) -# print("Is Cancelled:", service_node.is_cancelled()) -# print("Minimum Contribution Required:", service_node.minimum_contribution()) + def add_details_fetch_to_batch(self, batch): + batch.add(self.contract.functions.serviceNodeParams()) + batch.add(self.contract.functions.operator()) + batch.add(self.contract.functions.blsPubkey()) + batch.add(self.contract.functions.getContributions()) + batch.add(self.contract.functions.status()) diff --git a/web3client/contracts/service_node_contribution_factory.py b/web3client/contracts/service_node_contribution_factory.py new file mode 100644 index 0000000..2b80ea2 --- /dev/null +++ b/web3client/contracts/service_node_contribution_factory.py @@ -0,0 +1,21 @@ +from web3client.client import Web3Client +from web3client.contracts.contract import ContractInterface +from web3client.event_scanner import EventScanner + + +class ServiceNodeContributionFactory(ContractInterface): + abi_name = "ServiceNodeContributionFactory" + + def __init__(self, web3_client: Web3Client, contract_address: str, scanner_safety_blocks: int): + super().__init__(web3_client, contract_address, ServiceNodeContributionFactory.abi_name) + self.event_scanner = EventScanner( + provider_url=web3_client.provider_url, + events=[ + self.contract.events.NewServiceNodeContributionContract, + ], + filters={"address": self.contract_address}, + # How many maximum blocks at the time we request from JSON-RPC + # and we are unlikely to exceed the response size limit of the JSON-RPC server + max_chunk_scan_size=10_000_000, + safety_blocks=scanner_safety_blocks, + ) diff --git a/contracts/service_node_rewards.py b/web3client/contracts/service_node_rewards.py similarity index 52% rename from contracts/service_node_rewards.py rename to web3client/contracts/service_node_rewards.py index 0a26f3a..60dd8b8 100644 --- a/contracts/service_node_rewards.py +++ b/web3client/contracts/service_node_rewards.py @@ -1,32 +1,41 @@ -from web3 import Web3 -from abi_manager import ABIManager +from web3client.client import Web3Client +from web3client.contracts.contract import ContractInterface +from web3client.event_scanner import EventScanner + class ServiceNodeRewardsRecipient: def __init__(self): self.rewards = 0 self.claimed = 0 + class ServiceNodeRewardsMapEntry: def __init__(self): - self.value = ServiceNodeRewardsRecipient() - self.height = 0; - -class ServiceNodeRewardsInterface: - def __init__(self, provider_url: str, contract_address: str): - """ - Initialize the connection to the ServiceNodeContributionFactory contract. - - :param provider_url: URL of the Ethereum node to connect to. - :param contract_address: Address of the deployed ServiceNodeContributionFactory contract. - """ - self.web3 = Web3(Web3.HTTPProvider(provider_url)) - self.contract_address = Web3.to_checksum_address(contract_address) - manager = ABIManager() - abi = manager.load_abi('ServiceNodeRewards') - self.contract = self.web3.eth.contract(address=self.contract_address, abi=abi) - self.address_map = {} - - def allServiceNodeIDs(self): + self.value = ServiceNodeRewardsRecipient() + self.height = 0 + + +class ServiceNodeRewardsInterface(ContractInterface): + abi_name = "ServiceNodeRewards" + + def __init__(self, web3_client: Web3Client, contract_address: str, scanner_safety_blocks: int): + super().__init__(web3_client, contract_address, ServiceNodeRewardsInterface.abi_name) + self.event_scanner = EventScanner( + provider_url=web3_client.provider_url, + events=[ + self.contract.events.NewServiceNodeV2, + self.contract.events.ServiceNodeExitRequest, + self.contract.events.ServiceNodeExit, + self.contract.events.ServiceNodeLiquidated, + ], + filters={"address": self.contract_address}, + # How many maximum blocks at the time we request from JSON-RPC + # and we are unlikely to exceed the response size limit of the JSON-RPC server + max_chunk_scan_size=10_000_000, + safety_blocks=scanner_safety_blocks, + ) + + def get_all_service_node_contract_ids(self): """ Calls the allServiceNodeIds function to get the `id` and `bls_key` lists @@ -55,7 +64,7 @@ def recipients(self, eth_address: bytes) -> ServiceNodeRewardsRecipient: # Retrieve the recipient entry and check if enough time has elapsed to # update the entry, otherwise return the cached entry - entry = self.address_map[eth_address] + entry = self.address_map[eth_address] result = entry.value if entry.height >= height: return result @@ -63,24 +72,25 @@ def recipients(self, eth_address: bytes) -> ServiceNodeRewardsRecipient: # NOTE: Assuming a block time of 0.25s, we want a 30s block buffer. # TODO: This value is copied from oxen-core of the same name # `SAFE_BLOCKS` - SAFE_BLOCKS = 30 / 0.25; + SAFE_BLOCKS = 30 / 0.25 blocks_since_last_update = height - entry.height if blocks_since_last_update < SAFE_BLOCKS: return result # Enough blocks has elapsed, query the rewards from the contract - call_result = self.contract.functions.recipients(eth_address).call(block_identifier=height) + call_result = self.contract.functions.recipients(eth_address).call(block_identifier=height) result.rewards = call_result[0] result.claimed = call_result[1] - assert result.claimed <= result.rewards, "Contract returned that wallet '{}' claimed {} more than the rewards {} allocated to it!".format(eth_address.decode('utf-8'), - result.rewards, - result.claimed) - + assert ( + result.claimed <= result.rewards + ), "Contract returned that wallet '{}' claimed {} more than the rewards {} allocated to it!".format( + eth_address.decode("utf-8"), result.rewards, result.claimed + ) # Assign the updated entry back into the cache - entry.height = height - entry.value = result + entry.height = height + entry.value = result self.address_map[eth_address] = entry return result From c44989c53ec8592a1a64516d4651790cd00bf19a Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Wed, 20 Nov 2024 17:21:33 +1100 Subject: [PATCH 003/138] feat: eth event scanner --- web3client/event_scanner.py | 369 ++++++++++++++++++++++++++++++++++++ 1 file changed, 369 insertions(+) create mode 100644 web3client/event_scanner.py diff --git a/web3client/event_scanner.py b/web3client/event_scanner.py new file mode 100644 index 0000000..ebdad39 --- /dev/null +++ b/web3client/event_scanner.py @@ -0,0 +1,369 @@ +# Extended from https://web3py.readthedocs.io/en/stable/filters.html#example-code +"""A stateful event scanner for Ethereum-based blockchains using web3.py. + +With the stateful mechanism, you can do one batch scan or incremental scans, +where events are added wherever the scanner left off. +""" + +import time +import logging +from dataclasses import dataclass +from typing import Tuple, Optional, Callable, List, Iterable, Dict, Any + +from web3 import Web3, HTTPProvider +from web3.datastructures import AttributeDict +from web3.exceptions import Web3TypeError +from eth_abi.codec import ABICodec + +# Currently this method is not exposed over official web3 API, +# but we need it to construct eth_getLogs parameters +from web3._utils.filters import construct_event_filter_params +from web3._utils.events import get_event_data + +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + + +@dataclass +class ProcessedEvent: + tx: str + block: int + name: str + args: dict + + +class EventScanner: + """Scan blockchain for events and try not to abuse JSON-RPC API too much. + + Can be used for real-time scans, as it detects minor chain reorganisation and rescans. + Unlike the easy web3.contract.Contract, this scanner can scan events from multiple contracts at once. + For example, you can get all transfers from all tokens in the same scan. + + You *should* disable the default ``exception_retry_configuration`` on your provider for Web3, + because it cannot correctly throttle and decrease the `eth_getLogs` block number range. + """ + + def __init__( + self, + provider_url: str, + events: List, + filters: Dict[str, Any], + max_chunk_scan_size: int = 10000, + max_request_retries: int = 10, + request_retry_seconds: float = 2.0, + safety_blocks: int = 10, + ): + """ + :param events: List of web3 Event we scan + :param filters: Filters passed to get_logs + :param max_chunk_scan_size: JSON-RPC API limit in the number of blocks we query. (Recommendation: 10,000 for mainnet, 500,000 for testnets) + :param max_request_retries: How many times we try to reattempt a failed JSON-RPC call + :param request_retry_seconds: Delay between failed requests to let JSON-RPC server to recover + """ + + self.logger = logger + provider = HTTPProvider(provider_url) + # Disable the default JSON-RPC retry configuration + # as it correctly cannot handle eth_getLogs block range + provider.exception_retry_configuration = None + self.w3 = Web3(provider) + self.events = events + self.filters = filters + + # Our JSON-RPC throttling parameters + # self.min_scan_chunk_size = 10 # 12 s/block = 120 seconds period + self.min_scan_chunk_size = 10000 # 12 s/block = 120 seconds period + self.max_scan_chunk_size = max_chunk_scan_size + self.max_request_retries = max_request_retries + self.request_retry_seconds = request_retry_seconds + + self.safety_blocks = safety_blocks + + # Factor how fast we increase the chunk size if results are found + # # (slow down scan after starting to get hits) + self.chunk_size_decrease = 0.5 + + # Factor how fast we increase chunk size if no results found + # self.chunk_size_increase = 2.0 + self.chunk_size_increase = 10.0 + + # Start low at 20, this value is set at the end of the scan so we can use the optimal chunk size in future scans + self.optimal_chunk_size = 20 + + def scan_chunk(self, start_block, end_block) -> Tuple[int, list]: + """Read and process events between to block numbers. + + Dynamically decrease the size of the chunk if the case JSON-RPC server pukes out. + + :return: tuple(actual end block number, when this block was mined, processed events) + """ + + all_processed = [] + + # TODO: investigate sending all event types in a single call instead of looping over them + for event_type in self.events: + + # Callable that takes care of the underlying web3 call + def _fetch_events(_start_block, _end_block): + return _fetch_events_for_all_contracts( + self.w3, event_type, self.filters, from_block=_start_block, to_block=_end_block + ) + + # Do `n` retries on `eth_getLogs`, + # throttle down block range if needed + end_block, events = _retry_web3_call( + _fetch_events, + start_block=start_block, + end_block=end_block, + retries=self.max_request_retries, + delay=self.request_retry_seconds, + ) + + for evt in events: + idx = evt[ + "logIndex" + ] # Integer of the log index position in the block, null when its pending + + # We cannot avoid minor chain reorganisations, but + # at least we must avoid blocks that are not mined yet + assert idx is not None, "Somehow tried to scan a pending block" + + logger.debug(f"Processing event {evt['event']}, block: {evt['blockNumber']}") + + processed = self.process_event(evt) + all_processed.append(processed) + + return end_block, all_processed + + def estimate_next_chunk_size(self, current_chuck_size: int, event_found_count: int): + """Try to figure out optimal chunk size + + Our scanner might need to scan the whole blockchain for all events + + * We want to minimize API calls over empty blocks + + * We want to make sure that one scan chunk does not try to process too many entries once, as we try to control commit buffer size and potentially asynchronous busy loop + + * Do not overload node serving JSON-RPC API by asking data for too many events at a time + + Currently Ethereum JSON-API does not have an API to tell when a first event occurred in a blockchain + and our heuristics try to accelerate block fetching (chunk size) until we see the first event. + + These heuristics exponentially increase the scan chunk size depending on if we are seeing events or not. + When any transfers are encountered, we are back to scanning only a few blocks at a time. + It does not make sense to do a full chain scan starting from block 1, doing one JSON-RPC call per 20 blocks. + """ + + if event_found_count > 0: + # When we encounter first events, reset the chunk size window + current_chuck_size = self.min_scan_chunk_size + else: + current_chuck_size *= self.chunk_size_increase + + current_chuck_size = max(self.min_scan_chunk_size, current_chuck_size) + current_chuck_size = min(self.max_scan_chunk_size, current_chuck_size) + return int(current_chuck_size) + + def scan( + self, start_block, end_block, progress_callback=Optional[Callable] + ) -> Tuple[list, int]: + """Perform a token balances scan. + + Assumes all balances in the database are valid before start_block (no forks sneaked in). + + :param start_block: The first block included in the scan + + :param end_block: The last block included in the scan + + :param start_chunk_size: How many blocks we try to fetch over JSON-RPC on the first attempt + + :param progress_callback: If this is an UI application, update the progress of the scan + + :return: [All processed events, number of chunks used] + """ + + assert start_block <= end_block + + current_block = start_block + + # Scan in chunks, commit between + chunk_size = self.optimal_chunk_size + last_scan_duration = last_logs_found = 0 + total_chunks_scanned = 0 + + # All processed entries we got on this scan cycle + all_processed = [] + + while current_block <= end_block: + + # Print some diagnostics to logs to try to fiddle with real world JSON-RPC API performance + estimated_end_block = min(current_block + chunk_size, end_block) + logger.debug( + f"Scanning events for blocks: {current_block} - {estimated_end_block}, chunk size {chunk_size}, last chunk scan took {last_scan_duration}, last logs found {last_logs_found}" + ) + + start = time.time() + actual_end_block, new_entries = self.scan_chunk(current_block, estimated_end_block) + + # Where does our current chunk scan ends - are we out of chain yet? + current_end = actual_end_block + + last_scan_duration = time.time() - start + all_processed += new_entries + + if progress_callback: + progress_callback( + start_block, + end_block, + current_block, + chunk_size, + len(new_entries), + ) + + # Try to guess how many blocks to fetch over `eth_getLogs` API next time + chunk_size = self.estimate_next_chunk_size(chunk_size, len(new_entries)) + + # Set where the next chunk starts + current_block = current_end + 1 + total_chunks_scanned += 1 + + self.optimal_chunk_size = chunk_size + return all_processed, total_chunks_scanned + + def process_event(self, event: AttributeDict) -> ProcessedEvent: + """Record a ERC-20 transfer in our database.""" + # Events are keyed by their transaction hash and log index + # One transaction may contain multiple events + # and each one of those gets their own log index + # log_index = event.logIndex # Log index within the block + tx = event.transactionHash.hex() + block = event.blockNumber + name = event.event + args = event["args"] + return ProcessedEvent(tx=tx, block=block, name=name, args=args) + + def run( + self, + last_block: int, + end_block: int, + contract_creation_height=0, + ): + start = time.time() + # Scan from [last block scanned] - [latest ethereum block] + # Note that our chain reorg safety blocks cannot go negative + start_block = max(last_block - self.safety_blocks, 0, contract_creation_height) + blocks_to_scan = end_block - start_block + + print(f"Scanning events from blocks {start_block} - {end_block}") + + def _update_progress(start, end, current, chunk_size, events_count): + print( + f"Current block: {current}, blocks in a scan batch: {chunk_size}, events processed in a batch {events_count}" + ) + + # Run the scan + result, total_chunks_scanned = self.scan( + start_block, end_block, progress_callback=_update_progress + ) + + logger.debug( + f"Scanned total {len(result)} events in {time.time() - start}, total {total_chunks_scanned} chunk scans performed" + ) + + return result + + +def _retry_web3_call(func, start_block, end_block, retries, delay) -> Tuple[int, list]: + """A custom retry loop to throttle down block range. + + If our JSON-RPC server cannot serve all incoming `eth_getLogs` in a single request, + we retry and throttle down block range for every retry. + + For example, Go Ethereum does not indicate what is an acceptable response size. + It just fails on the server-side with a "context was cancelled" warning. + + :param func: A callable that triggers Ethereum JSON-RPC, as func(start_block, end_block) + :param start_block: The initial start block of the block range + :param end_block: The initial start block of the block range + :param retries: How many times we retry + :param delay: Time to sleep between retries + """ + for i in range(retries): + try: + return end_block, func(start_block, end_block) + except Exception as e: + # Assume this is HTTPConnectionPool(host='localhost', port=8545): Read timed out. (read timeout=10) + # from Go Ethereum. This translates to the error "context was cancelled" on the server side: + # https://github.com/ethereum/go-ethereum/issues/20426 + if i < retries - 1: + # Give some more verbose info than the default middleware + logger.warning( + f"Retrying events for block range {start_block} - {end_block} ({end_block-start_block}) failed with {e} , retrying in {delay} seconds" + ) + # Decrease the `eth_getBlocks` range + end_block = start_block + ((end_block - start_block) // 2) + # Let the JSON-RPC to recover e.g. from restart + time.sleep(delay) + continue + else: + logger.warning("Out of retries") + raise + + +def _fetch_events_for_all_contracts( + w3, event, argument_filters: Dict[str, Any], from_block: int, to_block: int +) -> Iterable: + """Get events using eth_getLogs API. + + This method is detached from any contract instance. + + This is a stateless method, as opposed to create_filter. + It can be safely called against nodes which do not provide `eth_newFilter` API, like Infura. + """ + + if from_block is None: + raise Web3TypeError("Missing mandatory keyword argument to get_logs: from_block") + + # Currently no way to poke this using a public web3.py API. + # This will return raw underlying ABI JSON object for the event + abi = event._get_event_abi() + + # Depending on the Solidity version used to compile + # the contract that uses the ABI, + # it might have Solidity ABI encoding v1 or v2. + # We just assume the default that you set on Web3 object here. + # More information here https://eth-abi.readthedocs.io/en/latest/index.html + codec: ABICodec = w3.codec + + # Here we need to poke a bit into Web3 internals, as this + # functionality is not exposed by default. + # Construct JSON-RPC raw filter presentation based on human readable Python descriptions + # Namely, convert event names to their keccak signatures + # More information here: + # https://github.com/ethereum/web3.py/blob/e176ce0793dafdd0573acc8d4b76425b6eb604ca/web3/_utils/filters.py#L71 + data_filter_set, event_filter_params = construct_event_filter_params( + abi, + codec, + address=argument_filters.get("address"), + argument_filters=argument_filters, + from_block=from_block, + to_block=to_block, + ) + + logger.debug(f"Querying eth_getLogs with the following parameters: {event_filter_params}") + + # Call JSON-RPC API on your Ethereum node. + # get_logs() returns raw AttributedDict entries + logs = w3.eth.get_logs(event_filter_params) + + # Convert raw binary data to Python proxy objects as described by ABI + all_events = [] + for log in logs: + # Convert raw JSON-RPC log result to human readable event by using ABI data + # More information how process_log works here + # https://github.com/ethereum/web3.py/blob/fbaf1ad11b0c7fac09ba34baff2c256cffe0a148/web3/_utils/events.py#L200 + evt = get_event_data(codec, abi, log) + # Note: This was originally yield, + # but deferring the timeout exception caused the throttle logic not to work + all_events.append(evt) + return all_events From 5563aea4677a9010cd64b87205fd162aad4bf87a Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Wed, 20 Nov 2024 17:22:11 +1100 Subject: [PATCH 004/138] feat: registration api --- registration/__init__.py | 0 schema.sqlite => registration/schema.sql | 22 +-- registration/validation.py | 32 +++++ registration/write.py | 49 +++++++ util/__init__.py | 23 ++++ util/data.py | 21 +++ util/parse.py | 166 +++++++++++++++++++++++ 7 files changed, 297 insertions(+), 16 deletions(-) create mode 100644 registration/__init__.py rename schema.sqlite => registration/schema.sql (53%) create mode 100644 registration/validation.py create mode 100644 registration/write.py create mode 100644 util/__init__.py create mode 100644 util/data.py create mode 100644 util/parse.py diff --git a/registration/__init__.py b/registration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/schema.sqlite b/registration/schema.sql similarity index 53% rename from schema.sqlite rename to registration/schema.sql index cd29d8c..c947796 100644 --- a/schema.sqlite +++ b/registration/schema.sql @@ -2,13 +2,12 @@ PRAGMA journal_mode=WAL; CREATE TABLE registrations ( - id INTEGER PRIMARY KEY NOT NULL, - pubkey_ed25519 BLOB NOT NULL, + contract BLOB, + operator BLOB NOT NULL, pubkey_bls BLOB NOT NULL, - sig_ed25519 BLOB NOT NULL, + pubkey_ed25519 BLOB NOT NULL, sig_bls BLOB NOT NULL, - operator BLOB NOT NULL, - contract BLOB, + sig_ed25519 BLOB NOT NULL, timestamp FLOAT NOT NULL DEFAULT ((julianday('now') - 2440587.5)*86400.0), /* unix epoch */ CHECK(length(pubkey_ed25519) == 32), @@ -18,15 +17,6 @@ CREATE TABLE registrations ( CHECK(length(operator) == 20), CHECK(contract IS NULL OR length(contract) == 20) ); -CREATE INDEX registrations_operator_idx ON registrations(operator); -CREATE UNIQUE INDEX registration_pk_multi_idx ON registrations(pubkey_ed25519, contract IS NULL); -CREATE TABLE contribution_contracts ( - id INTEGER PRIMARY KEY NOT NULL, - contract_address TEXT NOT NULL, - status INTEGER DEFAULT 1, - timestamp FLOAT NOT NULL DEFAULT ((julianday('now') - 2440587.5)*86400.0), /* unix epoch */ - - CHECK(length(contract_address) == 42) -- Assuming Ethereum addresses -); -CREATE UNIQUE INDEX contribution_contract_address_idx ON contribution_contracts(contract_address); +CREATE INDEX registrations_timestamp_idx ON registrations(timestamp DESC); +CREATE INDEX registrations_operator_idx ON registrations(operator, timestamp DESC); diff --git a/registration/validation.py b/registration/validation.py new file mode 100644 index 0000000..e57a592 --- /dev/null +++ b/registration/validation.py @@ -0,0 +1,32 @@ +import nacl.hash +import nacl.bindings as sodium +from nacl.signing import VerifyKey + + +class SNSignatureValidationError(ValueError): + pass + + +def check_reg_keys_sigs(params): + if len(params["pubkey_ed25519"]) != 32 or not sodium.crypto_core_ed25519_is_valid_point( + params["pubkey_ed25519"] + ): + raise SNSignatureValidationError("Ed25519 pubkey is invalid") + if len(params["pubkey_bls"]) != 64: # FIXME: bls pubkey validation? + raise SNSignatureValidationError("BLS pubkey is invalid") + if len(params["operator"]) != 20: + raise SNSignatureValidationError("operator address is invalid") + contract = params.get("contract") + if contract is not None and len(contract) != 20: + raise SNSignatureValidationError("contract address is invalid") + + signed = params["pubkey_ed25519"] + params["pubkey_bls"] + + try: + VerifyKey(params["pubkey_ed25519"]).verify(signed, params["sig_ed25519"]) + except nacl.exceptions.BadSignatureError: + raise SNSignatureValidationError("Ed25519 signature is invalid") + + # FIXME: BLS verification of pubkey_bls on signed + if False: + raise SNSignatureValidationError("BLS signature is invalid") diff --git a/registration/write.py b/registration/write.py new file mode 100644 index 0000000..4eb68ca --- /dev/null +++ b/registration/write.py @@ -0,0 +1,49 @@ +import sqlite3 +from contextlib import closing + +from log import Log + + +class DBWriterRegistrations: + def __init__(self, db_path: str, log_level: int, perf: bool = False): + self.db_path = db_path + self.log = Log("db_writer", log_level, enable_perf=perf).logger + + def write_registration_to_db(self, registration): + self.log.perf.start("write_registration_to_db") + with closing(sqlite3.connect(self.db_path)) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + + self.log.debug("Inserting {} registration".format(len(registration))) + self.log.perf.start("write_registration_to_db -> insert registration") + + cursor.execute( + """ + INSERT OR REPLACE INTO registrations ( + contract + operator, + pubkey_bls, + pubkey_ed25519, + sig_bls, + sig_ed25519, + ) + VALUES (?, ?, ?, ?, ?, ?) + """, + ( + registration.get("contract"), + registration.get("operator"), + registration.get("pubkey_bls"), + registration.get("pubkey_ed25519"), + registration.get("sig_bls"), + registration.get("sig_ed25519"), + ), + ) + + inserted_rows = cursor.rowcount + + self.log.perf.end("write_registration_to_db -> insert registration") + self.log.debug("Inserted {} rows into registration".format(inserted_rows)) + + connection.commit() + self.log.perf.end("write_registration_to_db") diff --git a/util/__init__.py b/util/__init__.py new file mode 100644 index 0000000..5889db6 --- /dev/null +++ b/util/__init__.py @@ -0,0 +1,23 @@ +from eth_utils import is_address + + +def is_not_empty_string(value) -> bool: + return value is not None and len(value) > 0 + + +def valid_address_assertion(address: str, name: str | None = None): + assert is_address(address), "{} in config.py is not a valid address: {}".format(name, address) + + +def format_seconds(seconds: int | float, precision: int = 3) -> str: + assert precision >= 0 + if precision == 0: + return seconds.__round__().__str__() + return seconds.__round__(precision).__str__() + + +def format_ms(ms: int | float, precision: int = 3) -> str: + assert precision >= 0 + if precision == 0: + return ms.__round__().__str__() + return ms.__round__(precision).__str__() diff --git a/util/data.py b/util/data.py new file mode 100644 index 0000000..42b4761 --- /dev/null +++ b/util/data.py @@ -0,0 +1,21 @@ +import time +from typing import Optional, Callable + + +class DataManager: + def __init__(self, stale_time_seconds: int = 0): + self.cache = {} + self.cache_expiry = {} + self.default_stale_time_seconds = stale_time_seconds + + def get(self, key, getter=Optional[Callable], getter_args=None, ttl=0): + if ttl == 0: + ttl = self.default_stale_time_seconds + now = time.time() + if key in self.cache and self.cache_expiry[key] > now: + return self.cache[key] + + data = getter(getter_args) if getter_args is not None else getter() + self.cache[key] = data + self.cache_expiry[key] = now + ttl + return data diff --git a/util/parse.py b/util/parse.py new file mode 100644 index 0000000..7437f07 --- /dev/null +++ b/util/parse.py @@ -0,0 +1,166 @@ +import re +from typing import Callable, Any +from functools import partial + +import string + +import eth_utils +import flask +import oxenc +from werkzeug.routing import BaseConverter + +eth_regex = "0x[0-9a-fA-F]{40}" + + +def raw_eth_addr(k, v): + if re.fullmatch(eth_regex, v): + if not eth_utils.is_address(v): + raise ParseError(k, "ETH address checksum failed") + return bytes.fromhex(v[2:]) + raise ParseError(k, "not an ETH address") + + +def hexify(container): + """ + Takes a dict or list and mutates it to change any `bytes` values in it to str hex representation + of the bytes, recursively. + """ + if isinstance(container, dict): + it = container.items() + elif isinstance(container, list): + it = enumerate(container) + else: + return + + for i, v in it: + if isinstance(v, bytes): + container[i] = v.hex() + else: + hexify(v) + + +class EthConverter(BaseConverter): + def __init__(self, url_map): + super().__init__(url_map) + self.regex = eth_regex + + +# Validates that input is 64 hex bytes and converts it to 32 bytes. +class Hex64Converter(BaseConverter): + def __init__(self, url_map): + super().__init__(url_map) + self.regex = "[0-9a-fA-F]{64}" + + def to_python(self, value): + return bytes.fromhex(value) + + def to_url(self, value): + return value.hex() + + +class ParseError(ValueError): + def __init__(self, field, reason): + self.field = field + super().__init__(f"{field}: {reason}") + + +class ParseMissingError(ParseError): + def __init__(self, field): + super().__init__(field, f"required parameter is missing") + + +class ParseUnknownError(ParseError): + def __init__(self, field): + super().__init__(field, f"unknown parameter") + + +class ParseMultipleError(ParseError): + def __init__(self, field): + super().__init__(field, f"cannot be specified multiple times") + + +def parse_query_params(params: dict[str, Callable[[str, str], Any]]): + """ + Takes a dict of fields and callables such as: + + { + "field": ("out", callable), + ... + } + + where: + - `"field"` is the expected query string name + - `callable` will be invoked as `callable("field", value)` to determined the returned value. + + On error, throws a ParseError with `.field` set to the "field" name that triggered the error. + + Notes: + - callable should throw a ParseError for an unaccept input value. + - if "-field" starts with "-" then the field is optional; otherwise it is an error if not + provided. The "-" is not included in the returned key. + - if "field" ends with "[]" then the value will be an array of values returned by the callable, + and the parameter can be specified multiple times. Otherwise a value can be specified only + once. The "[]" is not included in the returned key. + - you can do both of the above: "-field[]" will allow the value to be provided zero or more + times; the value will be omitted if not present in the input, and an array (under the "field") + key if provided at least once. + """ + + parsed = {} + + param_map = { + k.removeprefix("-").removesuffix("[]"): ( + k.startswith("-"), + k.endswith("[]"), + cb, + ) + for k, cb in params.items() + } + + for k, v in flask.request.values.items(multi=True): + found = param_map.get(k) + if found is None: + raise ParseUnknownError(k) + + _, multi, callback = found + + if multi: + parsed.setdefault(k, []).append(callback(k, v) if callback else v) + elif k not in parsed: + parsed[k] = callback(k, v) if callback else v + else: + raise ParseMultipleError(k) + + for k, p in param_map.items(): + optional = p[0] + if not optional and k not in flask.request.values: + raise ParseMissingError(k) + + return parsed + + +# Decodes `x` into a bytes of length `length`. `x` should be hex or base64 encoded, without +# whitespace. Both regular and "URL-safe" base64 are accepted. Padding is optional for base64 +# values. Throws ParseError if the input is invalid or of the wrong size. `length` must be at +# least 5 (smaller byte values are harder or even ambiguous to distinguish between hex and base64). +def decode_bytes(k, x, length): + assert length >= 5 + + hex_len = length * 2 + b64_unpadded = (length * 4 + 2) // 3 + b64_padded = (length + 2) // 3 * 4 + + if len(x) == hex_len and all(c in string.hexdigits for c in x): + return bytes.fromhex(x) + if len(x) in (b64_unpadded, b64_padded): + if oxenc.is_base64(x): + return oxenc.from_base64(x) + if "-" in x or "_" in x: # Looks like (maybe) url-safe b64 + x = x.replace("/", "_").replace("+", "-") + if oxenc.is_base64(x): + return oxenc.from_base64(x) + raise ParseError(k, f"expected {hex_len} hex or {b64_unpadded} base64 characters") + + +def byte_decoder(length: int): + return partial(decode_bytes, length=length) From 00f8b5191dd773138eff205c622bc05aeee137d0 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Wed, 20 Nov 2024 17:22:40 +1100 Subject: [PATCH 005/138] chore: move oxen and omq logic to package --- oxen/__init__.py | 0 omq.py => oxen/omq.py | 54 ++++++++---- oxen/rpc.py | 192 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 230 insertions(+), 16 deletions(-) create mode 100644 oxen/__init__.py rename omq.py => oxen/omq.py (72%) create mode 100644 oxen/rpc.py diff --git a/oxen/__init__.py b/oxen/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/omq.py b/oxen/omq.py similarity index 72% rename from omq.py rename to oxen/omq.py index 2d95d48..60dfda8 100644 --- a/omq.py +++ b/oxen/omq.py @@ -1,26 +1,28 @@ import oxenmq -import config import json import sys from datetime import datetime, timedelta omq, oxend = None, None -def omq_connection(): + + +def omq_connection(oxend_rpc): global omq, oxend if omq is None: - omq = oxenmq.OxenMQ(log_level=oxenmq.LogLevel.warn) - omq.max_message_size = 200*1024*1024 + omq = oxenmq.OxenMQ(log_level=oxenmq.LogLevel.warn) + omq.max_message_size = 200 * 1024 * 1024 omq.start() if oxend is None: - oxend_rpc = config.backend.rpc - oxend = omq.connect_remote(oxenmq.Address(oxend_rpc)) + oxend = omq.connect_remote(oxenmq.Address(oxend_rpc)) return (omq, oxend) + cached = {} cached_args = {} cache_expiry = {} -class FutureJSON(): + +class FutureJSON: """Class for making a OMQ JSON RPC request that uses a future to wait on the result, and caches the results for a set amount of time so that if the same endpoint with the same arguments is requested again the cache will be used instead of repeating the request. @@ -41,41 +43,61 @@ class FutureJSON(): timeout - maximum time to spend waiting for a reply """ - def __init__(self, omq, oxend, endpoint, cache_seconds=5, *, cache_key='', args=None, fail_okay=False, timeout=10): + def __init__( + self, + omq, + oxend, + endpoint, + cache_seconds=5, + *, + cache_key="", + args=None, + fail_okay=False, + timeout=10 + ): self.endpoint = endpoint self.cache_key = self.endpoint + cache_key self.fail_okay = fail_okay if args is not None: args = json.dumps(args).encode() - if self.cache_key in cached and cached_args[self.cache_key] == args and cache_expiry[self.cache_key] >= datetime.now(): + if ( + self.cache_key in cached + and cached_args[self.cache_key] == args + and cache_expiry[self.cache_key] >= datetime.now() + ): self.json = cached[self.cache_key] self.args = None self.future = None else: self.json = None self.args = args - self.future = omq.request_future(oxend, self.endpoint, [] if self.args is None else [self.args], timeout=timeout) + self.future = omq.request_future( + oxend, self.endpoint, [] if self.args is None else [self.args], timeout=timeout + ) self.cache_seconds = cache_seconds def get(self): """If the result is already available, returns it immediately (and can safely be called multiple times. - Otherwise waits for the result, parses as json, and caches it. Returns None if the request fails""" + Otherwise waits for the result, parses as json, and caches it. Returns None if the request fails + """ if self.json is None and self.future is not None: try: result = self.future.get() self.future = None - if result[0] != b'200': - raise RuntimeError("Request for {} failed: got {}".format(self.endpoint, result)) + if result[0] != b"200": + raise RuntimeError( + "Request for {} failed: got {}".format(self.endpoint, result) + ) self.json = json.loads(result[1]) if self.cache_seconds is not None: cached[self.cache_key] = self.json cached_args[self.cache_key] = self.args - cache_expiry[self.cache_key] = datetime.now() + timedelta(seconds=self.cache_seconds) + cache_expiry[self.cache_key] = datetime.now() + timedelta( + seconds=self.cache_seconds + ) except RuntimeError as e: if not self.fail_okay: print("Something getting wrong: {}".format(e), file=sys.stderr) self.future = None return self.json - - diff --git a/oxen/rpc.py b/oxen/rpc.py new file mode 100644 index 0000000..4a5669a --- /dev/null +++ b/oxen/rpc.py @@ -0,0 +1,192 @@ +import logging +from typing import TypedDict +from oxen.omq import FutureJSON, omq_connection +from dataclasses import dataclass + + +class ServiceNodeContributor(TypedDict): + address: str + amount: int + beneficiary: str + locked_contributions: list[int] + + +class ServiceNode(TypedDict): + active: bool + contract_id: int + # In separate table + contributors: list[ServiceNodeContributor] + decommission_count: int + earned_downtime_blocks: int + funded: bool + is_liquidatable: bool + is_removable: bool + last_reward_block_height: int + last_reward_transaction_index: int + last_uptime_proof: int + lokinet_version: list[int] + operator_address: str + operator_fee: int + payable: bool + portions_for_operator: int + pubkey_bls: str + pubkey_ed25519: str + pubkey_x25519: str + public_ip: str + pulse_votes: dict[str, list[int]] | None + quorumnet_port: int + registration_height: int + registration_hf_version: int + requested_unlock_height: int + service_node_pubkey: str + service_node_version: list[int] + staking_requirement: int + state_height: int + storage_lmq_port: int + storage_port: int + storage_server_version: list[int] + swarm: str + swarm_id: int + total_contributed: int + + +@dataclass +class NetworkInfo: + block_hash: str + block_height: int + hard_fork: int + immutable_block_hash: str + immutable_block_height: int + max_stakers: int + min_operator_contribution: int + nettype: str + pulse_target_timestamp: int + staking_requirement: int + version: str + + +class OxenRPC: + def __init__(self, logger: logging, rpc_url: str, cache_seconds: float | None = None): + self.log = logger + self.rpc_url = rpc_url + self.cache_seconds = cache_seconds + + def get_accrued_rewards(self) -> FutureJSON: + omq, oxend = omq_connection(self.rpc_url) + return FutureJSON( + omq, + oxend, + "rpc.get_accrued_rewards", + args={"addresses": []}, + cache_seconds=self.cache_seconds, + ) + + def bls_rewards_request(self, eth_address: str) -> FutureJSON: + omq, oxend = omq_connection(self.rpc_url) + eth_address_for_rpc = eth_address.lower() + if eth_address_for_rpc.startswith("0x"): + eth_address_for_rpc = eth_address_for_rpc[2:] + result = FutureJSON( + omq, + oxend, + "rpc.bls_rewards_request", + args={"address": eth_address_for_rpc}, + cache_seconds=self.cache_seconds, + ) + return result + + def bls_exit_liquidation_request(self, ed25519_pubkey: bytes, liquidate: bool) -> FutureJSON: + omq, oxend = omq_connection(self.rpc_url) + return FutureJSON( + omq, + oxend, + "rpc.bls_exit_liquidation_request", + args={"pubkey": ed25519_pubkey.hex(), "liquidate": liquidate}, + cache_seconds=self.cache_seconds, + ) + + def bls_exit_liquidation_list(self) -> FutureJSON: + omq, oxend = omq_connection(self.rpc_url) + return FutureJSON( + omq, + oxend, + "rpc.bls_exit_liquidation_list", + cache_seconds=self.cache_seconds, + ) + + def get_info(self) -> FutureJSON: + omq, oxend = omq_connection(self.rpc_url) + return FutureJSON( + omq, + oxend, + "rpc.get_info", + cache_seconds=self.cache_seconds, + ) + + def get_last_block_header(self) -> FutureJSON: + omq, oxend = omq_connection(self.rpc_url) + return FutureJSON( + omq, + oxend, + "rpc.get_last_block_header", + args={"fill_pow_hash": False, "get_tx_hashes": False}, + cache_seconds=self.cache_seconds, + ) + + def get_service_nodes(self) -> FutureJSON: + omq, oxend = omq_connection(self.rpc_url) + return FutureJSON( + omq, + oxend, + "rpc.get_service_nodes", + args={ + "all": True, + # TODO: decide if we want to reduce the number of fields returned, this needs to match the database too + # "fields": { + # x: True + # for x in ( + # "service_node_pubkey", + # "requested_unlock_height", + # "last_reward_block_height", + # "active", + # "pubkey_bls", + # "funded", + # "earned_downtime_blocks", + # "service_node_version", + # "contributors", + # "total_contributed", + # "total_reserved", + # "staking_requirement", + # "portions_for_operator", + # "operator_address", + # "pubkey_ed25519", + # "last_uptime_proof", + # "state_height", + # "swarm_id", + # "is_removable", + # "is_liquidatable", + # "operator_fee" + # ) + # }, + }, + cache_seconds=self.cache_seconds, + ) + + def get_network_info_from_network(self): + omq, oxend = omq_connection(self.rpc_url) + info = self.get_info(omq, oxend).get() + self.log.silly("get_network_info_from_network info: {}".format(info)) + + return NetworkInfo( + block_hash=info.get("top_block_hash"), + block_height=info.get("height"), + hard_fork=info.get("hard_fork"), + immutable_block_hash=info.get("immutable_block_hash"), + immutable_block_height=info.get("immutable_height"), + max_stakers=info.get("max_contributors"), + min_operator_contribution=info.get("min_operator_contribution"), + nettype=info.get("nettype"), + pulse_target_timestamp=info.get("pulse_target_timestamp"), + staking_requirement=info.get("staking_requirement"), + version=info.get("version"), + ) From 1f4fec3b5aeccb3e847d473fe4c73cd65f116b39 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Wed, 20 Nov 2024 17:22:54 +1100 Subject: [PATCH 006/138] feat: db manager --- db/__init__.py | 0 db/dataclasses.py | 130 +++++++++++ db/read.py | 216 +++++++++++++++++ db/schema.sql | 213 +++++++++++++++++ db/util.py | 32 +++ db/write.py | 574 ++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 1165 insertions(+) create mode 100644 db/__init__.py create mode 100644 db/dataclasses.py create mode 100644 db/read.py create mode 100644 db/schema.sql create mode 100644 db/util.py create mode 100644 db/write.py diff --git a/db/__init__.py b/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/db/dataclasses.py b/db/dataclasses.py new file mode 100644 index 0000000..48421f2 --- /dev/null +++ b/db/dataclasses.py @@ -0,0 +1,130 @@ +import json +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class DBNode: + active: bool + contract_id: str + decommission_count: int + earned_downtime_blocks: int + fetched_block_height: int + funded: bool + is_liquidatable: bool + is_removable: bool + last_reward_block_height: int + last_uptime_proof: int + lokinet_version: dict | None + operator_address: str + operator_fee: int + payable: bool + pubkey_bls: str + pubkey_ed25519: str + public_ip: str | None + pulse_votes: dict | None + quorumnet_port: int | None + registration_height: int + registration_hf_version: str + requested_unlock_height: int + service_node_pubkey: str + service_node_version: dict | None + staking_requirement: int + state_height: int + storage_lmq_port: int | None + storage_port: int | None + storage_server_version: dict | None + swarm: str + swarm_id: str + total_contributed: int + # Not in db but added after select + contributors: list | None + + def __post_init__(self): + self.lokinet_version = json.loads(self.lokinet_version) if self.lokinet_version else None + self.service_node_version = ( + json.loads(self.service_node_version) if self.service_node_version else None + ) + self.storage_server_version = ( + json.loads(self.storage_server_version) if self.storage_server_version else None + ) + self.pulse_votes = json.loads(self.pulse_votes) if self.pulse_votes else None + + +@dataclass +class DBContributionMain: + address: str + amount: int + beneficiary: str | None + contract_id: str + fetched_block_height: int + + def __post_init__(self): + # We don't need the contract_id or fetched_block_height fields when its a dict + self.__dataclass_fields__ = { + k: v + for k, v in self.__dataclass_fields__.items() + if k != "contract_id" and k != "fetched_block_height" + } + + +@dataclass +class DBNetworkInfo: + id: Optional[int] + block_hash: str + block_height: int + block_timestamp: float + hard_fork: int + immutable_block_hash: str + immutable_block_height: int + max_stakers: int + min_operator_contribution: int + nettype: str + pulse_target_timestamp: int + staking_requirement: int + version: str + + def __post_init__(self): + # We don't need the id field when its a dict + self.__dataclass_fields__ = { + k: v for k, v in self.__dataclass_fields__.items() if k != "id" + } + + +@dataclass +class DBContributionContract: + address: str + fee: int + operator_address: str + pubkey_bls: str + service_node_pubkey: str + service_node_signature: str + status: int + # Not in db but added after select + contributors: list | None + + +@dataclass +class DBContributionContractContribution: + address: str + amount: int + beneficiary_address: str + contract_address: str + reserved: int + + def __post_init__(self): + # We don't need the contract_address field when its a dict + self.__dataclass_fields__ = { + k: v for k, v in self.__dataclass_fields__.items() if k != "contract_address" + } + + +@dataclass +class SmartContractABI: + name: str + abi: dict + bytecode: bytes + deployed_bytecode: bytes + + def __post_init__(self): + self.abi = json.loads(self.abi) diff --git a/db/read.py b/db/read.py new file mode 100644 index 0000000..179cfa1 --- /dev/null +++ b/db/read.py @@ -0,0 +1,216 @@ +import sqlite3 +from contextlib import closing + +from db.dataclasses import DBNode, DBContributionMain, DBNetworkInfo, DBContributionContract, DBContributionContractContribution, SmartContractABI +from log import Log + + +class DBReader: + def __init__(self, db_path: str, log_level: int, perf: bool = False): + self.db_path = db_path + self.log = Log("db_reader", log_level, enable_perf=perf).logger + + def get_last_fetched_network_block_height(self) -> int: + self.log.perf.start("get_last_fetched_network_block_height") + with closing(sqlite3.connect(self.db_path)) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute("SELECT MAX(fetched_block_height) FROM service_nodes_staging") + (fetched_block_height,) = cursor.fetchone() + self.log.debug( + "get_last_fetched_network_block_height result: {}".format(fetched_block_height) + ) + self.log.perf.end("get_last_fetched_network_block_height") + return fetched_block_height if fetched_block_height is not None else 0 + + def get_last_commited_network_block_height(self) -> int: + self.log.perf.start("get_last_commited_network_block_height") + with closing(sqlite3.connect(self.db_path)) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute("SELECT MAX(fetched_block_height) FROM service_nodes_main") + (commited_block_height,) = cursor.fetchone() + self.log.debug( + "get_last_commited_network_block_height result: {}".format( + commited_block_height + ) + ) + self.log.perf.end("get_last_commited_network_block_height") + return commited_block_height if commited_block_height is not None else 0 + + def get_network_info(self): + self.log.perf.start("get_network_info") + with closing(sqlite3.connect(self.db_path)) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute("SELECT * FROM network_info LIMIT 1") + network_info = DBNetworkInfo(*cursor.fetchone()) + + self.log.debug("Network Info: {}".format(network_info)) + self.log.perf.end("get_network_info") + return network_info + + def get_last_fetched_arbitrum_event_block_height(self) -> int: + self.log.perf.start("get_last_fetched_arbitrum_event_block_height") + with closing(sqlite3.connect(self.db_path)) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute("SELECT MAX(block) FROM arbitrum_events") + (fetched_block_height,) = cursor.fetchone() + self.log.debug( + "get_last_fetched_arbitrum_event_block_height result: {}".format( + fetched_block_height + ) + ) + self.log.perf.end("get_last_fetched_arbitrum_event_block_height") + return fetched_block_height if fetched_block_height is not None else 0 + + def get_contribution_contracts(self): + self.log.perf.start("get_contribution_contracts") + with closing(sqlite3.connect(self.db_path)) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute("""SELECT * FROM contribution_contracts""") + contracts = cursor.fetchall() + + parsed_contracts = {} + for contract in contracts: + contract_dict = DBContributionContract(*contract, contributors=[]) + parsed_contracts[contract_dict.address] = contract_dict + + cursor.execute( + """ + SELECT * FROM contribution_contracts_contributions + """ + ) + contributions = cursor.fetchall() + for contribution in contributions: + contribution_dict = DBContributionContractContribution(*contribution) + parsed_contracts[contribution_dict.contract_address].contributors.append( + contribution_dict + ) + + self.log.debug("Parsed contribution contracts: {}".format(len(parsed_contracts))) + self.log.perf.end("get_contribution_contracts") + return list(parsed_contracts.values()) + + def get_contribution_contract_addresses(self): + self.log.perf.start("get_contribution_contracts") + with closing(sqlite3.connect(self.db_path)) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute( + """ + SELECT address FROM contribution_contracts + """ + ) + addresses = cursor.fetchall() + self.log.debug("Contract addresses: {}".format(len(addresses))) + self.log.perf.end("get_contribution_contracts") + return [address[0] for address in addresses] + + def get_nodes(self): + self.log.perf.start("get_nodes") + with closing(sqlite3.connect(self.db_path)) as connection: + with closing(connection.cursor()) as cursor: + # TODO: investigate using a join or something less messy than two select * queries + cursor.execute("""SELECT * FROM service_nodes_main""") + # We want to sort by fetched_block_height in ascending order so later updates overwrite earlier ones + cursor.execute( + """SELECT * FROM service_nodes_staging ORDER BY fetched_block_height ASC""" + ) + + parsed_nodes = {} + for node in cursor.fetchall(): + node_dict = DBNode(*node, contributors=[]) + parsed_nodes[node_dict.contract_id] = node_dict + + cursor.execute("""SELECT * from service_nodes_contributions_main""") + # We want to sort by fetched_block_height in ascending order so later updates overwrite earlier ones + cursor.execute( + """SELECT * from service_nodes_contributions_staging ORDER BY fetched_block_height ASC""" + ) + + parsed_contributions = {} + for contribution in cursor.fetchall(): + contribution_dict = DBContributionMain(*contribution) + # TODO: there has to be a better way to override the old data with new data + key = f"{contribution_dict.contract_id}{contribution_dict.address}" + parsed_contributions[key] = contribution_dict + + for contribution_dict in parsed_contributions.values(): + parsed_nodes[contribution_dict.contract_id].contributors.append( + contribution_dict + ) + + self.log.debug("Parsed nodes: {}".format(len(parsed_nodes))) + self.log.perf.end("get_nodes") + return list(parsed_nodes.values()) + + def get_smart_contract_abis(self): + self.log.perf.start("get_smart_contract_abis") + with closing(sqlite3.connect(self.db_path)) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute( + """ + SELECT * FROM smart_contract_abis + """ + ) + abis = [SmartContractABI(*abi) for abi in cursor.fetchall()] + self.log.debug("Smart contract abis: {}".format(len(abis))) + self.log.perf.end("get_smart_contract_abis") + return abis + + def get_smart_contract_abi(self, name: str): + self.log.perf.start("get_smart_contract_abi") + with closing(sqlite3.connect(self.db_path)) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute( + """ + SELECT * FROM smart_contract_abis WHERE name = ? + """, + (name,), + ) + abi = SmartContractABI(*cursor.fetchone()) + self.log.debug("Smart contract abi: {}".format(abi)) + self.log.perf.end("get_smart_contract_abi") + return abi + + def get_smart_contract_names(self) -> list[str]: + self.log.perf.start("get_smart_contract_names") + with closing(sqlite3.connect(self.db_path)) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute( + """ + SELECT name FROM smart_contract_abis + """ + ) + names = [name[0] for name in cursor.fetchall()] + self.log.debug("Smart contract names: {}".format(len(names))) + self.log.perf.end("get_smart_contract_names") + return names + + def get_smart_contract_addresses(self): + self.log.perf.start("get_smart_contract_addresses") + with closing(sqlite3.connect(self.db_path)) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute( + """ + SELECT address, name FROM smart_contracts + """ + ) + addresses = [ + {"address": address, "name": name} for address, name in cursor.fetchall() + ] + self.log.debug("Smart contract addresses: {}".format(len(addresses))) + self.log.perf.end("get_smart_contract_addresses") + return addresses + + def get_smart_contract_address(self, name: str): + self.log.perf.start("get_smart_contract_address") + with closing(sqlite3.connect(self.db_path)) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute( + """ + SELECT address FROM smart_contracts WHERE name = ? + """, + (name,), + ) + address = cursor.fetchone() + self.log.debug("Smart contract address: {}".format(address)) + self.log.perf.end("get_smart_contract_address") + return address[0] diff --git a/db/schema.sql b/db/schema.sql new file mode 100644 index 0000000..4f7018c --- /dev/null +++ b/db/schema.sql @@ -0,0 +1,213 @@ + +PRAGMA journal_mode=WAL; + +CREATE TABLE service_nodes_staging ( + active BOOLEAN NOT NULL, + contract_id INTEGER NOT NULL, + decommission_count INTEGER NOT NULL, + earned_downtime_blocks INTEGER NOT NULL, + fetched_block_height INTEGER NOT NULL, + funded BOOLEAN NOT NULL, + is_liquidatable BOOLEAN NOT NULL, + is_removable BOOLEAN NOT NULL, + last_reward_block_height INTEGER NOT NULL, +-- last_reward_transaction_index INTEGER NOT NULL, + last_uptime_proof INTEGER NOT NULL, + lokinet_version TEXT, + operator_address BLOB NOT NULL, + operator_fee INTEGER NOT NULL, + payable BOOLEAN NOT NULL, +-- portions_for_operator TEXT NOT NULL, -- too large to be an int + pubkey_bls BLOB NOT NULL, + pubkey_ed25519 BLOB NOT NULL, +-- pubkey_x25519 BLOB NOT NULL, + public_ip TEXT, + pulse_votes TEXT, -- JSON encoded dict + quorumnet_port INTEGER, + registration_height INTEGER NOT NULL, + registration_hf_version INTEGER NOT NULL, + requested_unlock_height INTEGER NOT NULL, + service_node_pubkey BLOB NOT NULL, + service_node_version TEXT, -- JSON encoded list of integers + staking_requirement INTEGER NOT NULL, + state_height INTEGER NOT NULL, + storage_lmq_port INTEGER, + storage_port INTEGER, + storage_server_version TEXT, -- JSON encoded list of integers + swarm TEXT NOT NULL, + swarm_id TEXT NOT NULL, -- too large to be an int + total_contributed INTEGER NOT NULL, + + PRIMARY KEY(contract_id, fetched_block_height) +); + + +CREATE INDEX service_nodes_staging_contract_id_idx ON service_nodes_staging(contract_id); +CREATE INDEX service_nodes_staging_fetched_block_height_desc_idx + ON service_nodes_staging(fetched_block_height DESC); + +CREATE TABLE service_nodes_contributions_staging ( + address BLOB NOT NULL, + amount INTEGER NOT NULL, + beneficiary BLOB, + contract_id INTEGER NOT NULL, + fetched_block_height INTEGER NOT NULL, + + FOREIGN KEY (contract_id, fetched_block_height) REFERENCES service_nodes_staging(contract_id, fetched_block_height), + PRIMARY KEY (contract_id, address, fetched_block_height) +); + +CREATE INDEX service_nodes_contributions_staging_contract_id_idx ON service_nodes_contributions_staging(contract_id); +CREATE INDEX service_nodes_contributions_staging_fetched_block_height_desc_idx ON service_nodes_contributions_staging(fetched_block_height DESC); +CREATE INDEX service_nodes_contributions_staging_fetched_block_height_asc_idx ON service_nodes_contributions_staging(fetched_block_height ASC); + + +CREATE TABLE service_nodes_main ( + active BOOLEAN NOT NULL, + contract_id INTEGER NOT NULL, + decommission_count INTEGER NOT NULL, + earned_downtime_blocks INTEGER NOT NULL, + fetched_block_height INTEGER NOT NULL, + funded BOOLEAN NOT NULL, + is_liquidatable BOOLEAN NOT NULL, + is_removable BOOLEAN NOT NULL, + last_reward_block_height INTEGER NOT NULL, +-- last_reward_transaction_index INTEGER NOT NULL, + last_uptime_proof INTEGER NOT NULL, + lokinet_version TEXT, + operator_address BLOB NOT NULL, + operator_fee INTEGER NOT NULL, + payable BOOLEAN NOT NULL, +-- portions_for_operator TEXT NOT NULL, -- too large to be an int + pubkey_bls BLOB NOT NULL, + pubkey_ed25519 BLOB NOT NULL, +-- pubkey_x25519 BLOB NOT NULL, + public_ip TEXT, + pulse_votes TEXT, -- JSON encoded dict + quorumnet_port INTEGER, + registration_height INTEGER NOT NULL, + registration_hf_version INTEGER NOT NULL, + requested_unlock_height INTEGER NOT NULL, + service_node_pubkey BLOB NOT NULL, + service_node_version TEXT, -- JSON encoded list of integers + staking_requirement INTEGER NOT NULL, + state_height INTEGER NOT NULL, + storage_lmq_port INTEGER, + storage_port INTEGER, + storage_server_version TEXT,-- JSON encoded list of integers + swarm TEXT NOT NULL, + swarm_id TEXT NOT NULL, -- too large to be an int + total_contributed INTEGER NOT NULL, + + PRIMARY KEY(contract_id) +); + + +CREATE INDEX service_nodes_main_contract_id_idx ON service_nodes_main(contract_id); +CREATE INDEX service_nodes_main_fetched_block_height_desc_idx + ON service_nodes_main(fetched_block_height DESC); + +CREATE TABLE service_nodes_contributions_main ( + address BLOB NOT NULL, + amount INTEGER NOT NULL, + beneficiary BLOB, + contract_id INTEGER NOT NULL, + fetched_block_height INTEGER NOT NULL, + + FOREIGN KEY (contract_id) REFERENCES service_nodes_main(contract_id), + PRIMARY KEY (contract_id, address) +); + +CREATE INDEX service_nodes_contributions_main_contract_id_idx ON service_nodes_contributions_main(contract_id); +CREATE INDEX service_nodes_contributions_main_fetched_block_height_desc_idx ON service_nodes_contributions_main(fetched_block_height DESC); + +CREATE TABLE network_info ( + id INTEGER PRIMARY KEY NOT NULL, + block_hash TEXT NOT NULL, + block_height INTEGER NOT NULL, + block_timestamp FLOAT NOT NULL, + hard_fork INTEGER NOT NULL, + immutable_block_hash TEXT NOT NULL, + immutable_block_height INTEGER NOT NULL, + max_stakers INTEGER NOT NULL, + min_operator_contribution INTEGER NOT NULL, + nettype TEXT NOT NULL, + pulse_target_timestamp INTEGER NOT NULL, + staking_requirement INTEGER NOT NULL, + version TEXT NOT NULL +); + +CREATE INDEX network_info_block_height_idx ON network_info(block_height DESC); + +CREATE TABLE arbitrum_events ( + block INTEGER NOT NULL, + tx TEXT NOT NULL, + name TEXT NOT NULL, + args TEXT NOT NULL, + PRIMARY KEY (block, tx, name) +); + +CREATE INDEX arbitrum_events_block_idx ON arbitrum_events(block DESC); + +CREATE TABLE contribution_contracts ( + address TEXT NOT NULL, + fee INTEGER NOT NULL, + operator_address TEXT NOT NULL, + pubkey_bls BLOB NOT NULL, + service_node_pubkey BLOB NOT NULL, + service_node_signature BLOB NOT NULL, + status INTEGER NOT NULL, + + PRIMARY KEY (address) +); + +CREATE INDEX contribution_contracts_address_idx ON contribution_contracts(address); + +CREATE TABLE contribution_contracts_contributions +( + address BLOB NOT NULL, + amount INTEGER NOT NULL, + beneficiary_address BLOB NOT NULL, + contract_address BLOB NOT NULL, + reserved INTEGER, + + FOREIGN KEY (contract_address) REFERENCES contribution_contracts (address), + PRIMARY KEY (contract_address, address) +); + +CREATE INDEX contribution_contracts_contributions_contract_address_address ON contribution_contracts_contributions(contract_address, address); +CREATE INDEX contribution_contracts_contributions_contract_address_address_amount ON contribution_contracts_contributions(contract_address, address, amount); + +CREATE TABLE smart_contract_abis ( + name TEXT NOT NULL, + abi TEXT NOT NULL, + bytecode BLOB NOT NULL, + deployed_bytecode BLOB NOT NULL, + PRIMARY KEY (name) +); + +CREATE TABLE smart_contracts ( + address TEXT NOT NULL, + name TEXT NOT NULL, + PRIMARY KEY (address), + + foreign key (name) references smart_contract_abis(name) +); + +-- TODO: check if this can be better, just ported over from the old db +CREATE TABLE IF NOT EXISTS registrations ( + contract BLOB, + operator BLOB NOT NULL, + pubkey_bls BLOB NOT NULL, + pubkey_ed25519 BLOB NOT NULL, + sig_bls BLOB NOT NULL, + sig_ed25519 BLOB NOT NULL, + timestamp FLOAT NOT NULL DEFAULT ((julianday('now') - 2440587.5)*86400.0), /* unix epoch */ + + CHECK(length(pubkey_ed25519) == 32), + CHECK(length(pubkey_bls) == 64), + CHECK(length(sig_ed25519) == 64), + CHECK(length(sig_bls) == 128), + CHECK(length(operator) == 20), + CHECK(contract IS NULL OR length(contract) == 20) +) \ No newline at end of file diff --git a/db/util.py b/db/util.py new file mode 100644 index 0000000..77ea0a1 --- /dev/null +++ b/db/util.py @@ -0,0 +1,32 @@ +import logging +import sqlite3 + + +def init_db(db_path: str, schema_path: str): + assert db_path is not None and len(db_path) > 0 + assert schema_path is not None and len(schema_path) > 0 + with sqlite3.connect(db_path) as conn: + with open(schema_path, "r") as file: + schema_sql = file.read() + conn.executescript(schema_sql) + + +def is_db_initialized(db_path: str): + with sqlite3.connect(db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT name FROM sqlite_master WHERE type='table'") + db_tables = cursor.fetchall() + logging.debug(f"Database tables: {db_tables}") + return len(db_tables) > 0 + + +SQLITE_MAX_INT = 2**63 - 1 # 9,223,372,036,854,775,807 +SQLITE_MIN_INT = -(2**63) # -9,223,372,036,854,775,808 + + +def assert_all_dict_values_are_within_sqlite_integer_range(node: dict): + for key, value in node.items(): + if isinstance(value, int): + assert ( + SQLITE_MIN_INT <= value <= SQLITE_MAX_INT + ), f"Integer value {value} for key '{key}' in dict is out of SQLite integer range." diff --git a/db/write.py b/db/write.py new file mode 100644 index 0000000..2587a97 --- /dev/null +++ b/db/write.py @@ -0,0 +1,574 @@ +import json +import sqlite3 +import time +from contextlib import closing + +from web3 import Web3 + +from arbitrum import ContributionContractDetails +from log import Log +from oxen.rpc import ServiceNode, NetworkInfo +from web3client.abi_manager import ABIData +from web3client.event_scanner import ProcessedEvent + + +class DBWriter: + def __init__(self, db_path: str, log_level: int, perf: bool = False): + self.db_path = db_path + self.log = Log("db_writer", log_level, enable_perf=perf).logger + + def write_nodes_to_staging_db( + self, + height: int, + parsed_nodes: list[ServiceNode], + # TODO: type the contributor_stake_map properly + contributions: list[dict[str, int]], + ): + self.log.perf.start("write_to_db") + + with closing(sqlite3.connect(self.db_path)) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + + self.log.debug("Inserting {} service nodes".format(len(parsed_nodes))) + self.log.perf.start("write_nodes_to_staging_db -> insert nodes") + + cursor.executemany( + """ + INSERT INTO service_nodes_staging ( + active, + contract_id, + decommission_count, + earned_downtime_blocks, + fetched_block_height, + funded, + is_liquidatable, + is_removable, + last_reward_block_height, + last_uptime_proof, + lokinet_version, + operator_address, + operator_fee, + payable, + pubkey_bls, + pubkey_ed25519, + public_ip, + pulse_votes, + quorumnet_port, + registration_height, + registration_hf_version, + requested_unlock_height, + service_node_pubkey, + service_node_version, + staking_requirement, + state_height, + storage_lmq_port, + storage_port, + storage_server_version, + swarm, + swarm_id, + total_contributed + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + ( + node.get("active"), + node.get("contract_id"), + node.get("decommission_count"), + node.get("earned_downtime_blocks"), + height, + node.get("funded"), + node.get("is_liquidatable"), + node.get("is_removable"), + node.get("last_reward_block_height"), + node.get("last_uptime_proof"), + node.get("lokinet_version"), + node.get("operator_address"), + node.get("operator_fee"), + node.get("payable"), + node.get("pubkey_bls"), + node.get("pubkey_ed25519"), + node.get("public_ip"), + node.get("pulse_votes"), + node.get("quorumnet_port"), + node.get("registration_height"), + node.get("registration_hf_version"), + node.get("requested_unlock_height"), + node.get("service_node_pubkey"), + node.get("service_node_version"), + node.get("staking_requirement"), + node.get("state_height"), + node.get("storage_lmq_port"), + node.get("storage_port"), + node.get("storage_server_version"), + node.get("swarm"), + node.get("swarm_id"), + node.get("total_contributed"), + ) + for node in parsed_nodes + ), + ) + + inserted_nodes_rows = cursor.rowcount + + self.log.perf.end("write_nodes_to_staging_db -> insert nodes") + self.log.debug( + "Inserted {} rows into service_nodes_staging".format(inserted_nodes_rows) + ) + self.log.debug("Inserting {} service node contributions".format(len(contributions))) + self.log.perf.start("write_nodes_to_staging_db -> insert contributions") + + cursor.executemany( + """ + INSERT OR REPLACE INTO service_nodes_contributions_staging ( + address, + amount, + beneficiary, + contract_id, + fetched_block_height + ) + VALUES (?, ?, ?, ?, ?) + """, + ( + ( + contribution["address"], + contribution["amount"], + contribution["beneficiary"], + contribution["contract_id"], + height, + ) + for contribution in contributions + ), + ) + + inserted_contributions_rows = cursor.rowcount + + self.log.perf.end("write_nodes_to_staging_db -> insert contributions") + self.log.debug( + "Inserted {} rows into service_nodes_contributions_staging".format( + inserted_contributions_rows + ) + ) + + connection.commit() + self.log.perf.end("write_to_db") + + def write_nodes_to_main_db(self, immutable_height: int): + """ + Gets all nodes from the staging db at or below the immutable_height and writes them to the main db then remove + those nodes from the staging db. + """ + self.log.perf.start("write_nodes_to_main_db") + with closing(sqlite3.connect(self.db_path)) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + self.log.perf.start("write_nodes_to_main_db -> select nodes") + + cursor.execute( + """ + SELECT * FROM service_nodes_staging WHERE fetched_block_height = ? + """, + (immutable_height,), + ) + nodes = cursor.fetchall() + selected_nodes_count = len(nodes) + + self.log.perf.end("write_nodes_to_main_db -> select nodes") + self.log.info("Found {} nodes to write to main db".format(selected_nodes_count)) + + # We only want to continue here if there are any nodes ready to commit. + if selected_nodes_count == 0: + self.log.debug("No nodes ready to commit") + return + + self.log.perf.start("write_nodes_to_main_db -> insert nodes") + + cursor.executemany( + """ + INSERT OR REPLACE INTO service_nodes_main ( + active, + contract_id, + decommission_count, + earned_downtime_blocks, + fetched_block_height, + funded, + is_liquidatable, + is_removable, + last_reward_block_height, + last_uptime_proof, + lokinet_version, + operator_address, + operator_fee, + payable, + pubkey_bls, + pubkey_ed25519, + public_ip, + pulse_votes, + quorumnet_port, + registration_height, + registration_hf_version, + requested_unlock_height, + service_node_pubkey, + service_node_version, + staking_requirement, + state_height, + storage_lmq_port, + storage_port, + storage_server_version, + swarm, + swarm_id, + total_contributed + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + nodes, + ) + + inserted_or_updated_rows = cursor.rowcount + + self.log.perf.end("write_nodes_to_main_db -> insert nodes") + self.log.info("Wrote {} rows to main db".format(inserted_or_updated_rows)) + + if inserted_or_updated_rows != len(nodes): + self.log.error( + "Inserted or updated {} rows, but expected {}".format( + inserted_or_updated_rows, len(nodes) + ) + ) + connection.rollback() + self.log.perf.end("write_nodes_to_main_db") + return + + self.log.perf.start("write_nodes_to_main_db -> select contributions") + + cursor.execute( + """ + SELECT * FROM service_nodes_contributions_staging WHERE fetched_block_height = ? + """, + (immutable_height,), + ) + contributions = cursor.fetchall() + selected_contributions_count = len(contributions) + + self.log.perf.end("write_nodes_to_main_db -> select contributions") + self.log.debug( + "Found {} contributions to write to main db".format( + selected_contributions_count + ) + ) + self.log.perf.start("write_nodes_to_main_db -> insert contributions") + + cursor.executemany( + """ + INSERT OR REPLACE INTO service_nodes_contributions_main (address, amount, beneficiary, contract_id, fetched_block_height) + VALUES (?, ?, ?, ?, ?) + """, + contributions, + ) + + inserted_contributions_rows = cursor.rowcount + + self.log.perf.end("write_nodes_to_main_db -> insert contributions") + self.log.info("Wrote {} rows to main db".format(inserted_contributions_rows)) + + if inserted_contributions_rows != len(contributions): + self.log.error( + "Inserted {} rows, but expected {}".format( + inserted_contributions_rows, len(contributions) + ) + ) + connection.rollback() + return + + self.log.perf.start("write_nodes_to_main_db -> delete nodes") + + cursor.execute( + """ + DELETE FROM service_nodes_staging WHERE fetched_block_height <= ? + """, + (immutable_height,), + ) + + deleted_rows = cursor.rowcount + + self.log.perf.end("write_nodes_to_main_db -> delete nodes") + self.log.info("Deleted {} rows from staging db".format(deleted_rows)) + self.log.perf.start("write_nodes_to_main_db -> delete contributions") + + cursor.execute( + """ + DELETE FROM service_nodes_contributions_staging WHERE fetched_block_height <= ? + """, + (immutable_height,), + ) + + deleted_contributions_rows = cursor.rowcount + + self.log.perf.end("write_nodes_to_main_db -> delete contributions") + self.log.info( + "Deleted {} rows from staging contributions db".format( + deleted_contributions_rows + ) + ) + + connection.commit() + self.log.info("Transaction committed successfully") + + self.log.perf.end("write_nodes_to_main_db") + + def write_network_info_to_db( + self, + network: NetworkInfo, + ): + self.log.perf.start("write_network_info_to_db") + with closing(sqlite3.connect(self.db_path)) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute( + """ + INSERT OR REPLACE INTO network_info ( + id, + block_hash, + block_height, + block_timestamp, + hard_fork, + immutable_block_hash, + immutable_block_height, + max_stakers, + min_operator_contribution, + nettype, + pulse_target_timestamp, + staking_requirement, + version + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + 1, + network.block_hash, + network.block_height, + time.time().__floor__(), + network.hard_fork, + network.immutable_block_hash, + network.immutable_block_height, + network.max_stakers, + network.min_operator_contribution, + network.nettype, + network.pulse_target_timestamp, + network.staking_requirement, + network.version, + ), + ) + connection.commit() + self.log.perf.end("write_network_info_to_db") + + def write_arbitrum_events_to_db(self, events: list[ProcessedEvent]): + self.log.perf.start("write_arbitrum_events_to_db") + + with closing(sqlite3.connect(self.db_path)) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + + self.log.debug("Inserting {} events into arbitrum_events".format(len(events))) + self.log.perf.start("write_arbitrum_events_to_db -> insert events") + + cursor.executemany( + """ + INSERT OR REPLACE INTO arbitrum_events ( + block, + tx, + name, + args + ) + VALUES (?, ?, ?, ?) + """, + ( + ( + event.block, + event.tx, + event.name, + Web3.to_json(dict(event.args)), + ) + for event in events + ), + ) + + inserted_or_updated_rows_count = cursor.rowcount + + self.log.perf.end("write_arbitrum_events_to_db -> insert events") + self.log.debug( + "Inserted or updated {} rows into arbitrum_events".format( + inserted_or_updated_rows_count + ) + ) + + connection.commit() + self.log.perf.end("write_arbitrum_events_to_db") + + def write_contribution_contracts_to_db( + self, contracts: list[ContributionContractDetails], contributions_list: list + ): + self.log.perf.start("write_contribution_contracts_to_db") + + with closing(sqlite3.connect(self.db_path)) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + + self.log.debug("Inserting {} contribution contracts".format(len(contracts))) + self.log.perf.start("write_contribution_contracts_to_db -> insert contracts") + + cursor.executemany( + """ + INSERT OR REPLACE INTO contribution_contracts ( + address, + fee, + operator_address, + pubkey_bls, + service_node_pubkey, + service_node_signature, + status + ) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, + ( + ( + contract.address, + contract.fee, + contract.operator_address, + contract.pubkey_bls, + contract.service_node_pubkey, + contract.service_node_signature, + contract.status, + ) + for contract in contracts + ), + ) + + inserted_contract_rows = cursor.rowcount + + self.log.perf.end("write_contribution_contracts_to_db -> insert contracts") + self.log.debug( + "Inserted or Updated {} rows into contribution_contracts".format( + inserted_contract_rows + ) + ) + self.log.debug( + "Inserting {} contract contributions".format(len(contributions_list)) + ) + self.log.perf.start( + "write_contribution_contracts_to_db -> insert contribution contracts contributions" + ) + + cursor.executemany( + """ + INSERT OR REPLACE INTO contribution_contracts_contributions ( + address, + amount, + beneficiary_address, + contract_address + ) + VALUES (?, ?, ?, ?) + """, + ( + ( + contribution["address"], + contribution["amount"], + contribution["beneficiary_address"], + contribution["contract_address"], + ) + for contribution in contributions_list + ), + ) + + inserted_contributions_rows = cursor.rowcount + + self.log.perf.end( + "write_contribution_contracts_to_db -> insert contribution contracts contributions" + ) + self.log.debug( + "Inserted {} rows into contribution_contracts_contributions".format( + inserted_contributions_rows + ) + ) + + connection.commit() + self.log.perf.end("write_contribution_contracts_to_db") + + def write_smart_contract_abis_to_db(self, abis: list[ABIData]): + self.log.perf.start("write_smart_contract_abis_to_db") + with closing(sqlite3.connect(self.db_path)) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + + self.log.debug("Inserting {} smart contract abis".format(len(abis))) + self.log.perf.start("write_smart_contract_abis_to_db -> insert abis") + + cursor.executemany( + """ + INSERT OR REPLACE INTO smart_contract_abis ( + name, + abi, + bytecode, + deployed_bytecode + ) + VALUES (?, ?, ?, ?) + """, + ( + ( + abi.name, + json.dumps(abi.abi), + abi.bytecode, + abi.deployed_bytecode, + ) + for abi in abis + ), + ) + + inserted_abi_rows = cursor.rowcount + + self.log.perf.end("write_smart_contract_abis_to_db -> insert abis") + self.log.debug( + "Inserted {} rows into smart_contract_abis".format(inserted_abi_rows) + ) + + connection.commit() + self.log.perf.end("write_smart_contract_abis_to_db") + + def write_smart_contract_details_to_db( + self, + contracts, + ): + self.log.perf.start("write_smart_contract_details_to_db") + with closing(sqlite3.connect(self.db_path)) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + + self.log.debug("Inserting {} smart contract details".format(len(contracts))) + self.log.perf.start("write_smart_contract_details_to_db -> insert contracts") + + cursor.executemany( + """ + INSERT OR REPLACE INTO smart_contracts ( + address, + name + ) + VALUES (?, ?) + """, + ( + ( + contract.get("address"), + contract.get("name"), + ) + for contract in contracts + ), + ) + + inserted_details_rows = cursor.rowcount + + self.log.perf.end("write_smart_contract_details_to_db -> insert details") + self.log.debug( + "Inserted {} rows into smart_contract_details".format(inserted_details_rows) + ) + + connection.commit() + self.log.perf.end("write_smart_contract_details_to_db") From 8f192884b3cdff5a000825481c767031695a0bcc Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Wed, 20 Nov 2024 17:23:03 +1100 Subject: [PATCH 007/138] feat: registrations reader --- registration/read.py | 66 ++++++++++++++ registrations.py | 204 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 270 insertions(+) create mode 100644 registration/read.py create mode 100644 registrations.py diff --git a/registration/read.py b/registration/read.py new file mode 100644 index 0000000..3f7ef87 --- /dev/null +++ b/registration/read.py @@ -0,0 +1,66 @@ +import sqlite3 +from contextlib import closing +from dataclasses import dataclass + +from log import Log + + +@dataclass +class Registration: + contract: bytes + operator: bytes + pubkey_bls: bytes + pubkey_ed25519: bytes + sig_bls: bytes + sig_ed25519: bytes + timestamp: float + + +class DBReaderRegistrations: + def __init__(self, db_path: str, log_level: int, perf: bool = False): + self.db_path = db_path + self.log = Log("db_reader", log_level, enable_perf=perf).logger + + def get_registrations(self): + self.log.perf.start("get_registrations") + with closing(sqlite3.connect(self.db_path)) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute( + """ + SELECT * FROM registrations ORDER BY timestamp DESC + """ + ) + registrations = [Registration(*registration) for registration in cursor.fetchall()] + self.log.debug("Registrations: {}".format(len(registrations))) + self.log.perf.end("get_registrations") + return registrations + + def get_registrations_for_operator(self, operator: bytes): + self.log.perf.start("get_registrations_for_operator") + with closing(sqlite3.connect(self.db_path)) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute( + """ + SELECT * FROM registrations WHERE operator = ? ORDER BY timestamp DESC + """, + (operator,), + ) + registrations = [Registration(*registration) for registration in cursor.fetchall()] + self.log.debug("Registrations: {}".format(len(registrations))) + self.log.perf.end("get_registrations_for_operator") + return registrations + + def get_registrations_by_pubkey(self, pubkey: bytes): + self.log.perf.start("get_registrations_by_pubkey") + with closing(sqlite3.connect(self.db_path)) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute( + """ + SELECT * FROM registrations WHERE pubkey_ed25519 = ? ORDER BY timestamp DESC + """, + (pubkey,), + ) + registrations = [Registration(*registration) for registration in cursor.fetchall()] + self.log.debug("Registrations: {}".format(len(registrations))) + self.log.perf.end("get_registrations_by_pubkey") + return registrations diff --git a/registrations.py b/registrations.py new file mode 100644 index 0000000..3f8d418 --- /dev/null +++ b/registrations.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +import flask +import time + +import eth_utils +import subprocess +import config + +from db.util import is_db_initialized, init_db +from log import Log +from registration.read import DBReaderRegistrations +from registration.validation import check_reg_keys_sigs +from registration.write import DBWriterRegistrations +from util.data import DataManager +from util.parse import ( + parse_query_params, + byte_decoder, + EthConverter, + hexify, + Hex64Converter, + raw_eth_addr, +) + + +class App(flask.Flask): + def __init__(self, name): + super().__init__(__name__) + log = Log(name, enable_perf=config.backend.performance_logging) + log.set_level(config.backend.log_level) + git_rev = subprocess.run( + ["git", "rev-parse", "--short=9", "HEAD"], stdout=subprocess.PIPE, text=True + ) + self.git_rev = git_rev.stdout.strip() if git_rev.returncode == 0 else "(unknown)" + + # Creates a generic logger to pipe other packages logs into the main app logger + generic_logger = Log(None) + generic_logger.set_level( + config.backend.log_level_generic + if config.backend.log_level_generic is not None + else config.backend.log_level + ) + self.log = log.logger + + if not is_db_initialized(config.backend.registration_sqlite_db): + self.log.info( + "Initializing database {} with schema {}".format( + config.backend.registration_sqlite_db, config.backend.registration_sqlite_schema + ) + ) + init_db( + config.backend.registration_sqlite_db, config.backend.registration_sqlite_schema + ) + + self.db_reader = DBReaderRegistrations( + db_path=config.backend.registration_sqlite_db, + log_level=config.backend.log_level, + perf=config.backend.performance_logging, + ) + self.db_writer = DBWriterRegistrations( + db_path=config.backend.registration_sqlite_db, + log_level=config.backend.log_level, + perf=config.backend.performance_logging, + ) + + self.data = DataManager(stale_time_seconds=config.backend.stale_time_seconds) + + self.allowed_contract_names = set() + + +app = App( + config.backend.registration_api_name if config.backend.registration_api_name else __name__ +) + + +app.url_map.converters["hex64"] = Hex64Converter +app.url_map.converters["eth_wallet"] = EthConverter + + +def json_response(vals): + """ + Takes a dict, adds some general info fields to it, and jsonifies it for a flask route function + return value. The dict gets passed through `hexify` first to convert any bytes values to hex. + """ + hexify(vals) + return flask.jsonify({**vals, "t": time.time()}) + + +@app.route("/info") +def get_network_info(): + return json_response({}) + + +""" +////////////////////////////////////////////////////////////// +// // +// Registration Endpoints // +// // +////////////////////////////////////////////////////////////// +""" + + +@app.route("/registrations/") +def operator_registrations(operator: str): + """ + Retrieves stored registration(s) for the given 'operator'. + + This returns an array in the "registrations" field containing as many registrations as are + currently stored for the given operator wallet, sorted from most to least recently submitted. + + Fields are the same as the version of this endpoint that takes a SN pubkey. + + Returns the JSON response with the 'registrations' for the given 'operator'. + """ + + operator_bytes = bytes.fromhex(operator[2:]) + + return json_response( + { + "registrations": app.data.get( + f"op-{operator_bytes}", + getter=app.db_reader.get_registrations_for_operator, + getter_args=operator_bytes, + ) + } + ) + + +@app.route("/registrations/") +def sn_pubkey_registrations(sn_pubkey: bytes) -> flask.Response: + """ + Retrieves stored registration(s) for the given service node pubkey. + + This returns an array in the "registrations" field containing either one or two registration + info dicts: a solo registration (if known) and a multi-contributor contract registration (if + known). These are sorted by timestamp of when the registration was last received/updated. + + Fields in each dict: + - "operator": the operator address. + - "contract": the contract address, for "type": "contract" and omitted for "type": "solo". + - "pubkey_ed25519": the primary SN pubkey, in hex. + - "pubkey_bls": the SN BLS pubkey, in hex. + - "sig_ed25519": the SN pubkey signed registration signature. + - "sig_bls": the SN BLS pubkey signed registration signature. + - "timestamp": the unix timestamp when this registration was received (or last updated) + + Returns the JSON response with the 'registrations' for the given 'sn_pubkey'. + """ + result = json_response( + { + "registrations": app.data.get( + f"sn-{sn_pubkey}", + getter=app.db_reader.get_registrations_by_pubkey, + getter_args=sn_pubkey, + ) + } + ) + return result + + +@app.route("/registrations/", methods=["POST"]) +@app.route("/store/", methods=["GET", "POST"]) +def store_registration(sn_pubkey: bytes): + """ + Stores (or replaces) the pubkeys/signatures associated with a service node that are needed to + call the smart contract to create a SN registration. These pubkeys/signatures are stored + indefinitely, allowing the operator to call them up whenever they like to re-submit a + registration for the same node. There is nothing confidential here: the values will be publicly + broadcast as part of the registration process already, and are constructed in such a way that + only the operator wallet can submit a registration using them. + + This works for both solo registrations and multi-registrations: for the latter, a contract + address is passed in the "c" parameter. + + The distinction at the SN layer is that contract registrations sign the contract address while + solo registrations sign the operator address. For submission to the blockchain, a contract + stake requires an additional interaction through a multi-contributor contract while solo + registrations can call the staking contract directly. + """ + + try: + params = parse_query_params( + { + "pubkey_bls": byte_decoder(64), + "sig_ed25519": byte_decoder(64), + "sig_bls": byte_decoder(128), + "-contract": raw_eth_addr, + "operator": raw_eth_addr, + } + ) + + params["pubkey_ed25519"] = sn_pubkey + + check_reg_keys_sigs(params) + except ValueError as e: + return json_response({"error": f"Invalid registration: {e}"}) + + app.db_writer.write_registration_to_db(params) + + params["operator"] = eth_utils.to_checksum_address(params["operator"]) + params["contract"] = ( + eth_utils.to_checksum_address(params["contract"]) if "contract" in params else None + ) + + return json_response({"success": True, "registration": params}) From e7e14c2adc30277ea519c0ac4e1c6b18ef0c4f3a Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Wed, 20 Nov 2024 17:28:08 +1100 Subject: [PATCH 008/138] feat: overhaul config --- .gitignore | 2 +- config_defaults.py | 114 ++++++++++++++++++++++++++++++--------------- config_validate.py | 79 +++++++++++++++++++++++++++++++ 3 files changed, 156 insertions(+), 39 deletions(-) create mode 100644 config_validate.py diff --git a/.gitignore b/.gitignore index 1a702ee..4c15328 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -/sent-backend.db* +/*.db /config.py /__pycache__ /oxend/*.sock diff --git a/config_defaults.py b/config_defaults.py index a8de692..0a95d1c 100644 --- a/config_defaults.py +++ b/config_defaults.py @@ -15,52 +15,90 @@ B58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + class Backend: - sqlite_db: str = 'sent-backend.db' - thread_pool_max_workers: int = 10 - stale_time_seconds: int = 30 - rpc: str = '' - oxen_wallet_regex: str = '' - reward_rate_pool_addr: str = '0x0000000000000000000000000000000000000000' - sn_contrib_factory_addr: str = '0x0000000000000000000000000000000000000000' - sn_rewards_addr: str = '0x0000000000000000000000000000000000000000' - sn_token_addr: str = '0x0000000000000000000000000000000000000000' - provider_url: str = 'http://localhost:8545' # Default hardhat private chain node address - log_level = logging.INFO + """ + SHARED CONFIG + """ + log_level = logging.INFO + log_level_generic = None # Logs from other packages will use log_level if this is not set + oxen_wallet_regex: str = "" + sqlite_db: str = "sent-backend.db" + sqlite_schema: str = "db/schema.sql" + rpc_shared: str = "" + rpc_shared_cache: int = 2 + + """ + API CONFIG + """ + api_name: str = "api" + rpc_api: str = "" + rpc_api_cache: int = 2 + """ + REGISTRATION API CONFIG + """ + registration_api_name: str = "registration_api" + registration_sqlite_db: str = "sent-backend-registrations.db" + registration_sqlite_schema: str = "registration/schema.sql" + + """ + FETCHER CONFIG + """ + abi_dir = "web3client/abis" + # Arbitrum runs at ~4 blocks per second, and the rpc node has a limit of 30m, so scan for 120 blocks + arbitrum_rescan_safety_blocks: int = 120 + addr_reward_rate_pool: str = "0x0000000000000000000000000000000000000000" + addr_sent: str = "0x0000000000000000000000000000000000000000" + addr_sn_contrib: str = "0x0000000000000000000000000000000000000000" + addr_sn_contrib_factory: str = "0x0000000000000000000000000000000000000000" + addr_sn_rewards: str = "0x0000000000000000000000000000000000000000" + refresh_rate_seconds_arbitrum: int = 30 + max_time_keeper_events: int = 10_000 + fetcher_name: str = "fetcher" + performance_logging: bool = False + rpc_fetcher: str = "" + rpc_fetcher_cache: int = 2 + stale_time_seconds: int = 30 + stale_time_seconds_contract_abis: int = 300 + thread_pool_max_workers: int = 50 + web3_caller_address: str | None = None + web3_private_key: str | None = None + web3_provider_url: str = "http://localhost:8545" # Default hardhat private chain node address) + # Session mainnet contracts -mainnet_backend = Backend() -mainnet_backend.sqlite_db = 'ssb-mainnet.db' -mainnet_backend.rpc = 'ipc://oxend/mainnet.sock' -mainnet_backend.oxen_wallet_regex = f'L[{B58_ALPHABET}]{{94}}"' +mainnet_backend = Backend() +mainnet_backend.oxen_wallet_regex = f'L[{B58_ALPHABET}]{{94}}"' +mainnet_backend.rpc_shared = "ipc://oxend/mainnet.sock" +mainnet_backend.sqlite_db = "ssb-mainnet.db" # Session testnet contracts -testnet_backend = Backend() -testnet_backend.sqlite_db = 'ssb-testnet.db' -testnet_backend.rpc = 'ipc://oxend/testnet.sock' -testnet_backend.oxen_wallet_regex = f"T[{B58_ALPHABET}]{{96}}" +testnet_backend = Backend() +testnet_backend.oxen_wallet_regex = f"T[{B58_ALPHABET}]{{96}}" +testnet_backend.rpc_shared = "ipc://oxend/testnet.sock" +testnet_backend.sqlite_db = "ssb-testnet.db" # Session devnet.v3 contracts -devnet_backend = Backend() -devnet_backend.sqlite_db = 'ssb-devnet.db' -devnet_backend.rpc = 'ipc://oxend/devnet.sock' -devnet_backend.oxen_wallet_regex = f"dV[{B58_ALPHABET}]{{95}}" -devnet_backend.reward_rate_pool_addr = '0xb515C61DE12f28eE908a905b930aFb80B9bAd7cf' -devnet_backend.sn_contrib_factory_addr = '0x0000000000000000000000000000000000000000' -devnet_backend.sn_rewards_addr = '0x75Dc11700b2D03902FCb5Ca7aFd6A859a1Fa25Cb' -devnet_backend.sn_token_addr = '0x0000000000000000000000000000000000000000' -devnet_backend.provider_url = 'https://sepolia-rollup.arbitrum.io/rpc' +devnet_backend = Backend() +devnet_backend.addr_reward_rate_pool = "0xb515C61DE12f28eE908a905b930aFb80B9bAd7cf" +devnet_backend.addr_sn_contrib = "0x0000000000000000000000000000000000000000" +devnet_backend.addr_sn_contrib_factory = "0x0000000000000000000000000000000000000000" +devnet_backend.addr_sn_rewards = "0x75Dc11700b2D03902FCb5Ca7aFd6A859a1Fa25Cb" +devnet_backend.oxen_wallet_regex = f"dV[{B58_ALPHABET}]{{95}}" +devnet_backend.rpc_shared = "ipc://oxend/devnet.sock" +devnet_backend.sqlite_db = "ssb-devnet.db" +devnet_backend.web3_provider_url = "https://sepolia-rollup.arbitrum.io/rpc" # Session stagenet.v3 contracts -stagenet_backend = Backend() -stagenet_backend.sqlite_db = 'ssb-stagenet.db' -stagenet_backend.rpc = 'ipc://oxend/stagenet.sock' -stagenet_backend.oxen_wallet_regex = f"ST[{B58_ALPHABET}]{{95}}" -stagenet_backend.reward_rate_pool_addr = '0x38cD8D3F93d591C18cf26B3Be4CB2c872aC37953' -stagenet_backend.sn_contrib_factory_addr = '0x66d0D4f71267b3150DafF7bD486AC5E097E7E4C6' -stagenet_backend.sn_rewards_addr = '0x4abfFB7f922767f22c7aa6524823d93FDDaB54b1' -stagenet_backend.sn_token_addr = '0x70c1f36C9cEBCa51B9344121D284D85BE36CD6bB' -stagenet_backend.provider_url = 'https://sepolia-rollup.arbitrum.io/rpc' +stagenet_backend = Backend() +stagenet_backend.addr_reward_rate_pool = "0x38cD8D3F93d591C18cf26B3Be4CB2c872aC37953" +stagenet_backend.addr_sn_contrib = "0x70c1f36C9cEBCa51B9344121D284D85BE36CD6bB" +stagenet_backend.addr_sn_contrib_factory = "0x66d0D4f71267b3150DafF7bD486AC5E097E7E4C6" +stagenet_backend.addr_sn_rewards = "0x4abfFB7f922767f22c7aa6524823d93FDDaB54b1" +stagenet_backend.oxen_wallet_regex = f"ST[{B58_ALPHABET}]{{95}}" +stagenet_backend.rpc_shared = "tcp://localhost:6786" +stagenet_backend.sqlite_db = "ssb-stagenet.db" +stagenet_backend.web3_provider_url = "http://10.24.0.1/arb_sepolia" # Assign the active backend to be used in the sent-staking-backend -backend = stagenet_backend +backend = stagenet_backend diff --git a/config_validate.py b/config_validate.py new file mode 100644 index 0000000..3fb7944 --- /dev/null +++ b/config_validate.py @@ -0,0 +1,79 @@ +import logging + +import config +from log import Log +from oxen.omq import omq_connection +from oxen.rpc import OxenRPC +from util import is_not_empty_string, valid_address_assertion + + +def validate_config(conf: config): + log = Log("config_validate").logger + log.perf.start("validate_config") + log.info("Validating config") + + """ + Production warnings + """ + if conf.backend.log_level < logging.INFO: + log.warning( + "Log level is set to {} which is less than INFO. This is not recommended for production.".format( + conf.backend.log_level + ) + ) + elif conf.backend.log_level > logging.ERROR: + log.warning( + "Log level is set to {} which is greater than ERROR. This is not recommended for production.".format( + conf.backend.log_level + ) + ) + + if conf.backend.performance_logging: + log.warning("Performance logging is enabled. This is not recommended for production.") + + """ + Validations + """ + + assert is_not_empty_string(conf.backend.sqlite_db), "sqlite_db is not set in config.py" + assert is_not_empty_string(conf.backend.rpc_fetcher), "rpc is not set in config.py" + assert is_not_empty_string( + conf.backend.oxen_wallet_regex + ), "oxen_wallet_regex is not set in config.py" + + # Assert all contract addresses are valid + valid_address_assertion(conf.backend.addr_sn_contrib, "addr_sn_contrib") + valid_address_assertion(conf.backend.addr_sent, "addr_sent") + valid_address_assertion(conf.backend.addr_sn_rewards, "addr_sn_rewards") + valid_address_assertion(conf.backend.addr_reward_rate_pool, "addr_reward_rate_pool") + + assert is_not_empty_string( + conf.backend.web3_provider_url + ), "web3_provider_url is not set in config.py" + + """ + Web3 client validations + """ + # web3_client = Web3Client( + # conf.backend.web3_provider_url, + # conf.backend.web3_caller_address, + # conf.backend.web3_private_key, + # log, + # ) + # block_number = web3_client.web3.eth.block_number + # log.debug("Config validation block number: {}".format(block_number)) + # assert block_number is not None, "Failed to get block number from web3 provider" + + """ + Oxen RPC validations + """ + omq, oxend = omq_connection(conf.backend.rpc_fetcher) + rpc = OxenRPC(log, conf.backend.rpc_fetcher, conf.backend.rpc_fetcher_cache) + res = rpc.get_info(omq, oxend).get() + log.debug("Config validation rpc response status: {}".format(res.get("status"))) + assert ( + res is not None and res.get("status") == "OK" + ), "Oxen RPC ping to {} failed with response: {}".format(conf.backend.rpc_fetcher, res) + + log.info("Config validation finished") + log.perf.end("validate_config") From 9968ad7dacd61b5b0d50ea9645b86a8ddffce262 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Wed, 20 Nov 2024 17:28:28 +1100 Subject: [PATCH 009/138] feat: create fetcher --- arbitrum.py | 144 +++++++++++++++++ fetcher.py | 449 ++++++++++++++++++++++++++++++++++++++++++++++++++++ timer.py | 25 --- 3 files changed, 593 insertions(+), 25 deletions(-) create mode 100644 arbitrum.py create mode 100644 fetcher.py delete mode 100644 timer.py diff --git a/arbitrum.py b/arbitrum.py new file mode 100644 index 0000000..0b8d384 --- /dev/null +++ b/arbitrum.py @@ -0,0 +1,144 @@ +import logging +from dataclasses import dataclass + +import eth_utils + +from web3client.client import Web3Client +from web3client.contracts.service_node_contribution import ServiceNodeContributionInterface +from web3client.contracts.service_node_contribution_factory import ServiceNodeContributionFactory +from web3client.contracts.service_node_rewards import ServiceNodeRewardsInterface + + +# TODO: we should be able to remove this once contract_id is always available via rpc.get_service_nodes +def get_service_node_rewards_contract_id_map(contract: ServiceNodeRewardsInterface): + """ + Update the map of service node contract ids to BLS public keys. This fetches the list of all service nodes from the + Service Node Rewards contract and maps them to their corresponding contract ids. + """ + [ids, bls_keys] = contract.get_all_service_node_contract_ids() + return {f"{x:064x}{y:064x}": contract_id for contract_id, (x, y) in zip(ids, bls_keys)} + + +def get_new_contribution_contracts( + web3_client: Web3Client, + logger: logging, + interface: ServiceNodeContributionFactory, + last_block: int, + end_block: int, +): + logger.perf.start("get_new_contribution_contracts") + events = interface.event_scanner.run(last_block=last_block, end_block=end_block) + + logger.perf.start("create_contribution_contract_instances") + contracts = [ + ServiceNodeContributionInterface(web3_client, event.args.contributorContract) + for event in events + if eth_utils.is_address(event.args.contributorContract) + ] + logger.perf.end("create_contribution_contract_instances") + logger.debug("Found {} new contract events".format(len(events))) + logger.perf.end("get_new_contribution_contracts") + return contracts, events + + +@dataclass +class ContributionContractDetails: + address: str | None + fee: int | None + operator_address: str | None + pubkey_bls: str | None + service_node_pubkey: str | None + service_node_signature: str | None + status: int | None + + +def update_contribution_contract_details( + web3_client: Web3Client, + logger: logging, + contracts: list[ServiceNodeContributionInterface], + max_requests_per_batch=1000, +): + logger.perf.start("chunk_contribution_contract_instances") + assert len(contracts) > 0, "Expected at least one contract" + assert max_requests_per_batch > 0, "Expected max_requests_per_batch > 0" + + requests_per_contract = ( + ServiceNodeContributionInterface.add_details_fetch_to_batch_added_batches() + ) + + max_chunk_size = max_requests_per_batch // requests_per_contract + + logger.debug( + "contracts: {}, requests_per_contract: {}, max_chunk_size: {}".format( + len(contracts), max_requests_per_batch, max_chunk_size + ) + ) + + chunks = [contracts[i : i + max_chunk_size] for i in range(0, len(contracts), max_chunk_size)] + logger.perf.end("chunk_contribution_contract_instances") + + logger.perf.start("fetch_contribution_contract_details total") + responses = [] + for chunk in chunks: + + assert len(chunk) <= max_requests_per_batch, "Expected chunk size <= {} got {}".format( + max_requests_per_batch, len(chunk) + ) + + logger.perf.start("fetch_contribution_contract_details_chunk of size {}".format(len(chunk))) + with web3_client.web3.batch_requests() as batch: + for contract in chunk: + contract.add_details_fetch_to_batch(batch) + + res = batch.execute() + responses.extend(res) + logger.perf.end("fetch_contribution_contract_details_chunk of size {}".format(len(chunk))) + + assert ( + len(responses) == len(contracts) * requests_per_contract + ), "Expected {} responses, got {}".format(len(contracts), len(responses)) + + contract_details = [] + contributions_list = [] + + for i in range(0, len(responses), requests_per_contract): + contract_address = contracts[i // requests_per_contract].contract_address + + params = responses[i] + + operator_address = responses[i + 1] + pubkey_bls_data = responses[i + 2] + + contributions = responses[i + 3] + contributions_addresses = contributions[0] + contributions_beneficiaries = contributions[1] + contributions_amounts = contributions[2] + + for j in range(len(contributions_addresses)): + contributions_list.append( + { + "contract_address": contract_address, + "address": contributions_addresses[j], + "amount": contributions_amounts[j], + "beneficiary_address": contributions_beneficiaries[j], + } + ) + + status = responses[i + 4] + + contract_details.append( + ContributionContractDetails( + address=contract_address, + service_node_pubkey=f"{params[0]:032x}", + service_node_signature=f"{params[1]:032x}{params[2]:032x}", + fee=params[3], + operator_address=operator_address, + pubkey_bls="0x{:0128x}".format((pubkey_bls_data[0] << 256) + pubkey_bls_data[1]), + status=status, + ) + ) + + logger.debug("Fetched details for {} contracts".format(len(contract_details))) + + logger.perf.end("fetch_contribution_contract_details total") + return contract_details, contributions_list diff --git a/fetcher.py b/fetcher.py new file mode 100644 index 0000000..6a0d64c --- /dev/null +++ b/fetcher.py @@ -0,0 +1,449 @@ +#!/usr/bin/env python3 +import json +import subprocess +import time + +import config +from arbitrum import ( + get_service_node_rewards_contract_id_map, + get_new_contribution_contracts, + update_contribution_contract_details, +) +from config_validate import validate_config +from db.util import ( + assert_all_dict_values_are_within_sqlite_integer_range, + is_db_initialized, + init_db, +) +from db.read import DBReader +from db.write import DBWriter +from log import Log +from oxen.rpc import ServiceNode, OxenRPC, NetworkInfo +from util import format_seconds +from log.time_keeper import TimeKeeper +from web3client.abi_manager import ABIManager +from web3client.client import Web3Client +from web3client.contracts.reward_rate_pool import RewardRatePoolInterface +from web3client.contracts.service_node_contribution import ( + ServiceNodeContributionInterface, +) +from web3client.contracts.service_node_contribution_factory import ( + ServiceNodeContributionFactory, +) +from web3client.contracts.service_node_rewards import ServiceNodeRewardsInterface +from web3client.contracts.sent import SENTInterface +from oxen.omq import omq_connection + + +class App: + def __init__(self, name): + super().__init__() + log = Log(name, enable_perf=config.backend.performance_logging) + log.set_level(config.backend.log_level) + + git_rev = subprocess.run( + ["git", "rev-parse", "--short=9", "HEAD"], stdout=subprocess.PIPE, text=True + ) + self.git_rev = git_rev.stdout.strip() if git_rev.returncode == 0 else "(unknown)" + + # Creates a generic logger to pipe other packages logs into the main app logger + generic_logger = Log(None) + generic_logger.set_level( + config.backend.log_level_generic + if config.backend.log_level_generic is not None + else config.backend.log_level + ) + + self.log = log.logger + validate_config(config) + if not is_db_initialized(config.backend.sqlite_db): + self.log.info( + "Initializing database {} with schema {}".format( + config.backend.sqlite_db, config.backend.sqlite_schema + ) + ) + init_db(config.backend.sqlite_db, config.backend.sqlite_schema) + + self.db_reader = DBReader( + db_path=config.backend.sqlite_db, + log_level=config.backend.log_level, + perf=config.backend.performance_logging, + ) + self.db_writer = DBWriter( + db_path=config.backend.sqlite_db, + log_level=config.backend.log_level, + perf=config.backend.performance_logging, + ) + + rpc_url = ( + config.backend.rpc_fetcher if config.backend.rpc_fetcher else config.backend.rpc_shared + ) + rpc_cache = ( + config.backend.rpc_fetcher_cache + if config.backend.rpc_fetcher_cache + else config.backend.rpc_shared_cache + ) + + self.rpc = OxenRPC( + self.log, + rpc_url, + rpc_cache, + ) + self.loop_sleep_refresh_rate_seconds = rpc_cache if rpc_cache > 0 else 5 + + self.arbitrum_details_last_updated = 0 + + self.web3_client = Web3Client( + provider_url=config.backend.web3_provider_url, + caller_address=config.backend.web3_caller_address, + private_key=config.backend.web3_private_key, + logger=self.log, + abi_manager=ABIManager(db_writer=self.db_writer, abi_dir=config.backend.abi_dir), + ) + + self.token_contract = SENTInterface( + web3_client=self.web3_client, contract_address=config.backend.addr_sent + ) + self.service_node_rewards = ServiceNodeRewardsInterface( + web3_client=self.web3_client, + contract_address=config.backend.addr_sn_rewards, + scanner_safety_blocks=config.backend.arbitrum_rescan_safety_blocks, + ) + self.reward_rate_pool = RewardRatePoolInterface( + web3_client=self.web3_client, contract_address=config.backend.addr_reward_rate_pool + ) + self.service_node_contribution_factory = ServiceNodeContributionFactory( + web3_client=self.web3_client, + contract_address=config.backend.addr_sn_contrib_factory, + scanner_safety_blocks=config.backend.arbitrum_rescan_safety_blocks, + ) + self.service_node_contribution = ServiceNodeContributionInterface( + web3_client=self.web3_client, + contract_address=config.backend.addr_sn_contrib, + ) + self.service_node_contribution_multi: dict[str, ServiceNodeContributionInterface] = {} + + self.time_keeper = TimeKeeper( + logger=Log("time_keeper").logger, + perf=config.backend.performance_logging, + max_events=config.backend.max_time_keeper_events, + ) + + self.bootstrap() + + def bootstrap(self): + self.log.info("Bootstrapping") + self.log.perf.start("bootstrap") + + contribution_contract_addresses = self.db_reader.get_contribution_contract_addresses() + self.log.debug( + "Found {} contribution contract addresses".format(len(contribution_contract_addresses)) + ) + + contract_details = [ + {"address": interface.contract_address, "name": interface.abi_name} + for interface in [ + self.service_node_contribution, + self.reward_rate_pool, + self.service_node_rewards, + self.token_contract, + self.service_node_contribution_factory, + ] + ] + for address in contribution_contract_addresses: + self.service_node_contribution_multi[address] = ServiceNodeContributionInterface( + self.web3_client, + address, + ) + contract_details.append( + {"address": address, "name": ServiceNodeContributionInterface.abi_name} + ) + + self.db_writer.write_smart_contract_details_to_db(contract_details) + + self.log.perf.end("bootstrap") + + def run(self): + t1_event_loop_exception_count = 0 + t2_event_loop_exception_count = 0 + try: + while True: + try: + self.log.perf.start("loop") + network = self.rpc.get_network_info_from_network() + + network_last_fetched_height = ( + self.db_reader.get_last_fetched_network_block_height() + ) + network_last_commited_height = ( + self.db_reader.get_last_commited_network_block_height() + ) + self.log.debug( + "Last fetched height: {}, Immutable height: {}, Commited height {}, Current height: {}, next block timestamp: {}, ".format( + network_last_fetched_height, + network.immutable_block_height, + network_last_commited_height, + network.block_height, + network.pulse_target_timestamp, + ) + ) + + if ( + time.time() - self.arbitrum_details_last_updated + > config.backend.refresh_rate_seconds_arbitrum + ): + self.time_keeper.add("arb_update") + self.update_arbitrum_details() + self.time_keeper.end("arb_update") + + if network.immutable_block_height > network_last_commited_height: + self.time_keeper.add("db_migrate") + self.db_writer.write_nodes_to_main_db(network.immutable_block_height) + self.time_keeper.end("db_migrate") + + if (network.block_height - 1) > network_last_fetched_height: + self.time_keeper.add("net_update") + self.update_network_details_and_nodes(network) + self.time_keeper.end("net_update") + + self.log.perf.end("loop") + self.time_keeper.log_time_keeper() + + now = time.time() + arb_next_update = ( + self.arbitrum_details_last_updated + + config.backend.refresh_rate_seconds_arbitrum + ) + + sleep_seconds = max( + self.loop_sleep_refresh_rate_seconds, + min( + arb_next_update, + network.pulse_target_timestamp, + ) + - now, + ) + + self.log.debug( + "Sleeping for {}s ({}) (Target Event: {})".format( + format_seconds(sleep_seconds), + format_seconds(now + sleep_seconds, 0), + ( + "network_update" + if sleep_seconds == network.pulse_target_timestamp + else ( + "arb_update" + if sleep_seconds == arb_next_update - now + else "min_refresh" + ) + ), + ) + ) + + time.sleep(sleep_seconds) + + except Exception as e: + self.log.error("Error in event loop task") + self.log.exception(e) + + t1_event_loop_exception_count += 1 + + if t2_event_loop_exception_count > 3: + self.log.warning( + "Too many t2 event loop exceptions, sleeping for 5 minutes before continuing" + ) + t2_event_loop_exception_count = 0 + time.sleep(300) + elif t1_event_loop_exception_count > 10: + self.log.warning( + "Too many t1 event loop exceptions, sleeping for 30 seconds before continuing" + ) + t2_event_loop_exception_count += 1 + t1_event_loop_exception_count = 0 + time.sleep(30) + else: + self.log.error("Sleeping for 1 second before continuing") + time.sleep(1) + + except KeyboardInterrupt: + self.log.info("Application exiting...") + + def update_network_details_and_nodes( + self, + network: NetworkInfo, + ): + self.log.perf.start("update_service_node_list") + self.log.info("Update service node list task start") + parsed_nodes, contributor_stake_map, current_height = self.fetch_service_node_list() + + self.db_writer.write_nodes_to_staging_db( + current_height, parsed_nodes, contributor_stake_map + ) + + self.db_writer.write_network_info_to_db(network) + self.log.info("Scheduled task finish") + self.log.perf.end("scheduled_task") + + def fetch_service_node_list(self): + self.log.perf.start("fetch_service_node_list") + current_height = None + parsed_nodes = [] + contributions = [] + + try: + omq, oxend = omq_connection(config.backend.rpc_fetcher) + res = self.rpc.get_service_nodes(omq, oxend).get() + current_height = res.get("height") + self.log.debug("Fetched service node list at height {}".format(current_height)) + + nodes: list[ServiceNode] = res.get("service_node_states") + self.log.debug("Fetched {} service nodes".format(len(nodes))) + + # TODO: remove once contract_id is available via rpc.get_service_nodes + contract_id_map = get_service_node_rewards_contract_id_map(self.service_node_rewards) + + for node in nodes: + pubkey_bls = None + try: + # TODO: remove once contract_id is available via rpc.get_service_nodes vv + pubkey_bls = node.get("pubkey_bls") + contract_id = contract_id_map.get(pubkey_bls) + if contract_id is None: + self.log.warning( + "Contract ID not found for node with BLS pubkey: {}".format(pubkey_bls) + ) + node["contract_id"] = contract_id + # TODO: remove once contract_id is available via rpc.get_service_nodes ^^ + + # contract_id = node.get("contract _id") + # assert contract_id is not None + + # Remove some fields that might appear if field:all is passed to the rpc + if "portions_for_operator" in node: + del node["portions_for_operator"] + + # Convert some ints to strings to avoid overflowing the sqlite integer type + node["swarm_id"] = str(node["swarm_id"]) + + lokinet_version = node.get("lokinet_version", None) + node["lokinet_version"] = ( + json.dumps(lokinet_version) if lokinet_version is not None else None + ) + + pulse_votes = node.get("pulse_votes", None) + node["pulse_votes"] = ( + json.dumps(pulse_votes) if pulse_votes is not None else None + ) + + service_node_version = node.get("service_node_version", None) + node["service_node_version"] = ( + json.dumps(service_node_version) + if service_node_version is not None + else None + ) + + storage_server_version = node.get("storage_server_version", None) + node["storage_server_version"] = ( + json.dumps(storage_server_version) + if storage_server_version is not None + else None + ) + + assert node["contract_id"] is not None + + assert_all_dict_values_are_within_sqlite_integer_range(node) + + for contributor in node.get("contributors", []): + contributor_address = None + try: + amount = contributor.get("amount") + assert amount is not None + + contributor_address = contributor.get("address") + assert contributor_address is not None + + contributions.append( + { + "address": contributor_address, + "beneficiary": contributor.get("beneficiary"), + "contract_id": contract_id, + "amount": amount, + } + ) + + except Exception as e: + self.log.error( + "Error processing contributor {} for node {}".format( + contributor_address, + pubkey_bls, + ) + ) + self.log.exception(e) + continue + + parsed_nodes.append(node) + + except Exception as e: + self.log.error("Error processing node {}".format(pubkey_bls)) + self.log.exception(e) + continue + + except Exception as e: + self.log.error("Error fetching and parsing service node list") + self.log.exception(e) + finally: + self.log.perf.end("update_service_node_list") + return parsed_nodes, contributions, current_height + + def update_arbitrum_details(self): + try: + self.log.perf.start("update_arbitrum_details") + self.log.info("Update arbitrum details task start") + + last_event_block_height = self.db_reader.get_last_fetched_arbitrum_event_block_height() + end_block = self.web3_client.web3.eth.block_number - 1 + + new_contribution_contracts, new_contribution_events = get_new_contribution_contracts( + self.web3_client, + self.log, + self.service_node_contribution_factory, + last_event_block_height, + end_block, + ) + + new_contracts = [] + for contract in new_contribution_contracts: + self.service_node_contribution_multi[contract.contract_address] = contract + new_contracts.append( + {"address": contract.contract_address, "name": contract.abi_name} + ) + + self.db_writer.write_smart_contract_details_to_db(new_contracts) + + contract_details_list, contributions_list = update_contribution_contract_details( + self.web3_client, self.log, list(self.service_node_contribution_multi.values()) + ) + + self.db_writer.write_contribution_contracts_to_db( + contract_details_list, contributions_list + ) + + service_node_rewards_events = self.service_node_rewards.event_scanner.run( + last_block=last_event_block_height, + end_block=end_block, + ) + + service_node_rewards_events.extend(new_contribution_events) + + self.db_writer.write_arbitrum_events_to_db(service_node_rewards_events) + + self.arbitrum_details_last_updated = time.time() + self.log.perf.end("update_arbitrum_details") + + except Exception as e: + self.log.error("Error fetching and parsing arbitrum details") + self.log.exception(e) + + +app = App(config.backend.fetcher_name if config.backend.fetcher_name else __name__) +app.run() diff --git a/timer.py b/timer.py deleted file mode 100644 index 2d5f949..0000000 --- a/timer.py +++ /dev/null @@ -1,25 +0,0 @@ -import logging - -try: - import uwsgi # noqa: F401 -except ModuleNotFoundError: - logging.error( - """ -WARNING: Failed to load uwsgidecorators; we probably aren't running under uwsgi. -""" - ) - - class timer: - """Do-nothing stub""" - - def __init__(self, secs, **kwargs): - pass - - def __call__(self, f): - pass - - -else: - import uwsgidecorators - - timer = uwsgidecorators.timer From 5d549b5c43f5f9c489a6cd763ede45e0ccb81dec Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Wed, 20 Nov 2024 17:28:36 +1100 Subject: [PATCH 010/138] feat: read only api --- api.py | 300 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 300 insertions(+) create mode 100644 api.py diff --git a/api.py b/api.py new file mode 100644 index 0000000..ee52675 --- /dev/null +++ b/api.py @@ -0,0 +1,300 @@ +#!/usr/bin/env python3 +import flask +import time + +import eth_utils +import subprocess + +from blackd import handle + +import config +from werkzeug.routing import BaseConverter +from db.read import DBReader +from log import Log +from oxen.rpc import OxenRPC +from util.data import DataManager +from util.parse import eth_regex, Hex64Converter, hexify, EthConverter + + +class WalletInfo: + def __init__(self): + self.rewards = 0 # Atomic SENT + self.contract_rewards = 0 + self.contract_claimed = 0 + + +class App(flask.Flask): + def __init__(self, name): + super().__init__(__name__) + log = Log(name, enable_perf=config.backend.performance_logging) + log.set_level(config.backend.log_level) + git_rev = subprocess.run( + ["git", "rev-parse", "--short=9", "HEAD"], stdout=subprocess.PIPE, text=True + ) + self.git_rev = git_rev.stdout.strip() if git_rev.returncode == 0 else "(unknown)" + + # Creates a generic logger to pipe other packages logs into the main app logger + generic_logger = Log(None) + generic_logger.set_level( + config.backend.log_level_generic + if config.backend.log_level_generic is not None + else config.backend.log_level + ) + self.log = log.logger + + self.db_reader = DBReader( + db_path=config.backend.sqlite_db, + log_level=config.backend.log_level, + perf=config.backend.performance_logging, + ) + + rpc_url = config.backend.rpc_api if config.backend.rpc_api else config.backend.rpc_shared + rpc_cache = ( + config.backend.rpc_api_cache + if config.backend.rpc_api_cache + else config.backend.rpc_shared_cache + ) + + self.rpc = OxenRPC( + self.log, + rpc_url, + rpc_cache, + ) + self.loop_sleep_refresh_rate_seconds = rpc_cache if rpc_cache > 0 else 5 + + self.data = DataManager(stale_time_seconds=config.backend.stale_time_seconds) + + self.allowed_contract_names = set() + + +app = App(config.backend.api_name if config.backend.api_name else __name__) + + +def get_and_refresh_allowed_contract_names(): + allowed_contract_names = set() + for name in app.db_reader.get_smart_contract_names(): + allowed_contract_names.add(name) + app.allowed_contract_names = allowed_contract_names + return allowed_contract_names + + +app.url_map.converters["hex64"] = Hex64Converter +app.url_map.converters["eth_wallet"] = EthConverter + + +def json_response(vals): + """ + Takes a dict, adds some general info fields to it, and jsonifies it for a flask route function + return value. The dict gets passed through `hexify` first to convert any bytes values to hex. + """ + hexify(vals) + network = app.data.get("network_info", getter=app.db_reader.get_network_info) + return flask.jsonify({**vals, "network": network, "t": time.time()}) + + +@app.route("/info") +def get_network_info(): + return json_response({}) + + +def get_nodes_cached(): + return app.data.get("nodes", getter=app.db_reader.get_nodes) + + +@app.route("/nodes") +def get_nodes(): + return json_response({"nodes": app.data.get("nodes", getter=app.db_reader.get_nodes)}) + + +""" +////////////////////////////////////////////////////////////// +// // +// Stake Endpoints // +// // +////////////////////////////////////////////////////////////// +""" + + +# TODO: might make sense to investigate storing contributor and operator addresses in the db as blobs and compare with bytes +@app.route("/stakes/") +@app.route("/nodes/") +def get_stakes(eth_wal: str): + try: + if not eth_wal or not eth_utils.is_address(eth_wal): + raise ValueError("Invalid wallet address") + + nodes = get_nodes_cached() + + related_nodes = [] + for node in nodes: + if node.operator_address == eth_wal: + related_nodes.append(node) + elif node.contributors is not None: + for contributor in node.contributors: + if contributor.address == eth_wal: + related_nodes.append(node) + + return json_response({"stakes": related_nodes}) + + except ValueError as e: + app.logger.error(f"Exception: {e}") + return flask.abort(400, e) + except Exception as e: + app.logger.error(f"Exception: {e}") + return flask.abort(500, e) + + +""" +////////////////////////////////////////////////////////////// +// // +// Contract Endpoints // +// // +////////////////////////////////////////////////////////////// +""" + + +def get_cached_allowed_contract_names(): + return app.data.get( + "allowed_contract_names", + getter=get_and_refresh_allowed_contract_names, + ttl=config.backend.stale_time_seconds_contract_abis, + ) + + +@app.route("/contract/names") +def get_abi_names(): + return json_response({"names": list(get_cached_allowed_contract_names())}) + + +@app.route("/contract/abis") +def get_abis(): + return json_response( + {"abis": app.data.get("abis", getter=app.db_reader.get_smart_contract_abis)} + ) + + +@app.route("/contract/addresses") +def get_contract_addresses(): + return json_response( + {"addresses": app.data.get("addresses", getter=app.db_reader.get_smart_contract_addresses)} + ) + + +@app.route("/contract/contribution") +def get_open_contract_details(): + return json_response( + {"contracts": app.data.get("contracts", getter=app.db_reader.get_contribution_contracts)} + ) + + +@app.route("/contract/abi/") +def get_abi(contract_name: str): + if contract_name not in get_cached_allowed_contract_names(): + return flask.abort(404, f"Contract {contract_name} not found") + + return json_response( + { + "contract": app.data.get( + "abi", getter=app.db_reader.get_smart_contract_abi, getter_args=contract_name + ) + } + ) + + +@app.route("/contract/address/") +def get_contract_address(contract_name: str): + if contract_name not in get_cached_allowed_contract_names(): + return flask.abort(404, f"Contract {contract_name} not found") + + return json_response( + { + "address": app.data.get( + "address", + getter=app.db_reader.get_smart_contract_address, + getter_args=contract_name, + ) + } + ) + + +""" +////////////////////////////////////////////////////////////// +// // +// Exit Endpoints // +// // +////////////////////////////////////////////////////////////// +""" + + +def handle_get_exit_and_liquidation(ed25519_pubkey: bytes, liquidate: bool): + try: + response = app.rpc.bls_exit_liquidation_request(ed25519_pubkey, liquidate).get() + if response is None: + return flask.abort(504) # Gateway timeout + if "status" in response: + response.pop("status") + result = json_response({"result": response}) + return result + except TimeoutError: + return flask.abort(408) # Request timeout + + +@app.route("/exit/") +def get_exit(ed25519_pubkey: bytes): + return handle_get_exit_and_liquidation(ed25519_pubkey, liquidate=False) + + +@app.route("/liquidation/") +def get_liquidation(ed25519_pubkey: bytes): + return handle_get_exit_and_liquidation(ed25519_pubkey, liquidate=True) + + +@app.route("/exit_liquidation_list") +def get_exit_liquidation_list(): + # TODO: add exit list management to main.py and main database and implement db_reader.get_exitable_nodes + # return json_response( + # {"result": app.data.get("exit_liquidation_list", getter=app.db_reader.get_exitable_nodes)} + # ) + return json_response({"result": []}) + + +""" +////////////////////////////////////////////////////////////// +// // +// Rewards Endpoints // +// // +////////////////////////////////////////////////////////////// +""" + + +@app.route("/rewards/", methods=["GET", "POST"]) +def get_rewards(eth_wal: str): + if flask.request.method == "GET": + # TODO: implement db_reader.get_rewards_for_wallet and add rewards to main.py and main database + # return json_response( + # {"result": app.data.get(f"rewards-{eth_wal}", getter=app.db_reader.get_rewards_for_wallet, getter_args=eth_wal)} + # ) + return json_response({}) + + if flask.request.method == "POST": + try: + response = app.rpc.bls_rewards_request(eth_utils.to_checksum_address(eth_wal)).get() + if response is None: + return flask.abort(504) # Gateway timeout + if "status" in response: + response.pop("status") + if "address" in response: + response.pop("address") + result = json_response({"result": response}) + return result + except TimeoutError: + return flask.abort(408) # Request timeout + + return flask.abort(405) # Method not allowed + + +def bootstrap(): + get_and_refresh_allowed_contract_names() + + +bootstrap() From a653a36a4495972b6263287b98e3406e2d28d55b Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Wed, 20 Nov 2024 17:28:46 +1100 Subject: [PATCH 011/138] chore: update README.md --- README.md | 53 ++++++++++++++++++++++++++++++++++++------------ requirements.txt | 4 ++-- 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 1b00a47..5ad726b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# SENT Staking Backend +# Session Staking Backend ## Running the backend @@ -8,6 +8,7 @@ The `liboxenc-dev` and `liboxenmq-dev` packages require the development headers [Oxen Deb Repository](https://deb.oxen.io). Follow those instructions then they can be installed with `apt`. To run the backend on **Ubuntu >= 24.04**: + ```shell apt install build-essential python3-pip python3-dev pybind11-dev liboxenc-dev liboxenmq-dev python3 -m pip install eth_utils web3 PyNaCl Flask uWSGI @@ -20,19 +21,40 @@ Instructions available at: - [oxen-pyoxenc](https://github.com/oxen-io/oxen-pyoxenc) - [oxen-pyoxenmq](https://github.com/oxen-io/oxen-pyoxenmq) +### Structure + +The backend is split into three parts: + +- **Fetcher**: `fetcher.py` is the main server that handles all the fetching, processing, and main database writing. +- **Api**: `api.py` is the main API server that handles most requests. +- **Registrations API**: `api_registrations.py` is the registration API server that handles all registration requests + and registration database management. + +You can just run whichever service you want, but the intended usage is to run all three: + +- The fetcher will create and update the main database with network, node, contract, and arbitrum information. +- The API is purely read-only and is a glorified wrapper for the main database with caching. +- The registration API is used to store and retrieve registration information for nodes. + ### Instance -Before running an instance, `oxend` must be running and its address/smart contracts configured in -`config.py`. +Before running the Fetcher or API, `oxend` must be running and its address/smart contracts configured in `config.py`. -It's possible to run the service in flask directly but the timers to poll the smart contracts -requires WSGI. Both methods are detailed below: +### Running the Fetcher + +The fetcher is a pure python script that runs in a loop and fetches data from the smart contracts and the oxend RPC. You +can simply run it with `python3 fetcher.py`. + +### Running the API + +It's possible to run the API in flask directly, but you'll want to use uwsgi in production. Both methods are detailed +below: FLASK_APP=sent flask run --reload --debugger - uwsgi --http 127.0.0.1:5000 --master -p 4 -w sent --callable app + uwsgi --http 127.0.0.1:5000 --master -p 4 -w api --callable app -You may optionally append `--fs-reload sent.py` to the `uwsgi` invocation to -automatically restart the server when `sent.py` is modified. +You may optionally append `--fs-reload api.py` to the `uwsgi` invocation to +automatically restart the server when `api.py` is modified. After the server is running, visit `127.0.0.1:5000/info` to verify that the server is up and responding correctly with a payload like the following: @@ -53,6 +75,10 @@ responding correctly with a payload like the following: } ``` +### Running the Registrations API + +**Follow the same instructions as the API above. Replacing `api` with `registrations` in the commands.** + ## Setting up an oxend instance The default configuration for mainnet.py, testnet.py, devnet.py look for mainnet.sock, testnet.sock, @@ -63,7 +89,7 @@ There are a few ways to make this work. ### Symlinks -You can add symlinks here to existing oxend sockets. If oxend and the backend code are running as +You can add symlinks here to existing oxend sockets. If oxend and the backend code are running as the same user then you can simply create a symlink: ln -s /var/lib/oxend/oxend.sock mainnet.sock @@ -75,7 +101,7 @@ you can make it work, but will need an extra step to configure socket permission and then do one of: -- Set the active group of the running sent staking backend to the `_loki` user. Whether this is +- Set the active group of the running sent staking backend to the `_loki` user. Whether this is easy or not depends on how the backend service is running. - Add the `_loki` group to the supplemental groups of the user that will be running this backend. @@ -89,14 +115,14 @@ and then do one of: Do *NOT*: -- Run any production service as root (including under sudo). Don't be tempted just because "it +- Run any production service as root (including under sudo). Don't be tempted just because "it works" under sudo: by running things under root/sudo you compromise the security of your entire - system as a solution to properly setting up permissions. Please don't do this, ever. + system as a solution to properly setting up permissions. Please don't do this, ever. ### Make oxend create the socket The `lmq-public=ipc:///path/to/sent-staking-backend/oxend/mainnet.sock` can be added to oxen.conf -(or run with `--lmq-public=...`) to have it listen on that socket. Note that when oxend and +(or run with `--lmq-public=...`) to have it listen on that socket. Note that when oxend and sent-staking-backend are running as separate users, this has the same permission issues as the Symlinks approach (see above for solutions). @@ -106,3 +132,4 @@ You can configure oxend with `lmq-public=tcp://127.0.0.1:6789` (choose whatever place of `6789`) in the oxen.conf config file [alternatively: run oxend with `--lmq-public=tcp://127.0.0.1:6789`] and then add/uncomment the `mainnet_rpc=...` (or `testnet_rpc=` or `devnet_rpc=`) line in config.py. + diff --git a/requirements.txt b/requirements.txt index 6517afa..330c771 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,6 @@ oxenc==1.0.4 PyNaCl==1.5.0 eth-utils==5.0.0 Werkzeug==3.0.4 -uWSGI==2.0.26 web3==7.2.0 -eth-typing==5.0.0 \ No newline at end of file +eth-typing==5.0.0 +eth_abi==5.1.0 \ No newline at end of file From b552013bb4879951766bc209e30c41541923da47 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Thu, 21 Nov 2024 14:11:45 +1100 Subject: [PATCH 012/138] fix: config rpc validation --- config_validate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config_validate.py b/config_validate.py index 3fb7944..31f3083 100644 --- a/config_validate.py +++ b/config_validate.py @@ -36,7 +36,8 @@ def validate_config(conf: config): """ assert is_not_empty_string(conf.backend.sqlite_db), "sqlite_db is not set in config.py" - assert is_not_empty_string(conf.backend.rpc_fetcher), "rpc is not set in config.py" + rpc_url = conf.backend.rpc_fetcher if conf.backend.rpc_fetcher else conf.backend.rpc + assert is_not_empty_string(rpc_url), "rpc url is not set in config.py requires rpc_fetcher or rpc" assert is_not_empty_string( conf.backend.oxen_wallet_regex ), "oxen_wallet_regex is not set in config.py" From 65c01d642d6d1e0f02c675b88916e2caf215dbff Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Thu, 21 Nov 2024 14:13:04 +1100 Subject: [PATCH 013/138] fix: omq rpc test --- config_validate.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/config_validate.py b/config_validate.py index 31f3083..1aa2418 100644 --- a/config_validate.py +++ b/config_validate.py @@ -68,9 +68,8 @@ def validate_config(conf: config): """ Oxen RPC validations """ - omq, oxend = omq_connection(conf.backend.rpc_fetcher) - rpc = OxenRPC(log, conf.backend.rpc_fetcher, conf.backend.rpc_fetcher_cache) - res = rpc.get_info(omq, oxend).get() + rpc = OxenRPC(log, rpc_url, 0) + res = rpc.get_info().get() log.debug("Config validation rpc response status: {}".format(res.get("status"))) assert ( res is not None and res.get("status") == "OK" From 83c2f9272e331cc7e19e1885b5753f6e09846c9c Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Thu, 21 Nov 2024 14:17:05 +1100 Subject: [PATCH 014/138] fix: rpc shared config var --- config_validate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config_validate.py b/config_validate.py index 1aa2418..90302e6 100644 --- a/config_validate.py +++ b/config_validate.py @@ -36,7 +36,7 @@ def validate_config(conf: config): """ assert is_not_empty_string(conf.backend.sqlite_db), "sqlite_db is not set in config.py" - rpc_url = conf.backend.rpc_fetcher if conf.backend.rpc_fetcher else conf.backend.rpc + rpc_url = conf.backend.rpc_fetcher if conf.backend.rpc_fetcher else conf.backend.rpc_shared assert is_not_empty_string(rpc_url), "rpc url is not set in config.py requires rpc_fetcher or rpc" assert is_not_empty_string( conf.backend.oxen_wallet_regex From 1cb76302ba534376bd38539bbdd9ca92be04af2e Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Thu, 21 Nov 2024 14:22:20 +1100 Subject: [PATCH 015/138] fix: perf logger and rpc arg call --- log/time_keeper.py | 2 +- oxen/rpc.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/log/time_keeper.py b/log/time_keeper.py index 9f50c15..48775f0 100644 --- a/log/time_keeper.py +++ b/log/time_keeper.py @@ -21,7 +21,7 @@ def __init__(self, logger: logging, perf=False, max_events=10_000): self.exec_cpu_durations = {} if perf: - self.perf = PerformanceLogger(enabled=True) + self.perf = PerformanceLogger(logger.perf, enabled=True) def add(self, name: str): self.exec_timestamps.setdefault(name, []).append(time.time()) diff --git a/oxen/rpc.py b/oxen/rpc.py index 4a5669a..012e148 100644 --- a/oxen/rpc.py +++ b/oxen/rpc.py @@ -173,8 +173,7 @@ def get_service_nodes(self) -> FutureJSON: ) def get_network_info_from_network(self): - omq, oxend = omq_connection(self.rpc_url) - info = self.get_info(omq, oxend).get() + info = self.get_info().get() self.log.silly("get_network_info_from_network info: {}".format(info)) return NetworkInfo( From cfd8aa67c2b99b1d3e3f3f51444c9812f1398f48 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Thu, 21 Nov 2024 14:27:41 +1100 Subject: [PATCH 016/138] fix: add self logger as non if perf is off --- log/perf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/log/perf.py b/log/perf.py index ab3dc78..351b2f0 100644 --- a/log/perf.py +++ b/log/perf.py @@ -14,6 +14,7 @@ def __init__(self, logger: info = None, enabled=True): self.check_for_orphans_interval = 3600 # 1 hour self.last_orphan_prune = 0 else: + self.logger = None self.start = self._noop self.end = self._noop From e0953810ca5e1dfd4f18e7168fb5dc3f148a1075 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Thu, 21 Nov 2024 14:55:52 +1100 Subject: [PATCH 017/138] fix: performance logger --- fetcher.py | 3 +-- log/__init__.py | 4 ++-- log/perf.py | 8 ++++---- log/time_keeper.py | 2 +- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/fetcher.py b/fetcher.py index 6a0d64c..ef52882 100644 --- a/fetcher.py +++ b/fetcher.py @@ -291,8 +291,7 @@ def fetch_service_node_list(self): contributions = [] try: - omq, oxend = omq_connection(config.backend.rpc_fetcher) - res = self.rpc.get_service_nodes(omq, oxend).get() + res = self.rpc.get_service_nodes().get() current_height = res.get("height") self.log.debug("Fetched service node list at height {}".format(current_height)) diff --git a/log/__init__.py b/log/__init__.py index 9554185..dd13821 100644 --- a/log/__init__.py +++ b/log/__init__.py @@ -3,7 +3,7 @@ from log.perf import PerformanceLogger from log.util import add_logging_level -CUSTOM_LOG_LEVELS = {"SILLY": 1, "PERF": 69} +CUSTOM_LOG_LEVELS = {"SILLY": 1, "PERFORMANCE": 69} for label, lvl in CUSTOM_LOG_LEVELS.items(): add_logging_level(label, lvl) @@ -48,7 +48,7 @@ def __init__(self, name, initial_level=CUSTOM_LOG_LEVELS["SILLY"], enable_perf=F ch.setFormatter(CustomFormatter()) self.logger.addHandler(ch) - self.logger.perf = PerformanceLogger(self.logger.perf, enable_perf) + self.logger.perf = PerformanceLogger(self.logger, enable_perf) def set_level(self, level: str) -> None: named_level = logging.getLevelName(level) diff --git a/log/perf.py b/log/perf.py index 351b2f0..3b802b2 100644 --- a/log/perf.py +++ b/log/perf.py @@ -1,9 +1,9 @@ -from logging import info +import logging import time class PerformanceLogger: - def __init__(self, logger: info = None, enabled=True): + def __init__(self, logger: logging = None, enabled=True): if enabled: self.logger = logger self.start = self._start_enabled @@ -43,11 +43,11 @@ def _log_end(self, label, elapsed_ms, elapsed_cpu_ms): return if elapsed_ms is not None and elapsed_cpu_ms is not None: - self.logger( + self.logger.performance( f"Elapsed time for '{label}': {elapsed_ms:.6f} ms ({elapsed_cpu_ms:.6f} cpu ms)" ) else: - self.logger(f"No start time recorded for label '{label}'") + self.logger.performance(f"No start time recorded for label '{label}'") def _cleanup_orphans(self): now = time.time() diff --git a/log/time_keeper.py b/log/time_keeper.py index 48775f0..859c9cd 100644 --- a/log/time_keeper.py +++ b/log/time_keeper.py @@ -21,7 +21,7 @@ def __init__(self, logger: logging, perf=False, max_events=10_000): self.exec_cpu_durations = {} if perf: - self.perf = PerformanceLogger(logger.perf, enabled=True) + self.perf = PerformanceLogger(logger, enabled=True) def add(self, name: str): self.exec_timestamps.setdefault(name, []).append(time.time()) From 4317c288f44a577d6843164db52565f6c2572a4e Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Wed, 27 Nov 2024 09:40:42 +1100 Subject: [PATCH 018/138] feat: add median operator fee to network info api endpoint --- api.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/api.py b/api.py index ee52675..5351954 100644 --- a/api.py +++ b/api.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 +import dataclasses +import statistics import flask import time - import eth_utils import subprocess @@ -82,13 +83,28 @@ def get_and_refresh_allowed_contract_names(): app.url_map.converters["eth_wallet"] = EthConverter +def get_median_operator_fee(): + # remove nodes that only have a single contributor + nodes = [n for n in get_nodes_cached() if len(n.contributors) > 1] + return statistics.median([n.operator_fee for n in nodes]) + + +def get_network_info_uncached(): + network_info = app.db_reader.get_network_info() + if network_info is None: + return None + network_info = dataclasses.asdict(network_info) + network_info["median_operator_fee"] = get_median_operator_fee() + return network_info + + def json_response(vals): """ Takes a dict, adds some general info fields to it, and jsonifies it for a flask route function return value. The dict gets passed through `hexify` first to convert any bytes values to hex. """ hexify(vals) - network = app.data.get("network_info", getter=app.db_reader.get_network_info) + network = app.data.get("network_info", getter=get_network_info_uncached) return flask.jsonify({**vals, "network": network, "t": time.time()}) @@ -103,7 +119,7 @@ def get_nodes_cached(): @app.route("/nodes") def get_nodes(): - return json_response({"nodes": app.data.get("nodes", getter=app.db_reader.get_nodes)}) + return json_response({"nodes": get_nodes_cached()}) """ From 906328c7ddabad8a4e6e181f2742135fe34aa69a Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Thu, 12 Dec 2024 09:25:23 +1100 Subject: [PATCH 019/138] feat: add api route to get stakes by sn pubkey --- api.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/api.py b/api.py index 5351954..f946440 100644 --- a/api.py +++ b/api.py @@ -134,7 +134,7 @@ def get_nodes(): # TODO: might make sense to investigate storing contributor and operator addresses in the db as blobs and compare with bytes @app.route("/stakes/") @app.route("/nodes/") -def get_stakes(eth_wal: str): +def get_stakes_for_eth_address(eth_wal: str): try: if not eth_wal or not eth_utils.is_address(eth_wal): raise ValueError("Invalid wallet address") @@ -160,6 +160,19 @@ def get_stakes(eth_wal: str): return flask.abort(500, e) +@app.route("/stakes/") +@app.route("/nodes/") +def get_stakes_for_sn_pubkey(sn_pubkey: bytes): + try: + nodes = get_nodes_cached() + related_nodes = [node for node in nodes if node.pubkey_ed25519 == sn_pubkey] + return json_response({"stakes": related_nodes}) + + except Exception as e: + app.logger.error(f"Exception: {e}") + return flask.abort(500, e) + + """ ////////////////////////////////////////////////////////////// // // From aaa27e758d4bb7f67c6b9ec79d47e04eed7bda6e Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 20 Dec 2024 16:19:05 +1100 Subject: [PATCH 020/138] feat: add arbitrum network info and event endpoints --- api.py | 30 +++++++++++ db/dataclasses.py | 17 +++++++ db/read.py | 53 +++++++++++++++++++- db/write.py | 48 +++++++++++++++++- fetcher.py | 9 +++- web3client/contracts/service_node_rewards.py | 1 + 6 files changed, 154 insertions(+), 4 deletions(-) diff --git a/api.py b/api.py index f946440..c109d88 100644 --- a/api.py +++ b/api.py @@ -208,6 +208,11 @@ def get_contract_addresses(): {"addresses": app.data.get("addresses", getter=app.db_reader.get_smart_contract_addresses)} ) +@app.route("/contract/addresses/core") +def get_contract_addresses_core(): + return json_response( + {"addresses": app.data.get("addresses_core", getter=app.db_reader.get_smart_contract_addresses_core)} + ) @app.route("/contract/contribution") def get_open_contract_details(): @@ -246,6 +251,31 @@ def get_contract_address(contract_name: str): ) + + +""" +////////////////////////////////////////////////////////////// +// // +// Event Endpoints // +// // +////////////////////////////////////////////////////////////// +""" + +def get_events_handler(count_limit=500, skip=0): + limit = min(count_limit, 500) + events, limit, skip, total = app.data.get("events-{}-{}".format(count_limit,skip), getter=app.db_reader.get_arbitrum_events, getter_args=[limit, skip], ttl=10) + pagination = {"limit": limit, "skip": skip, "total": total} + + return {"events": events, "pagination": pagination} + +@app.route("/events//") +def get_events(count: int, skip: int): + return json_response(get_events_handler(count, skip)) + +@app.route("/arbitrum-info") +def get_arbitrum_info(): + return json_response({"info": app.data.get("arbitrum-info", getter=app.db_reader.get_arbitrum_info)}) + """ ////////////////////////////////////////////////////////////// // // diff --git a/db/dataclasses.py b/db/dataclasses.py index 48421f2..9836592 100644 --- a/db/dataclasses.py +++ b/db/dataclasses.py @@ -128,3 +128,20 @@ class SmartContractABI: def __post_init__(self): self.abi = json.loads(self.abi) + +@dataclass +class ArbitrumEvent: + block: int + tx: str + name: str + args: str + + def __post_init__(self): + self.args = json.loads(self.args) + +@dataclass +class ArbitrumInfo: + block: int + timestamp: float + balance_reward_rate_pool: int + balance_service_node_rewards: int diff --git a/db/read.py b/db/read.py index 179cfa1..692a1a0 100644 --- a/db/read.py +++ b/db/read.py @@ -1,7 +1,8 @@ import sqlite3 from contextlib import closing -from db.dataclasses import DBNode, DBContributionMain, DBNetworkInfo, DBContributionContract, DBContributionContractContribution, SmartContractABI +from db.dataclasses import DBNode, DBContributionMain, DBNetworkInfo, DBContributionContract, \ + DBContributionContractContribution, SmartContractABI, ArbitrumEvent, ArbitrumInfo from log import Log @@ -200,6 +201,22 @@ def get_smart_contract_addresses(self): self.log.perf.end("get_smart_contract_addresses") return addresses + def get_smart_contract_addresses_core(self): + self.log.perf.start("get_smart_contract_addresses_core") + with closing(sqlite3.connect(self.db_path)) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute( + """ + SELECT address, name FROM smart_contracts WHERE name IN ('ServiceNodeRewards', 'ServiceNodeContributionFactory', 'ServiceNodeRewards') + """ + ) + addresses = [ + {"address": address, "name": name} for address, name in cursor.fetchall() + ] + self.log.debug("Smart contract addresses: {}".format(len(addresses))) + self.log.perf.end("get_smart_contract_addresses_core") + return addresses + def get_smart_contract_address(self, name: str): self.log.perf.start("get_smart_contract_address") with closing(sqlite3.connect(self.db_path)) as connection: @@ -214,3 +231,37 @@ def get_smart_contract_address(self, name: str): self.log.debug("Smart contract address: {}".format(address)) self.log.perf.end("get_smart_contract_address") return address[0] + + def get_arbitrum_events(self, args=None): + if args is None: + args = [1000, 0] + self.log.perf.start("get_arbitrum_events") + with closing(sqlite3.connect(self.db_path)) as connection: + with closing(connection.cursor()) as cursor: + limit = args[0] + skip = args[1] + cursor.execute( + """ + SELECT * FROM arbitrum_events ORDER BY block DESC LIMIT ? OFFSET ? + """, + (limit, skip), + ) + events = [ArbitrumEvent(*event) for event in cursor.fetchall()] + self.log.debug("Arbitrum events: {}".format(len(events))) + self.log.perf.end("get_arbitrum_events") + + cursor.execute("SELECT COUNT(*) FROM arbitrum_events") + total = cursor.fetchone()[0] + + return events, limit, skip, total + + def get_arbitrum_info(self): + self.log.perf.start("get_arbitrum_info") + with closing(sqlite3.connect(self.db_path)) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute("SELECT * FROM arbitrum_info ORDER BY block DESC LIMIT 1") + info = ArbitrumInfo(*cursor.fetchone()) + + self.log.debug("Arbitrum info: {}".format(info)) + self.log.perf.end("get_arbitrum_info") + return info \ No newline at end of file diff --git a/db/write.py b/db/write.py index 2587a97..22c4fc6 100644 --- a/db/write.py +++ b/db/write.py @@ -385,7 +385,7 @@ def write_arbitrum_events_to_db(self, events: list[ProcessedEvent]): ( ( event.block, - event.tx, + "0x" + event.tx, event.name, Web3.to_json(dict(event.args)), ) @@ -452,6 +452,28 @@ def write_contribution_contracts_to_db( inserted_contract_rows ) ) + self.log.perf.start("write_contribution_contracts_to_db -> delete contributions") + + # The contributors for a contact need to be deleted before the contract can be inserted again to account + # for contract resets, or contributors leaving the contract. We could read from the db and only delete + # the missing ones but this should be more performant. + # TODO: investigate a better solution + cursor.executemany( + """DELETE FROM contribution_contracts_contributions WHERE contract_address = ?""", + (( + contract.address, + ) + for contract in contracts) + ) + + deleted_contributions_rows = cursor.rowcount + + self.log.perf.end("write_contribution_contracts_to_db -> delete contributions") + self.log.debug( + "Deleted {} rows from contribution_contracts_contributions".format( + deleted_contributions_rows + ) + ) self.log.debug( "Inserting {} contract contributions".format(len(contributions_list)) ) @@ -461,7 +483,7 @@ def write_contribution_contracts_to_db( cursor.executemany( """ - INSERT OR REPLACE INTO contribution_contracts_contributions ( + INSERT INTO contribution_contracts_contributions ( address, amount, beneficiary_address, @@ -572,3 +594,25 @@ def write_smart_contract_details_to_db( connection.commit() self.log.perf.end("write_smart_contract_details_to_db") + + def write_arbitrum_info_to_db(self, current_block, service_node_rewards_balance, reward_rate_pool_balance): + self.log.perf.start("write_arbitrum_info_to_db") + with closing(sqlite3.connect(self.db_path)) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + self.log.debug( + "Inserting arbitrum info: current block {}, service node rewards balance {}, reward rate pool balance {}".format( + current_block, service_node_rewards_balance, reward_rate_pool_balance)) + self.log.perf.start("write_arbitrum_info_to_db -> insert info") + + cursor.execute("INSERT OR REPLACE INTO arbitrum_info (block, balance_service_node_rewards, balance_reward_rate_pool) VALUES (?, ?, ?)", (current_block, service_node_rewards_balance, reward_rate_pool_balance)) + + inserted_info_rows = cursor.rowcount + + self.log.perf.end("write_arbitrum_info_to_db -> insert info") + self.log.debug( + "Inserted {} rows into arbitrum_info".format(inserted_info_rows) + ) + + connection.commit() + self.log.perf.end("write_arbitrum_info_to_db") diff --git a/fetcher.py b/fetcher.py index ef52882..7b92b9c 100644 --- a/fetcher.py +++ b/fetcher.py @@ -400,7 +400,13 @@ def update_arbitrum_details(self): self.log.info("Update arbitrum details task start") last_event_block_height = self.db_reader.get_last_fetched_arbitrum_event_block_height() - end_block = self.web3_client.web3.eth.block_number - 1 + current_block = self.web3_client.web3.eth.block_number + end_block = current_block - 1 + + service_node_rewards_balance = self.token_contract.balance_of(self.service_node_rewards.contract_address) + reward_rate_pool_balance = self.token_contract.balance_of(self.reward_rate_pool.contract_address) + self.log.debug("Arbitrum info: service node rewards balance {}, reward rate pool balance {}".format(service_node_rewards_balance, reward_rate_pool_balance)) + self.db_writer.write_arbitrum_info_to_db(current_block, service_node_rewards_balance, reward_rate_pool_balance) new_contribution_contracts, new_contribution_events = get_new_contribution_contracts( self.web3_client, @@ -436,6 +442,7 @@ def update_arbitrum_details(self): self.db_writer.write_arbitrum_events_to_db(service_node_rewards_events) + self.arbitrum_details_last_updated = time.time() self.log.perf.end("update_arbitrum_details") diff --git a/web3client/contracts/service_node_rewards.py b/web3client/contracts/service_node_rewards.py index 60dd8b8..2e9e445 100644 --- a/web3client/contracts/service_node_rewards.py +++ b/web3client/contracts/service_node_rewards.py @@ -27,6 +27,7 @@ def __init__(self, web3_client: Web3Client, contract_address: str, scanner_safet self.contract.events.ServiceNodeExitRequest, self.contract.events.ServiceNodeExit, self.contract.events.ServiceNodeLiquidated, + self.contract.events.RewardsClaimed, ], filters={"address": self.contract_address}, # How many maximum blocks at the time we request from JSON-RPC From 92e0641511fcdf20e5f99d8353b8211668b7963a Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Tue, 24 Dec 2024 09:29:47 +1100 Subject: [PATCH 021/138] fix: change arbiturm provider url to list of urls for later adding of backup support --- config_defaults.py | 14 ++++++++------ config_validate.py | 29 +++++++++++++++++------------ fetcher.py | 2 +- web3client/client.py | 13 +++++-------- 4 files changed, 31 insertions(+), 27 deletions(-) diff --git a/config_defaults.py b/config_defaults.py index 0a95d1c..0a2e595 100644 --- a/config_defaults.py +++ b/config_defaults.py @@ -25,7 +25,7 @@ class Backend: oxen_wallet_regex: str = "" sqlite_db: str = "sent-backend.db" sqlite_schema: str = "db/schema.sql" - rpc_shared: str = "" + rpc_shared: list[str] = "" rpc_shared_cache: int = 2 """ @@ -35,9 +35,11 @@ class Backend: rpc_api: str = "" rpc_api_cache: int = 2 """ - REGISTRATION API CONFIG + REGISTRATION CONFIG """ registration_api_name: str = "registration_api" + # NOTE: This can be the same DB as the main API, but you must manually run the registrations/schema.sql script in + # the main db so it can be populated with the required tables. registration_sqlite_db: str = "sent-backend-registrations.db" registration_sqlite_schema: str = "registration/schema.sql" @@ -63,7 +65,7 @@ class Backend: thread_pool_max_workers: int = 50 web3_caller_address: str | None = None web3_private_key: str | None = None - web3_provider_url: str = "http://localhost:8545" # Default hardhat private chain node address) + web3_provider_urls: list[str] = ["http://localhost:8545"] # Default hardhat private chain node address) # Session mainnet contracts @@ -87,18 +89,18 @@ class Backend: devnet_backend.oxen_wallet_regex = f"dV[{B58_ALPHABET}]{{95}}" devnet_backend.rpc_shared = "ipc://oxend/devnet.sock" devnet_backend.sqlite_db = "ssb-devnet.db" -devnet_backend.web3_provider_url = "https://sepolia-rollup.arbitrum.io/rpc" +devnet_backend.web3_provider_urls = ["https://sepolia-rollup.arbitrum.io/rpc"] # Session stagenet.v3 contracts stagenet_backend = Backend() stagenet_backend.addr_reward_rate_pool = "0x38cD8D3F93d591C18cf26B3Be4CB2c872aC37953" -stagenet_backend.addr_sn_contrib = "0x70c1f36C9cEBCa51B9344121D284D85BE36CD6bB" +stagenet_backend.addr_sent = "0x70c1f36C9cEBCa51B9344121D284D85BE36CD6bB" stagenet_backend.addr_sn_contrib_factory = "0x66d0D4f71267b3150DafF7bD486AC5E097E7E4C6" stagenet_backend.addr_sn_rewards = "0x4abfFB7f922767f22c7aa6524823d93FDDaB54b1" stagenet_backend.oxen_wallet_regex = f"ST[{B58_ALPHABET}]{{95}}" stagenet_backend.rpc_shared = "tcp://localhost:6786" stagenet_backend.sqlite_db = "ssb-stagenet.db" -stagenet_backend.web3_provider_url = "http://10.24.0.1/arb_sepolia" +stagenet_backend.web3_provider_urls = ["http://10.24.0.2/arb_sepolia"] # Assign the active backend to be used in the sent-staking-backend backend = stagenet_backend diff --git a/config_validate.py b/config_validate.py index 90302e6..c9b3123 100644 --- a/config_validate.py +++ b/config_validate.py @@ -5,6 +5,7 @@ from oxen.omq import omq_connection from oxen.rpc import OxenRPC from util import is_not_empty_string, valid_address_assertion +from web3client.client import Web3Client def validate_config(conf: config): @@ -48,22 +49,26 @@ def validate_config(conf: config): valid_address_assertion(conf.backend.addr_sn_rewards, "addr_sn_rewards") valid_address_assertion(conf.backend.addr_reward_rate_pool, "addr_reward_rate_pool") - assert is_not_empty_string( - conf.backend.web3_provider_url - ), "web3_provider_url is not set in config.py" + assert conf.backend.web3_provider_urls is not None and len( + conf.backend.web3_provider_urls + ) > 0, "web3_provider_urls is not set in config.py" + + for web3_provider_url in conf.backend.web3_provider_urls: + assert is_not_empty_string(web3_provider_url), "web3_provider_urls is not set properly in config.py" """ Web3 client validations """ - # web3_client = Web3Client( - # conf.backend.web3_provider_url, - # conf.backend.web3_caller_address, - # conf.backend.web3_private_key, - # log, - # ) - # block_number = web3_client.web3.eth.block_number - # log.debug("Config validation block number: {}".format(block_number)) - # assert block_number is not None, "Failed to get block number from web3 provider" + web3_client = Web3Client( + conf.backend.web3_provider_urls, + conf.backend.web3_caller_address, + conf.backend.web3_private_key, + log, + ) + + block_number = web3_client.web3.eth.block_number + log.debug("Config validation block number: {}".format(block_number)) + assert block_number is not None, "Failed to get block number from web3 provider" """ Oxen RPC validations diff --git a/fetcher.py b/fetcher.py index 7b92b9c..42ef153 100644 --- a/fetcher.py +++ b/fetcher.py @@ -94,7 +94,7 @@ def __init__(self, name): self.arbitrum_details_last_updated = 0 self.web3_client = Web3Client( - provider_url=config.backend.web3_provider_url, + provider_urls=config.backend.web3_provider_urls, caller_address=config.backend.web3_caller_address, private_key=config.backend.web3_private_key, logger=self.log, diff --git a/web3client/client.py b/web3client/client.py index 248027d..ac2e1ec 100644 --- a/web3client/client.py +++ b/web3client/client.py @@ -1,7 +1,7 @@ import logging import eth_utils -from web3 import Web3 +from web3 import Web3, HTTPProvider from web3.contract.contract import ContractFunction from web3client.abi_manager import ABIManager @@ -9,7 +9,7 @@ class Web3Client: def __init__( self, - provider_url: str, + provider_urls: list[str], caller_address: str | None, private_key: str | None, logger: logging, @@ -18,18 +18,15 @@ def __init__( """ Initialize the web3 client. - :param provider_url: URL of the Ethereum node to connect to. + :param provider_urls: List of URLs for Ethereum nodes to connect to. :param caller_address: Address of the caller. :param private_key: Private key of the caller. """ - self.web3 = Web3(Web3.HTTPProvider(provider_url)) + self.web3 = Web3(HTTPProvider(endpoint_uri=provider_urls[0])) + self.provider_url = provider_urls[0] self.abi_manager = abi_manager self.chain_id = self.web3.eth.chain_id - if provider_url is None: - raise ValueError("Provider URL is None") - self.provider_url = provider_url - self.private_key = private_key if private_key is None: logger.warning("private_key is None, contract writes will be disabled") From cd10d496200569a1bbe0092856731626d12b6c26 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Tue, 24 Dec 2024 09:33:29 +1100 Subject: [PATCH 022/138] feat: add rewards fetching and endpoints to fetcher and api --- api.py | 34 +++++++++++++++------------------- db/dataclasses.py | 5 +++++ db/read.py | 15 ++++++++++++++- db/write.py | 33 +++++++++++++++++++++++++++++++++ fetcher.py | 36 ++++++++++++++++++++++++++++++++++++ 5 files changed, 103 insertions(+), 20 deletions(-) diff --git a/api.py b/api.py index c109d88..920eacc 100644 --- a/api.py +++ b/api.py @@ -14,15 +14,7 @@ from log import Log from oxen.rpc import OxenRPC from util.data import DataManager -from util.parse import eth_regex, Hex64Converter, hexify, EthConverter - - -class WalletInfo: - def __init__(self): - self.rewards = 0 # Atomic SENT - self.contract_rewards = 0 - self.contract_claimed = 0 - +from util.parse import Hex64Converter, hexify, EthConverter class App(flask.Flask): def __init__(self, name): @@ -325,27 +317,31 @@ def get_exit_liquidation_list(): ////////////////////////////////////////////////////////////// """ +def get_rewards_signature_uncached(eth_wal: str): + if not eth_wal or not eth_utils.is_address(eth_wal): + raise ValueError("Invalid wallet address") + response = app.rpc.bls_rewards_request(eth_utils.to_checksum_address(eth_wal)).get() + if response is None: + raise TimeoutError("Failed to get rewards signature") + return response @app.route("/rewards/", methods=["GET", "POST"]) def get_rewards(eth_wal: str): if flask.request.method == "GET": - # TODO: implement db_reader.get_rewards_for_wallet and add rewards to main.py and main database - # return json_response( - # {"result": app.data.get(f"rewards-{eth_wal}", getter=app.db_reader.get_rewards_for_wallet, getter_args=eth_wal)} - # ) - return json_response({}) + # We cache all rewards info for all wallets so we don't need to multiple reads in a short period of time + rewards_info = app.data.get(f"rewards_info", getter=app.db_reader.get_rewards_info) + return json_response({"rewards": rewards_info.get(eth_wal, 0)}) if flask.request.method == "POST": try: - response = app.rpc.bls_rewards_request(eth_utils.to_checksum_address(eth_wal)).get() - if response is None: - return flask.abort(504) # Gateway timeout + response = app.data.get(f"rewards-sig-{eth_wal}", getter=get_rewards_signature_uncached, getter_args=eth_wal) if "status" in response: response.pop("status") if "address" in response: response.pop("address") - result = json_response({"result": response}) - return result + return json_response({"rewards": response}) + except ValueError as e: + return flask.abort(400, str(e)) except TimeoutError: return flask.abort(408) # Request timeout diff --git a/db/dataclasses.py b/db/dataclasses.py index 9836592..649d7c2 100644 --- a/db/dataclasses.py +++ b/db/dataclasses.py @@ -145,3 +145,8 @@ class ArbitrumInfo: timestamp: float balance_reward_rate_pool: int balance_service_node_rewards: int + +@dataclass +class RewardsInfo: + address: str + rewards: int diff --git a/db/read.py b/db/read.py index 692a1a0..deaa6b9 100644 --- a/db/read.py +++ b/db/read.py @@ -2,7 +2,7 @@ from contextlib import closing from db.dataclasses import DBNode, DBContributionMain, DBNetworkInfo, DBContributionContract, \ - DBContributionContractContribution, SmartContractABI, ArbitrumEvent, ArbitrumInfo + DBContributionContractContribution, SmartContractABI, ArbitrumEvent, ArbitrumInfo, RewardsInfo from log import Log @@ -142,6 +142,19 @@ def get_nodes(self): self.log.perf.end("get_nodes") return list(parsed_nodes.values()) + def get_rewards_info(self): + self.log.perf.start("get_rewards_info") + with closing(sqlite3.connect(self.db_path)) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute("SELECT * FROM rewards_info") + rewards_info = { + address: rewards + for address, rewards in cursor.fetchall() + } + self.log.debug("Rewards info: {}".format(len(rewards_info))) + self.log.perf.end("get_rewards_info") + return rewards_info + def get_smart_contract_abis(self): self.log.perf.start("get_smart_contract_abis") with closing(sqlite3.connect(self.db_path)) as connection: diff --git a/db/write.py b/db/write.py index 22c4fc6..8b265dd 100644 --- a/db/write.py +++ b/db/write.py @@ -6,6 +6,7 @@ from web3 import Web3 from arbitrum import ContributionContractDetails +from db.dataclasses import RewardsInfo from log import Log from oxen.rpc import ServiceNode, NetworkInfo from web3client.abi_manager import ABIData @@ -362,6 +363,38 @@ def write_network_info_to_db( connection.commit() self.log.perf.end("write_network_info_to_db") + def write_rewards_info_to_db(self, rewards_info: list[RewardsInfo]): + self.log.perf.start("write_rewards_info_to_db") + with closing(sqlite3.connect(self.db_path)) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + self.log.debug("Inserting {} rewards info".format(len(rewards_info))) + self.log.perf.start("write_rewards_info_to_db -> insert rewards info") + + cursor.executemany( + """ + INSERT OR REPLACE INTO rewards_info (address, rewards) + VALUES (?, ?) + """, + ( + ( + info.address, + info.rewards, + ) + for info in rewards_info + ), + ) + + inserted_rewards_rows = cursor.rowcount + + self.log.perf.end("write_rewards_info_to_db -> insert rewards info") + self.log.debug( + "Inserted {} rows into rewards_info".format(inserted_rewards_rows) + ) + + connection.commit() + self.log.perf.end("write_rewards_info_to_db") + def write_arbitrum_events_to_db(self, events: list[ProcessedEvent]): self.log.perf.start("write_arbitrum_events_to_db") diff --git a/fetcher.py b/fetcher.py index 42ef153..1aca67b 100644 --- a/fetcher.py +++ b/fetcher.py @@ -10,6 +10,7 @@ update_contribution_contract_details, ) from config_validate import validate_config +from db.dataclasses import RewardsInfo from db.util import ( assert_all_dict_values_are_within_sqlite_integer_range, is_db_initialized, @@ -281,6 +282,10 @@ def update_network_details_and_nodes( ) self.db_writer.write_network_info_to_db(network) + + rewards_info = self.get_rewards_info() + self.db_writer.write_rewards_info_to_db(rewards_info) + self.log.info("Scheduled task finish") self.log.perf.end("scheduled_task") @@ -394,6 +399,37 @@ def fetch_service_node_list(self): self.log.perf.end("update_service_node_list") return parsed_nodes, contributions, current_height + def get_rewards_info(self): + self.log.perf.start("update_rewards_details") + self.log.debug("Update rewards details task start") + rewards_info = [] + try: + # Get the accrued rewards values for each wallet + accrued_rewards_json = self.rpc.get_accrued_rewards().get() + + assert accrued_rewards_json is not None, "Accrued rewards request failed" + assert accrued_rewards_json["status"] == "OK", "Accrued rewards request failed {}".format(accrued_rewards_json) + assert "balances" in accrued_rewards_json, "Accrued rewards request failed, 'balances' key was missing: {}".format(accrued_rewards_json) + + + # Populate (Binary ETH wallet address -> accrued_rewards) table + for address_hex, rewards in accrued_rewards_json.get("balances").items(): + # Ignore non-ethereum addresses (e.g. left oxen rewards, not relevant) + trimmed_address_hex = address_hex[2:] if address_hex.startswith("0x") else address_hex + if len(trimmed_address_hex) != 40: + self.log.warning("Invalid address {}".format(trimmed_address_hex)) + continue + + rewards_info.append(RewardsInfo(address_hex, rewards)) + + except Exception as e: + self.log.error("Error fetching and parsing rewards details") + self.log.exception(e) + finally: + self.log.perf.end("update_rewards_details") + return rewards_info + + def update_arbitrum_details(self): try: self.log.perf.start("update_arbitrum_details") From e16f1f744a757270f061a6a0c985f90c5f794425 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Tue, 24 Dec 2024 09:34:24 +1100 Subject: [PATCH 023/138] chore: move registration getter api endpoints to main api --- api.py | 77 ++++++++++++++++++++++++++++++++++++++++++++--- db/dataclasses.py | 10 ++++++ fetcher.py | 2 +- registrations.py | 59 ------------------------------------ 4 files changed, 83 insertions(+), 65 deletions(-) diff --git a/api.py b/api.py index 920eacc..d41153e 100644 --- a/api.py +++ b/api.py @@ -6,13 +6,11 @@ import eth_utils import subprocess -from blackd import handle - import config -from werkzeug.routing import BaseConverter from db.read import DBReader from log import Log from oxen.rpc import OxenRPC +from registration.read import DBReaderRegistrations from util.data import DataManager from util.parse import Hex64Converter, hexify, EthConverter @@ -40,6 +38,11 @@ def __init__(self, name): log_level=config.backend.log_level, perf=config.backend.performance_logging, ) + self.db_reader_registrations = DBReaderRegistrations( + db_path=config.backend.registration_sqlite_db, + log_level=config.backend.log_level, + perf=config.backend.performance_logging, + ) rpc_url = config.backend.rpc_api if config.backend.rpc_api else config.backend.rpc_shared rpc_cache = ( @@ -243,8 +246,6 @@ def get_contract_address(contract_name: str): ) - - """ ////////////////////////////////////////////////////////////// // // @@ -348,6 +349,72 @@ def get_rewards(eth_wal: str): return flask.abort(405) # Method not allowed +""" +////////////////////////////////////////////////////////////// +// // +// Registration Endpoints // +// // +////////////////////////////////////////////////////////////// +""" + + +@app.route("/registrations/") +def operator_registrations(operator: str): + """ + Retrieves stored registration(s) for the given 'operator'. + + This returns an array in the "registrations" field containing as many registrations as are + currently stored for the given operator wallet, sorted from most to least recently submitted. + + Fields are the same as the version of this endpoint that takes a SN pubkey. + + Returns the JSON response with the 'registrations' for the given 'operator'. + """ + + operator_bytes = bytes.fromhex(operator[2:]) + + return json_response( + { + "registrations": app.data.get( + f"op-{operator_bytes}", + getter=app.db_reader_registrations.get_registrations_for_operator, + getter_args=operator_bytes, + ) + } + ) + + +@app.route("/registrations/") +def sn_pubkey_registrations(sn_pubkey: bytes) -> flask.Response: + """ + Retrieves stored registration(s) for the given service node pubkey. + + This returns an array in the "registrations" field containing either one or two registration + info dicts: a solo registration (if known) and a multi-contributor contract registration (if + known). These are sorted by timestamp of when the registration was last received/updated. + + Fields in each dict: + - "operator": the operator address. + - "contract": the contract address, for "type": "contract" and omitted for "type": "solo". + - "pubkey_ed25519": the primary SN pubkey, in hex. + - "pubkey_bls": the SN BLS pubkey, in hex. + - "sig_ed25519": the SN pubkey signed registration signature. + - "sig_bls": the SN BLS pubkey signed registration signature. + - "timestamp": the unix timestamp when this registration was received (or last updated) + + Returns the JSON response with the 'registrations' for the given 'sn_pubkey'. + """ + result = json_response( + { + "registrations": app.data.get( + f"sn-{sn_pubkey}", + getter=app.db_reader_registrations.get_registrations_by_pubkey, + getter_args=sn_pubkey, + ) + } + ) + return result + def bootstrap(): get_and_refresh_allowed_contract_names() diff --git a/db/dataclasses.py b/db/dataclasses.py index 649d7c2..011a24a 100644 --- a/db/dataclasses.py +++ b/db/dataclasses.py @@ -150,3 +150,13 @@ class ArbitrumInfo: class RewardsInfo: address: str rewards: int + +@dataclass +class Registration: + contract: bytes + operator: bytes + pubkey_bls: bytes + pubkey_ed25519: bytes + sig_bls: bytes + sig_ed25519: bytes + timestamp: float \ No newline at end of file diff --git a/fetcher.py b/fetcher.py index 1aca67b..9bfe50c 100644 --- a/fetcher.py +++ b/fetcher.py @@ -20,7 +20,7 @@ from db.write import DBWriter from log import Log from oxen.rpc import ServiceNode, OxenRPC, NetworkInfo -from util import format_seconds +from util import format_seconds, is_not_empty_string from log.time_keeper import TimeKeeper from web3client.abi_manager import ABIManager from web3client.client import Web3Client diff --git a/registrations.py b/registrations.py index 3f8d418..13a9f74 100644 --- a/registrations.py +++ b/registrations.py @@ -98,65 +98,6 @@ def get_network_info(): ////////////////////////////////////////////////////////////// """ - -@app.route("/registrations/") -def operator_registrations(operator: str): - """ - Retrieves stored registration(s) for the given 'operator'. - - This returns an array in the "registrations" field containing as many registrations as are - currently stored for the given operator wallet, sorted from most to least recently submitted. - - Fields are the same as the version of this endpoint that takes a SN pubkey. - - Returns the JSON response with the 'registrations' for the given 'operator'. - """ - - operator_bytes = bytes.fromhex(operator[2:]) - - return json_response( - { - "registrations": app.data.get( - f"op-{operator_bytes}", - getter=app.db_reader.get_registrations_for_operator, - getter_args=operator_bytes, - ) - } - ) - - -@app.route("/registrations/") -def sn_pubkey_registrations(sn_pubkey: bytes) -> flask.Response: - """ - Retrieves stored registration(s) for the given service node pubkey. - - This returns an array in the "registrations" field containing either one or two registration - info dicts: a solo registration (if known) and a multi-contributor contract registration (if - known). These are sorted by timestamp of when the registration was last received/updated. - - Fields in each dict: - - "operator": the operator address. - - "contract": the contract address, for "type": "contract" and omitted for "type": "solo". - - "pubkey_ed25519": the primary SN pubkey, in hex. - - "pubkey_bls": the SN BLS pubkey, in hex. - - "sig_ed25519": the SN pubkey signed registration signature. - - "sig_bls": the SN BLS pubkey signed registration signature. - - "timestamp": the unix timestamp when this registration was received (or last updated) - - Returns the JSON response with the 'registrations' for the given 'sn_pubkey'. - """ - result = json_response( - { - "registrations": app.data.get( - f"sn-{sn_pubkey}", - getter=app.db_reader.get_registrations_by_pubkey, - getter_args=sn_pubkey, - ) - } - ) - return result - - @app.route("/registrations/", methods=["POST"]) @app.route("/store/", methods=["GET", "POST"]) def store_registration(sn_pubkey: bytes): From 7949251a7fdcb98aad30c0f2e8027f3bb2f9526c Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Tue, 24 Dec 2024 09:34:45 +1100 Subject: [PATCH 024/138] fix: update db schema with arbitrum info and rewards info tables --- db/schema.sql | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/db/schema.sql b/db/schema.sql index 4f7018c..26a8deb 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -139,6 +139,20 @@ CREATE TABLE network_info ( CREATE INDEX network_info_block_height_idx ON network_info(block_height DESC); +CREATE TABLE rewards_info ( + address BLOB NOT NULL PRIMARY KEY, + rewards INTEGER NOT NULL +); + +CREATE TABLE arbitrum_info ( + block INTEGER PRIMARY KEY NOT NULL, + timestamp FLOAT NOT NULL DEFAULT ((julianday('now') - 2440587.5)*86400.0), /* unix epoch */ + balance_reward_rate_pool INTEGER NOT NULL, + balance_service_node_rewards INTEGER NOT NULL +); + +CREATE INDEX arbitrum_info_block_idx ON arbitrum_info(block DESC); + CREATE TABLE arbitrum_events ( block INTEGER NOT NULL, tx TEXT NOT NULL, @@ -193,21 +207,3 @@ CREATE TABLE smart_contracts ( foreign key (name) references smart_contract_abis(name) ); - --- TODO: check if this can be better, just ported over from the old db -CREATE TABLE IF NOT EXISTS registrations ( - contract BLOB, - operator BLOB NOT NULL, - pubkey_bls BLOB NOT NULL, - pubkey_ed25519 BLOB NOT NULL, - sig_bls BLOB NOT NULL, - sig_ed25519 BLOB NOT NULL, - timestamp FLOAT NOT NULL DEFAULT ((julianday('now') - 2440587.5)*86400.0), /* unix epoch */ - - CHECK(length(pubkey_ed25519) == 32), - CHECK(length(pubkey_bls) == 64), - CHECK(length(sig_ed25519) == 64), - CHECK(length(sig_bls) == 128), - CHECK(length(operator) == 20), - CHECK(contract IS NULL OR length(contract) == 20) -) \ No newline at end of file From a51fd12e91528db74301681bbc3098a9bee805cb Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Sun, 29 Dec 2024 08:51:22 +1100 Subject: [PATCH 025/138] feat: add timestamps to events --- api.py | 7 +++ arbitrum.py | 91 +++++++++++++++++++++++++++++++++++++ db/dataclasses.py | 3 ++ db/read.py | 21 ++++++++- db/schema.sql | 8 +++- db/write.py | 6 ++- fetcher.py | 15 +++--- web3client/event_scanner.py | 13 ++++-- 8 files changed, 149 insertions(+), 15 deletions(-) diff --git a/api.py b/api.py index d41153e..e373771 100644 --- a/api.py +++ b/api.py @@ -269,6 +269,13 @@ def get_events(count: int, skip: int): def get_arbitrum_info(): return json_response({"info": app.data.get("arbitrum-info", getter=app.db_reader.get_arbitrum_info)}) +@app.route("/stake-events/") +def get_stake_events(contract_id: int): + if contract_id < 0: + return flask.abort(400, "Invalid contract ID") + + return json_response({"events": app.data.get("stake-events-{}".format(contract_id), getter=app.db_reader.get_arbitrum_events_for_stake_contrat_id, getter_args=contract_id)}) + """ ////////////////////////////////////////////////////////////// // // diff --git a/arbitrum.py b/arbitrum.py index 0b8d384..cd755ea 100644 --- a/arbitrum.py +++ b/arbitrum.py @@ -2,11 +2,13 @@ from dataclasses import dataclass import eth_utils +from web3.exceptions import BlockNotFound from web3client.client import Web3Client from web3client.contracts.service_node_contribution import ServiceNodeContributionInterface from web3client.contracts.service_node_contribution_factory import ServiceNodeContributionFactory from web3client.contracts.service_node_rewards import ServiceNodeRewardsInterface +from web3client.event_scanner import ProcessedEvent # TODO: we should be able to remove this once contract_id is always available via rpc.get_service_nodes @@ -142,3 +144,92 @@ def update_contribution_contract_details( logger.perf.end("fetch_contribution_contract_details total") return contract_details, contributions_list + + +def get_block_timestamp(web3_client:Web3Client, block_num: int): + """Get block timestamp""" + try: + block_info = web3_client.web3.eth.get_block(block_num) + except BlockNotFound: + # Block was not mined yet, + # minor chain reorganisation? + return None + return block_info.get("timestamp") + + +def estimate_block_timestamp(ref_block: int, ref_block_timestamp: int, target_block: int): + """Estimate block timestamp + Arbitrum averages 4 blocks per second + """ + seconds_diff = (target_block - ref_block) / 4 + return ref_block_timestamp + seconds_diff + + +def batch_populate_events_with_block_timestamps( + web3_client: Web3Client, + logger: logging, + events: list[ProcessedEvent], + max_requests_per_batch=1000, +): + logger.perf.start("chunk_new_event_blocks") + if len(events) == 0: + return + assert max_requests_per_batch > 0, "Expected max_requests_per_batch > 0" + + max_chunk_size = max_requests_per_batch + + blocks = list({event.block for event in events}) + + logger.debug( + "events: {}, requests_per_contract: {}, max_chunk_size: {}".format( + len(blocks), max_requests_per_batch, max_chunk_size + ) + ) + + chunks = [blocks[i: i + max_chunk_size] for i in range(0, len(blocks), max_chunk_size)] + logger.perf.end("chunk_new_event_blocks") + + logger.perf.start("fetch_new_event_block_timestamps total") + responses = [] + for chunk in chunks: + + assert len(chunk) <= max_requests_per_batch, "Expected chunk size <= {} got {}".format( + max_requests_per_batch, len(chunk) + ) + + logger.perf.start("fetch_new_event_block_timestamps of size {}".format(len(chunk))) + with web3_client.web3.batch_requests() as batch: + for block in chunk: + batch.add(web3_client.web3.eth.get_block(block)) + + res = batch.execute() + responses.extend(res) + logger.perf.end("fetch_new_event_block_timestamps of size {}".format(len(chunk))) + + assert ( + len(responses) == len(blocks) + ), "Expected {} responses, got {}".format(len(blocks), len(responses)) + + logger.debug("Fetched timestamps of blocks for {} events".format(len(blocks))) + + logger.perf.end("fetch_new_event_block_timestamps total") + + block_timestamps = { + block_info["number"]: block_info["timestamp"] + for block_info in responses + } + + for event in events: + event.timestamp = block_timestamps.get(event.block) + if event.timestamp is None: + logger.warning("No timestamp for block {}".format(event.block)) + + return + +def populate_events_with_main_arg(events: list[ProcessedEvent]): + """ + Populates the main_arg field of the event with a main argument if one is specified, otherwise the value of the first argument. + """ + for event in events: + # NOTE: This is apparently the best way to get the 0th element of a dict + event.main_arg = event.args[next(iter(event.args))] diff --git a/db/dataclasses.py b/db/dataclasses.py index 011a24a..d1d8a87 100644 --- a/db/dataclasses.py +++ b/db/dataclasses.py @@ -2,6 +2,8 @@ from dataclasses import dataclass from typing import Optional +from web3client.event_scanner import ProcessedEvent + @dataclass class DBNode: @@ -39,6 +41,7 @@ class DBNode: total_contributed: int # Not in db but added after select contributors: list | None + events: list[ProcessedEvent] | None def __post_init__(self): self.lokinet_version = json.loads(self.lokinet_version) if self.lokinet_version else None diff --git a/db/read.py b/db/read.py index deaa6b9..fe221ce 100644 --- a/db/read.py +++ b/db/read.py @@ -1,9 +1,12 @@ import sqlite3 from contextlib import closing +import eth_utils + from db.dataclasses import DBNode, DBContributionMain, DBNetworkInfo, DBContributionContract, \ DBContributionContractContribution, SmartContractABI, ArbitrumEvent, ArbitrumInfo, RewardsInfo from log import Log +from web3client.event_scanner import ProcessedEvent class DBReader: @@ -277,4 +280,20 @@ def get_arbitrum_info(self): self.log.debug("Arbitrum info: {}".format(info)) self.log.perf.end("get_arbitrum_info") - return info \ No newline at end of file + return info + + def get_arbitrum_events_for_stake_contrat_id(self, contract_id: int): + self.log.perf.start("get_events_for_stake_contrat_id") + with closing(sqlite3.connect(self.db_path)) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute( + """ + SELECT * FROM arbitrum_events WHERE main_arg = ? ORDER BY block DESC + """, + (contract_id,), + ) + events = [ArbitrumEvent(*event) for event in cursor.fetchall()] + self.log.debug("Arbitrum events: {}".format(len(events))) + self.log.perf.end("get_events_for_stake_contrat_id") + return events + diff --git a/db/schema.sql b/db/schema.sql index 26a8deb..067f3bc 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -154,14 +154,18 @@ CREATE TABLE arbitrum_info ( CREATE INDEX arbitrum_info_block_idx ON arbitrum_info(block DESC); CREATE TABLE arbitrum_events ( + args TEXT NOT NULL, block INTEGER NOT NULL, - tx TEXT NOT NULL, + main_arg TEXT, name TEXT NOT NULL, - args TEXT NOT NULL, + timestamp INTEGER NOT NULL, + tx TEXT NOT NULL, PRIMARY KEY (block, tx, name) ); CREATE INDEX arbitrum_events_block_idx ON arbitrum_events(block DESC); +CREATE INDEX arbitrum_events_block_timestamp ON arbitrum_events(timestamp DESC); +CREATE INDEX arbitrum_events_main_arg_idx ON arbitrum_events(main_arg, block DESC); CREATE TABLE contribution_contracts ( address TEXT NOT NULL, diff --git a/db/write.py b/db/write.py index 8b265dd..8997bb6 100644 --- a/db/write.py +++ b/db/write.py @@ -409,17 +409,21 @@ def write_arbitrum_events_to_db(self, events: list[ProcessedEvent]): """ INSERT OR REPLACE INTO arbitrum_events ( block, + timestamp, tx, name, + main_arg, args ) - VALUES (?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?) """, ( ( event.block, + event.timestamp, "0x" + event.tx, event.name, + event.main_arg, Web3.to_json(dict(event.args)), ) for event in events diff --git a/fetcher.py b/fetcher.py index 9bfe50c..7c91816 100644 --- a/fetcher.py +++ b/fetcher.py @@ -415,9 +415,9 @@ def get_rewards_info(self): # Populate (Binary ETH wallet address -> accrued_rewards) table for address_hex, rewards in accrued_rewards_json.get("balances").items(): # Ignore non-ethereum addresses (e.g. left oxen rewards, not relevant) - trimmed_address_hex = address_hex[2:] if address_hex.startswith("0x") else address_hex - if len(trimmed_address_hex) != 40: - self.log.warning("Invalid address {}".format(trimmed_address_hex)) + address = address_hex if address_hex.startswith("0x") else "0x" + address_hex + if len(address) != 42: + self.log.warning("Invalid address {}".format(address)) continue rewards_info.append(RewardsInfo(address_hex, rewards)) @@ -469,15 +469,16 @@ def update_arbitrum_details(self): contract_details_list, contributions_list ) - service_node_rewards_events = self.service_node_rewards.event_scanner.run( + events = self.service_node_rewards.event_scanner.run( last_block=last_event_block_height, end_block=end_block, ) - service_node_rewards_events.extend(new_contribution_events) - - self.db_writer.write_arbitrum_events_to_db(service_node_rewards_events) + events.extend(new_contribution_events) + batch_populate_events_with_block_timestamps(self.web3_client, self.log, events) + populate_events_with_main_arg(events) + self.db_writer.write_arbitrum_events_to_db(events) self.arbitrum_details_last_updated = time.time() self.log.perf.end("update_arbitrum_details") diff --git a/web3client/event_scanner.py b/web3client/event_scanner.py index ebdad39..463ff24 100644 --- a/web3client/event_scanner.py +++ b/web3client/event_scanner.py @@ -4,7 +4,7 @@ With the stateful mechanism, you can do one batch scan or incremental scans, where events are added wherever the scanner left off. """ - +import json import time import logging from dataclasses import dataclass @@ -26,10 +26,15 @@ @dataclass class ProcessedEvent: - tx: str + args: dict block: int + main_arg: str | None name: str - args: dict + timestamp: None | int + tx: str + + def __post_init__(self): + self.args = json.loads(self.args) if type(self.args) == str else self.args class EventScanner: @@ -240,7 +245,7 @@ def process_event(self, event: AttributeDict) -> ProcessedEvent: block = event.blockNumber name = event.event args = event["args"] - return ProcessedEvent(tx=tx, block=block, name=name, args=args) + return ProcessedEvent(tx=tx, block=block, name=name, args=args, timestamp=None, main_arg=None) def run( self, From b8993c1ec73aebc1e9399b3eea83353e41ac25d3 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Sun, 29 Dec 2024 08:52:45 +1100 Subject: [PATCH 026/138] feat: add node counts to network info --- db/dataclasses.py | 2 ++ db/schema.sql | 2 ++ db/write.py | 8 +++++++- fetcher.py | 11 ++++++++--- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/db/dataclasses.py b/db/dataclasses.py index d1d8a87..414fe46 100644 --- a/db/dataclasses.py +++ b/db/dataclasses.py @@ -74,6 +74,7 @@ def __post_init__(self): @dataclass class DBNetworkInfo: id: Optional[int] + active_node_count: int block_hash: str block_height: int block_timestamp: float @@ -83,6 +84,7 @@ class DBNetworkInfo: max_stakers: int min_operator_contribution: int nettype: str + node_count: int pulse_target_timestamp: int staking_requirement: int version: str diff --git a/db/schema.sql b/db/schema.sql index 067f3bc..bfa2a20 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -123,6 +123,7 @@ CREATE INDEX service_nodes_contributions_main_fetched_block_height_desc_idx ON s CREATE TABLE network_info ( id INTEGER PRIMARY KEY NOT NULL, + active_node_count INTEGER NOT NULL, block_hash TEXT NOT NULL, block_height INTEGER NOT NULL, block_timestamp FLOAT NOT NULL, @@ -132,6 +133,7 @@ CREATE TABLE network_info ( max_stakers INTEGER NOT NULL, min_operator_contribution INTEGER NOT NULL, nettype TEXT NOT NULL, + node_count INTEGER NOT NULL, pulse_target_timestamp INTEGER NOT NULL, staking_requirement INTEGER NOT NULL, version TEXT NOT NULL diff --git a/db/write.py b/db/write.py index 8997bb6..8781671 100644 --- a/db/write.py +++ b/db/write.py @@ -321,6 +321,8 @@ def write_nodes_to_main_db(self, immutable_height: int): def write_network_info_to_db( self, network: NetworkInfo, + node_count: int, + active_node_count: int, ): self.log.perf.start("write_network_info_to_db") with closing(sqlite3.connect(self.db_path)) as connection: @@ -329,6 +331,7 @@ def write_network_info_to_db( """ INSERT OR REPLACE INTO network_info ( id, + active_node_count, block_hash, block_height, block_timestamp, @@ -337,15 +340,17 @@ def write_network_info_to_db( immutable_block_height, max_stakers, min_operator_contribution, + node_count, nettype, pulse_target_timestamp, staking_requirement, version ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( 1, + active_node_count, network.block_hash, network.block_height, time.time().__floor__(), @@ -354,6 +359,7 @@ def write_network_info_to_db( network.immutable_block_height, network.max_stakers, network.min_operator_contribution, + node_count, network.nettype, network.pulse_target_timestamp, network.staking_requirement, diff --git a/fetcher.py b/fetcher.py index 7c91816..0066ef6 100644 --- a/fetcher.py +++ b/fetcher.py @@ -7,7 +7,7 @@ from arbitrum import ( get_service_node_rewards_contract_id_map, get_new_contribution_contracts, - update_contribution_contract_details, + update_contribution_contract_details, batch_populate_events_with_block_timestamps, populate_events_with_main_arg, ) from config_validate import validate_config from db.dataclasses import RewardsInfo @@ -275,13 +275,13 @@ def update_network_details_and_nodes( ): self.log.perf.start("update_service_node_list") self.log.info("Update service node list task start") - parsed_nodes, contributor_stake_map, current_height = self.fetch_service_node_list() + parsed_nodes, contributor_stake_map, current_height, node_count, active_node_count = self.fetch_service_node_list() self.db_writer.write_nodes_to_staging_db( current_height, parsed_nodes, contributor_stake_map ) - self.db_writer.write_network_info_to_db(network) + self.db_writer.write_network_info_to_db(network=network, node_count=node_count, active_node_count=active_node_count) rewards_info = self.get_rewards_info() self.db_writer.write_rewards_info_to_db(rewards_info) @@ -294,6 +294,7 @@ def fetch_service_node_list(self): current_height = None parsed_nodes = [] contributions = [] + active_node_count = 0 try: res = self.rpc.get_service_nodes().get() @@ -312,6 +313,10 @@ def fetch_service_node_list(self): # TODO: remove once contract_id is available via rpc.get_service_nodes vv pubkey_bls = node.get("pubkey_bls") contract_id = contract_id_map.get(pubkey_bls) + + if node.get("active"): + active_node_count += 1 + if contract_id is None: self.log.warning( "Contract ID not found for node with BLS pubkey: {}".format(pubkey_bls) From 6f4fee38e8cc95da4d0a3712f40dc5ea91b1a5c5 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Sun, 29 Dec 2024 08:53:43 +1100 Subject: [PATCH 027/138] feat: add node exit list parsing to fetcher --- db/dataclasses.py | 16 +++++++++++++++- db/read.py | 30 +++++++++++++++++++++++++++--- db/schema.sql | 4 ++++ db/write.py | 37 ++++++++++++++++++++++++++++++++++++- fetcher.py | 40 ++++++++++++++++++++++++++++++++++++++-- 5 files changed, 120 insertions(+), 7 deletions(-) diff --git a/db/dataclasses.py b/db/dataclasses.py index 414fe46..de1e8c6 100644 --- a/db/dataclasses.py +++ b/db/dataclasses.py @@ -8,7 +8,7 @@ @dataclass class DBNode: active: bool - contract_id: str + contract_id: int decommission_count: int earned_downtime_blocks: int fetched_block_height: int @@ -39,6 +39,12 @@ class DBNode: swarm: str swarm_id: str total_contributed: int + + # Not in staging db, optional in main db + deregistration_height: int | None + exit_type: str | None + liquidation_height: int | None + # Not in db but added after select contributors: list | None events: list[ProcessedEvent] | None @@ -54,6 +60,14 @@ def __post_init__(self): self.pulse_votes = json.loads(self.pulse_votes) if self.pulse_votes else None +@dataclass +class DBNodeExit: + pubkey_bls: str + deregistration_height: int | None + exit_type: str + liquidation_height: int + + @dataclass class DBContributionMain: address: str diff --git a/db/read.py b/db/read.py index fe221ce..dd0084b 100644 --- a/db/read.py +++ b/db/read.py @@ -111,16 +111,22 @@ def get_nodes(self): self.log.perf.start("get_nodes") with closing(sqlite3.connect(self.db_path)) as connection: with closing(connection.cursor()) as cursor: + parsed_nodes = {} + # TODO: investigate using a join or something less messy than two select * queries cursor.execute("""SELECT * FROM service_nodes_main""") + + for node in cursor.fetchall(): + node_dict = DBNode(*node, contributors=[], events=[]) + parsed_nodes[node_dict.contract_id] = node_dict + # We want to sort by fetched_block_height in ascending order so later updates overwrite earlier ones cursor.execute( """SELECT * FROM service_nodes_staging ORDER BY fetched_block_height ASC""" ) - parsed_nodes = {} for node in cursor.fetchall(): - node_dict = DBNode(*node, contributors=[]) + node_dict = DBNode(*node, exit_type=None, deregistration_height=None, liquidation_height=None, contributors=[], events=[]) parsed_nodes[node_dict.contract_id] = node_dict cursor.execute("""SELECT * from service_nodes_contributions_main""") @@ -141,7 +147,25 @@ def get_nodes(self): contribution_dict ) - self.log.debug("Parsed nodes: {}".format(len(parsed_nodes))) + contract_ids = list(parsed_nodes.keys()) + + placeholder= '?' # For SQLite. See DBAPI paramstyle. + placeholders= ', '.join(placeholder for unused in contract_ids) + query= 'SELECT * FROM arbitrum_events WHERE main_arg IN (%s) ORDER BY block DESC' % placeholders + cursor.execute(query, contract_ids) + + for event in cursor.fetchall(): + processed_event = ProcessedEvent(*event) + try: + contract_id = int(processed_event.main_arg) + parsed_nodes[contract_id].events.append(processed_event) + except Exception as e: + self.log.error("Error processing event: {}".format(e)) + continue + + nodes_list = list(parsed_nodes.values()) + + self.log.debug("Parsed nodes: {}".format(len(nodes_list))) self.log.perf.end("get_nodes") return list(parsed_nodes.values()) diff --git a/db/schema.sql b/db/schema.sql index bfa2a20..a553b85 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -98,6 +98,10 @@ CREATE TABLE service_nodes_main ( swarm TEXT NOT NULL, swarm_id TEXT NOT NULL, -- too large to be an int total_contributed INTEGER NOT NULL, + /** NOTE: The exit details below are unique to the main db */ + deregistration_height INTEGER, + exit_type TEXT, + liquidation_height INTEGER, PRIMARY KEY(contract_id) ); diff --git a/db/write.py b/db/write.py index 8781671..3df8745 100644 --- a/db/write.py +++ b/db/write.py @@ -6,7 +6,7 @@ from web3 import Web3 from arbitrum import ContributionContractDetails -from db.dataclasses import RewardsInfo +from db.dataclasses import RewardsInfo, DBNodeExit from log import Log from oxen.rpc import ServiceNode, NetworkInfo from web3client.abi_manager import ABIData @@ -318,6 +318,41 @@ def write_nodes_to_main_db(self, immutable_height: int): self.log.perf.end("write_nodes_to_main_db") + def write_exit_list_to_db(self, exit_list: list[DBNodeExit]): + self.log.perf.start("write_exit_list_to_db") + with closing(sqlite3.connect(self.db_path)) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + self.log.debug("Updating nodes in main with {} exit events".format(len(exit_list))) + self.log.perf.start("write_exit_list_to_db -> insert exit events") + cursor.executemany( + """ + UPDATE service_nodes_main SET + deregistration_height = ?, + exit_type = ?, + liquidation_height = ? + WHERE pubkey_bls = ? + """, + ( + ( + e.deregistration_height, + e.exit_type, + e.liquidation_height, + e.pubkey_bls, + ) + for e in exit_list + ) + ) + inserted_exit_rows = cursor.rowcount + + self.log.perf.end("write_exit_list_to_db -> insert exit events") + self.log.debug( + "Inserted {} rows into exit events".format(inserted_exit_rows) + ) + + connection.commit() + self.log.perf.end("write_exit_list_to_db") + def write_network_info_to_db( self, network: NetworkInfo, diff --git a/fetcher.py b/fetcher.py index 0066ef6..59317fd 100644 --- a/fetcher.py +++ b/fetcher.py @@ -10,7 +10,7 @@ update_contribution_contract_details, batch_populate_events_with_block_timestamps, populate_events_with_main_arg, ) from config_validate import validate_config -from db.dataclasses import RewardsInfo +from db.dataclasses import RewardsInfo, DBNodeExit from db.util import ( assert_all_dict_values_are_within_sqlite_integer_range, is_db_initialized, @@ -201,6 +201,9 @@ def run(self): self.time_keeper.add("db_migrate") self.db_writer.write_nodes_to_main_db(network.immutable_block_height) self.time_keeper.end("db_migrate") + self.time_keeper.add("exit_list_update") + self.update_exit_list() + self.time_keeper.end("exit_list_update") if (network.block_height - 1) > network_last_fetched_height: self.time_keeper.add("net_update") @@ -402,7 +405,40 @@ def fetch_service_node_list(self): self.log.exception(e) finally: self.log.perf.end("update_service_node_list") - return parsed_nodes, contributions, current_height + return parsed_nodes, contributions, current_height, len(parsed_nodes), active_node_count + + def update_exit_list(self): + self.log.perf.start("update_exit_list") + self.log.info("Update exit list task start") + exit_liquidation_list = self.rpc.bls_exit_liquidation_list().get() + + if exit_liquidation_list is None: + self.log.warning("bls_exit_liquidation_list is None, fetching exit list failed") + return + + exit_events = [] + for entry in exit_liquidation_list: + + pubkey_bls = entry.get("info").get("bls_public_key") + if pubkey_bls is None: + self.log.warning(f"info.bls_public_key is None for bls_exit_liquidation_list entry: {entry}") + continue + + exit_type = entry.get("type") + exit_events.append( + DBNodeExit( + pubkey_bls=pubkey_bls, + deregistration_height=entry.get("height") if exit_type == "deregister" else None, + exit_type=exit_type, + liquidation_height=entry.get("liquidation_height"), + ) + ) + + self.log.debug("Processed {} exit events".format(len(exit_events))) + self.db_writer.write_exit_list_to_db(exit_events) + self.log.info("Update exit list task finish") + self.log.perf.end("update_exit_list") + def get_rewards_info(self): self.log.perf.start("update_rewards_details") From e208a272ddf40f2814e0458d1e7685ffb728cf75 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Sun, 29 Dec 2024 08:53:58 +1100 Subject: [PATCH 028/138] fix: rewards address parsing --- db/read.py | 4 ++-- fetcher.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/db/read.py b/db/read.py index dd0084b..dae19b7 100644 --- a/db/read.py +++ b/db/read.py @@ -175,8 +175,8 @@ def get_rewards_info(self): with closing(connection.cursor()) as cursor: cursor.execute("SELECT * FROM rewards_info") rewards_info = { - address: rewards - for address, rewards in cursor.fetchall() + eth_utils.to_checksum_address(address_hex): rewards + for address_hex, rewards in cursor.fetchall() } self.log.debug("Rewards info: {}".format(len(rewards_info))) self.log.perf.end("get_rewards_info") diff --git a/fetcher.py b/fetcher.py index 59317fd..94088e0 100644 --- a/fetcher.py +++ b/fetcher.py @@ -461,7 +461,7 @@ def get_rewards_info(self): self.log.warning("Invalid address {}".format(address)) continue - rewards_info.append(RewardsInfo(address_hex, rewards)) + rewards_info.append(RewardsInfo(address, rewards)) except Exception as e: self.log.error("Error fetching and parsing rewards details") From 2f4181707da211e374d116b6dcca1dd00574042d Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Sun, 29 Dec 2024 08:54:21 +1100 Subject: [PATCH 029/138] fix: add contribution contract api endpoints --- api.py | 72 +++++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 59 insertions(+), 13 deletions(-) diff --git a/api.py b/api.py index e373771..5a14263 100644 --- a/api.py +++ b/api.py @@ -116,6 +116,12 @@ def get_nodes_cached(): def get_nodes(): return json_response({"nodes": get_nodes_cached()}) +def get_nodes_bls_keys_uncached(): + return [node.pubkey_bls for node in get_nodes_cached()] + +@app.route("/nodes/bls") +def get_nodes_bls_keys(): + return json_response({"bls_keys": app.data.get("nodes_bls_keys", getter=get_nodes_bls_keys_uncached)}) """ ////////////////////////////////////////////////////////////// @@ -125,6 +131,22 @@ def get_nodes(): ////////////////////////////////////////////////////////////// """ +def get_related_stakes_for_eth_address_uncached(eth_wal: str): + nodes = get_nodes_cached() + + related_nodes = [] + for node in nodes: + if node.operator_address == eth_wal: + related_nodes.append(node) + elif node.contributors is not None: + for contributor in node.contributors: + if contributor.address == eth_wal: + related_nodes.append(node) + + return related_nodes + +def get_related_stakes_for_eth_address_cached(eth_wal: str): + return app.data.get("related-stakes-{}".format(eth_wal), getter=get_related_stakes_for_eth_address_uncached, getter_args=eth_wal) # TODO: might make sense to investigate storing contributor and operator addresses in the db as blobs and compare with bytes @app.route("/stakes/") @@ -134,24 +156,14 @@ def get_stakes_for_eth_address(eth_wal: str): if not eth_wal or not eth_utils.is_address(eth_wal): raise ValueError("Invalid wallet address") - nodes = get_nodes_cached() - - related_nodes = [] - for node in nodes: - if node.operator_address == eth_wal: - related_nodes.append(node) - elif node.contributors is not None: - for contributor in node.contributors: - if contributor.address == eth_wal: - related_nodes.append(node) - - return json_response({"stakes": related_nodes}) + return json_response({"stakes": get_related_stakes_for_eth_address_cached(eth_wal), "contracts": get_related_contribution_contracts_for_eth_address_cached(eth_wal)}) except ValueError as e: app.logger.error(f"Exception: {e}") return flask.abort(400, e) except Exception as e: app.logger.error(f"Exception: {e}") + app.logger.exception(e) return flask.abort(500, e) @@ -209,12 +221,46 @@ def get_contract_addresses_core(): {"addresses": app.data.get("addresses_core", getter=app.db_reader.get_smart_contract_addresses_core)} ) +def get_contribution_contracts_cached(): + return app.data.get("contracts", getter=app.db_reader.get_contribution_contracts) + @app.route("/contract/contribution") def get_open_contract_details(): return json_response( - {"contracts": app.data.get("contracts", getter=app.db_reader.get_contribution_contracts)} + {"contracts": get_contribution_contracts_cached()} ) +def get_related_contribution_contracts_for_eth_address_uncached(eth_wal: str): + contracts = get_contribution_contracts_cached() + + related_contracts = [] + for contract in contracts: + if contract.operator_address == eth_wal: + related_contracts.append(contract) + elif contract.contributors is not None: + for contributor in contract.contributors: + if contributor.address == eth_wal: + related_contracts.append(contract) + return related_contracts + +def get_related_contribution_contracts_for_eth_address_cached(eth_wal: str): + return app.data.get("related-contracts-{}".format(eth_wal), getter=get_related_contribution_contracts_for_eth_address_uncached, getter_args=eth_wal) + +@app.route("/contract/contribution/") +def get_contribution_contracts_for_wallet(eth_wal: str): + try: + if not eth_wal or not eth_utils.is_address(eth_wal): + raise ValueError("Invalid wallet address") + + return json_response({"contracts": get_related_contribution_contracts_for_eth_address_cached(eth_wal)}) + + except ValueError as e: + app.logger.error(f"Exception: {e}") + return flask.abort(400, e) + except Exception as e: + app.logger.error(f"Exception: {e}") + return flask.abort(500, e) + @app.route("/contract/abi/") def get_abi(contract_name: str): From 001204852737dd7eb79c8ac9cbb38eec44e5f07a Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Sun, 29 Dec 2024 08:55:11 +1100 Subject: [PATCH 030/138] fix: event scanner block range chunk size passed between requests --- web3client/event_scanner.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/web3client/event_scanner.py b/web3client/event_scanner.py index 463ff24..c30f4d2 100644 --- a/web3client/event_scanner.py +++ b/web3client/event_scanner.py @@ -53,9 +53,9 @@ def __init__( provider_url: str, events: List, filters: Dict[str, Any], - max_chunk_scan_size: int = 10000, - max_request_retries: int = 10, - request_retry_seconds: float = 2.0, + max_chunk_scan_size: int = 10_000, + max_request_retries: int = 5, + request_retry_seconds: float = 5, safety_blocks: int = 10, ): """ @@ -77,7 +77,7 @@ def __init__( # Our JSON-RPC throttling parameters # self.min_scan_chunk_size = 10 # 12 s/block = 120 seconds period - self.min_scan_chunk_size = 10000 # 12 s/block = 120 seconds period + self.min_scan_chunk_size = 10 # 12 s/block = 120 seconds period self.max_scan_chunk_size = max_chunk_scan_size self.max_request_retries = max_request_retries self.request_retry_seconds = request_retry_seconds @@ -158,12 +158,7 @@ def estimate_next_chunk_size(self, current_chuck_size: int, event_found_count: i When any transfers are encountered, we are back to scanning only a few blocks at a time. It does not make sense to do a full chain scan starting from block 1, doing one JSON-RPC call per 20 blocks. """ - - if event_found_count > 0: - # When we encounter first events, reset the chunk size window - current_chuck_size = self.min_scan_chunk_size - else: - current_chuck_size *= self.chunk_size_increase + current_chuck_size *= self.chunk_size_increase current_chuck_size = max(self.min_scan_chunk_size, current_chuck_size) current_chuck_size = min(self.max_scan_chunk_size, current_chuck_size) From f71a41cc2a24d8e724a9d4e2f3f803ced97ef722 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Sun, 29 Dec 2024 09:41:38 +1100 Subject: [PATCH 031/138] fix: time keeper perf enabled --- log/time_keeper.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/log/time_keeper.py b/log/time_keeper.py index 859c9cd..a897ee9 100644 --- a/log/time_keeper.py +++ b/log/time_keeper.py @@ -20,8 +20,7 @@ def __init__(self, logger: logging, perf=False, max_events=10_000): self.exec_durations = {} self.exec_cpu_durations = {} - if perf: - self.perf = PerformanceLogger(logger, enabled=True) + self.perf = PerformanceLogger(logger, enabled=perf) def add(self, name: str): self.exec_timestamps.setdefault(name, []).append(time.time()) From f9650349de18c0cd09d5cc57de44bfbc3a555c61 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Sun, 29 Dec 2024 09:49:59 +1100 Subject: [PATCH 032/138] fix: time keeper perf --- fetcher.py | 1 - log/perf.py | 4 +++- log/time_keeper.py | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/fetcher.py b/fetcher.py index 94088e0..b30f7a6 100644 --- a/fetcher.py +++ b/fetcher.py @@ -126,7 +126,6 @@ def __init__(self, name): self.time_keeper = TimeKeeper( logger=Log("time_keeper").logger, - perf=config.backend.performance_logging, max_events=config.backend.max_time_keeper_events, ) diff --git a/log/perf.py b/log/perf.py index 3b802b2..98833f6 100644 --- a/log/perf.py +++ b/log/perf.py @@ -8,6 +8,7 @@ def __init__(self, logger: logging = None, enabled=True): self.logger = logger self.start = self._start_enabled self.end = self.end_enabled + self.end_timer = self.end_timer_enabled self.times = {} self.cpu_times = {} self.orphaned_event_age_seconds = 3600 # 1 hour @@ -17,6 +18,7 @@ def __init__(self, logger: logging = None, enabled=True): self.logger = None self.start = self._noop self.end = self._noop + self.end_timer = self._noop def _start_enabled(self, label): self.times[label] = time.perf_counter_ns() @@ -26,7 +28,7 @@ def end_enabled(self, label): elapsed_ms, elapsed_cpu_ms = self.end_timer(label) self._log_end(label, elapsed_ms, elapsed_cpu_ms) - def end_timer(self, label): + def end_timer_enabled(self, label): start_time = self.times.pop(label, None) start_time_cpu = self.cpu_times.pop(label, None) diff --git a/log/time_keeper.py b/log/time_keeper.py index a897ee9..3e7300c 100644 --- a/log/time_keeper.py +++ b/log/time_keeper.py @@ -7,7 +7,7 @@ class TimeKeeper: - def __init__(self, logger: logging, perf=False, max_events=10_000): + def __init__(self, logger: logging, max_events=10_000): self.max_events = max_events assert self.max_events > 100, "max_events must be greater than 100 to be meaningful" assert self.max_events < 10e6, "max_events must be less than 10e6 to avoid memory issues" @@ -20,7 +20,7 @@ def __init__(self, logger: logging, perf=False, max_events=10_000): self.exec_durations = {} self.exec_cpu_durations = {} - self.perf = PerformanceLogger(logger, enabled=perf) + self.perf = PerformanceLogger(logger, enabled=True) def add(self, name: str): self.exec_timestamps.setdefault(name, []).append(time.time()) From ca798dc5fc1650b0345891b443f93335683efc0b Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 30 Dec 2024 09:50:26 +1100 Subject: [PATCH 033/138] fix: registration insert sql statement syntax --- registration/write.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/registration/write.py b/registration/write.py index 4eb68ca..4134e10 100644 --- a/registration/write.py +++ b/registration/write.py @@ -21,12 +21,12 @@ def write_registration_to_db(self, registration): cursor.execute( """ INSERT OR REPLACE INTO registrations ( - contract + contract, operator, pubkey_bls, pubkey_ed25519, sig_bls, - sig_ed25519, + sig_ed25519 ) VALUES (?, ?, ?, ?, ?, ?) """, From 828c3bb03b302acb9a5b7edc0d2006da1a5df52c Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 30 Dec 2024 10:26:24 +1100 Subject: [PATCH 034/138] fix: registration bytes formatting --- db/dataclasses.py | 14 ++++++++++++-- registration/read.py | 15 +-------------- util/parse.py | 8 +++++++- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/db/dataclasses.py b/db/dataclasses.py index de1e8c6..79d2018 100644 --- a/db/dataclasses.py +++ b/db/dataclasses.py @@ -2,6 +2,7 @@ from dataclasses import dataclass from typing import Optional +from util.parse import eth_format from web3client.event_scanner import ProcessedEvent @@ -172,10 +173,19 @@ class RewardsInfo: @dataclass class Registration: - contract: bytes + contract: bytes | None operator: bytes pubkey_bls: bytes pubkey_ed25519: bytes sig_bls: bytes sig_ed25519: bytes - timestamp: float \ No newline at end of file + timestamp: float + + def __post_init__(self): + self.contract = self.contract.hex() if self.contract is not None else None + self.operator = eth_format(self.operator) + self.pubkey_bls = self.pubkey_bls.hex() + self.pubkey_ed25519 = self.pubkey_ed25519.hex() + self.sig_bls = self.sig_bls.hex() + self.sig_ed25519 = self.sig_ed25519.hex() + diff --git a/registration/read.py b/registration/read.py index 3f7ef87..dcc4a32 100644 --- a/registration/read.py +++ b/registration/read.py @@ -1,21 +1,8 @@ import sqlite3 from contextlib import closing -from dataclasses import dataclass - +from db.dataclasses import Registration from log import Log - -@dataclass -class Registration: - contract: bytes - operator: bytes - pubkey_bls: bytes - pubkey_ed25519: bytes - sig_bls: bytes - sig_ed25519: bytes - timestamp: float - - class DBReaderRegistrations: def __init__(self, db_path: str, log_level: int, perf: bool = False): self.db_path = db_path diff --git a/util/parse.py b/util/parse.py index 7437f07..062af34 100644 --- a/util/parse.py +++ b/util/parse.py @@ -1,5 +1,5 @@ import re -from typing import Callable, Any +from typing import Callable, Any, Union from functools import partial import string @@ -7,6 +7,7 @@ import eth_utils import flask import oxenc +from eth_typing import ChecksumAddress from werkzeug.routing import BaseConverter eth_regex = "0x[0-9a-fA-F]{40}" @@ -38,6 +39,11 @@ def hexify(container): else: hexify(v) +def eth_format(addr: Union[bytes, str]) -> ChecksumAddress: + try: + return eth_utils.to_checksum_address(addr) + except ValueError: + raise ParseError(addr, "Invalid ETH address") class EthConverter(BaseConverter): def __init__(self, url_map): From c7d5f4eeac313cc497c64bbf9abebe75c78a1990 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Tue, 31 Dec 2024 09:58:52 +1100 Subject: [PATCH 035/138] fix: overriding in stake list from staging contributors --- db/read.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/db/read.py b/db/read.py index dae19b7..fe820ba 100644 --- a/db/read.py +++ b/db/read.py @@ -6,6 +6,7 @@ from db.dataclasses import DBNode, DBContributionMain, DBNetworkInfo, DBContributionContract, \ DBContributionContractContribution, SmartContractABI, ArbitrumEvent, ArbitrumInfo, RewardsInfo from log import Log +from util.parse import eth_format from web3client.event_scanner import ProcessedEvent @@ -127,17 +128,27 @@ def get_nodes(self): for node in cursor.fetchall(): node_dict = DBNode(*node, exit_type=None, deregistration_height=None, liquidation_height=None, contributors=[], events=[]) + existing_node = parsed_nodes.get(node_dict.contract_id) + if existing_node is not None: + existing_node.exit_type = node_dict.exit_type + existing_node.deregistration_height = node_dict.deregistration_height + existing_node.liquidation_height = node_dict.liquidation_height parsed_nodes[node_dict.contract_id] = node_dict + cursor.execute("""SELECT * from service_nodes_contributions_main""") + + db_contributions_main = [DBContributionMain(*contribution) for contribution in cursor.fetchall()] + # We want to sort by fetched_block_height in ascending order so later updates overwrite earlier ones cursor.execute( """SELECT * from service_nodes_contributions_staging ORDER BY fetched_block_height ASC""" ) + db_contributions_staging = [DBContributionMain(*contribution) for contribution in cursor.fetchall()] + parsed_contributions = {} - for contribution in cursor.fetchall(): - contribution_dict = DBContributionMain(*contribution) + for contribution_dict in db_contributions_main + db_contributions_staging: # TODO: there has to be a better way to override the old data with new data key = f"{contribution_dict.contract_id}{contribution_dict.address}" parsed_contributions[key] = contribution_dict @@ -175,7 +186,7 @@ def get_rewards_info(self): with closing(connection.cursor()) as cursor: cursor.execute("SELECT * FROM rewards_info") rewards_info = { - eth_utils.to_checksum_address(address_hex): rewards + eth_format(address_hex): rewards for address_hex, rewards in cursor.fetchall() } self.log.debug("Rewards info: {}".format(len(rewards_info))) From 71ca9bef4a57925ee33f4c58e2f473ec75272425 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Tue, 31 Dec 2024 09:59:35 +1100 Subject: [PATCH 036/138] fix: use eth checksum addresses for api endpoints --- api.py | 45 ++++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/api.py b/api.py index 5a14263..788c15a 100644 --- a/api.py +++ b/api.py @@ -6,13 +6,16 @@ import eth_utils import subprocess +from eth_typing import ChecksumAddress + import config from db.read import DBReader from log import Log from oxen.rpc import OxenRPC from registration.read import DBReaderRegistrations from util.data import DataManager -from util.parse import Hex64Converter, hexify, EthConverter +from util.parse import Hex64Converter, hexify, EthConverter, eth_format + class App(flask.Flask): def __init__(self, name): @@ -116,6 +119,7 @@ def get_nodes_cached(): def get_nodes(): return json_response({"nodes": get_nodes_cached()}) +# TODO: Get from contract def get_nodes_bls_keys_uncached(): return [node.pubkey_bls for node in get_nodes_cached()] @@ -131,32 +135,30 @@ def get_nodes_bls_keys(): ////////////////////////////////////////////////////////////// """ -def get_related_stakes_for_eth_address_uncached(eth_wal: str): +def get_related_stakes_for_eth_address_uncached(address: ChecksumAddress): nodes = get_nodes_cached() related_nodes = [] for node in nodes: - if node.operator_address == eth_wal: + if eth_format(node.operator_address) == address: related_nodes.append(node) elif node.contributors is not None: for contributor in node.contributors: - if contributor.address == eth_wal: + if eth_format(contributor.address) == address: related_nodes.append(node) return related_nodes -def get_related_stakes_for_eth_address_cached(eth_wal: str): - return app.data.get("related-stakes-{}".format(eth_wal), getter=get_related_stakes_for_eth_address_uncached, getter_args=eth_wal) +def get_related_stakes_for_eth_address_cached(address: ChecksumAddress): + return app.data.get("related-stakes-{}".format(address), getter=get_related_stakes_for_eth_address_uncached, getter_args=address) # TODO: might make sense to investigate storing contributor and operator addresses in the db as blobs and compare with bytes @app.route("/stakes/") @app.route("/nodes/") def get_stakes_for_eth_address(eth_wal: str): try: - if not eth_wal or not eth_utils.is_address(eth_wal): - raise ValueError("Invalid wallet address") - - return json_response({"stakes": get_related_stakes_for_eth_address_cached(eth_wal), "contracts": get_related_contribution_contracts_for_eth_address_cached(eth_wal)}) + address = eth_format(eth_wal) + return json_response({"stakes": get_related_stakes_for_eth_address_cached(address), "contracts": get_related_contribution_contracts_for_eth_address_cached(address)}) except ValueError as e: app.logger.error(f"Exception: {e}") @@ -354,13 +356,14 @@ def get_liquidation(ed25519_pubkey: bytes): return handle_get_exit_and_liquidation(ed25519_pubkey, liquidate=True) +def get_exit_liquidation_list_uncached(): + return app.rpc.bls_exit_liquidation_list().get() + @app.route("/exit_liquidation_list") def get_exit_liquidation_list(): - # TODO: add exit list management to main.py and main database and implement db_reader.get_exitable_nodes - # return json_response( - # {"result": app.data.get("exit_liquidation_list", getter=app.db_reader.get_exitable_nodes)} - # ) - return json_response({"result": []}) + return json_response( + {"result": app.data.get("exit_liquidation_list", getter=get_exit_liquidation_list_uncached)} + ) """ @@ -371,24 +374,24 @@ def get_exit_liquidation_list(): ////////////////////////////////////////////////////////////// """ -def get_rewards_signature_uncached(eth_wal: str): - if not eth_wal or not eth_utils.is_address(eth_wal): - raise ValueError("Invalid wallet address") - response = app.rpc.bls_rewards_request(eth_utils.to_checksum_address(eth_wal)).get() +def get_rewards_signature_uncached(address: ChecksumAddress): + response = app.rpc.bls_rewards_request(address).get() if response is None: raise TimeoutError("Failed to get rewards signature") return response @app.route("/rewards/", methods=["GET", "POST"]) def get_rewards(eth_wal: str): + address = eth_format(eth_wal) + if flask.request.method == "GET": # We cache all rewards info for all wallets so we don't need to multiple reads in a short period of time rewards_info = app.data.get(f"rewards_info", getter=app.db_reader.get_rewards_info) - return json_response({"rewards": rewards_info.get(eth_wal, 0)}) + return json_response({"rewards": rewards_info.get(address, 0)}) if flask.request.method == "POST": try: - response = app.data.get(f"rewards-sig-{eth_wal}", getter=get_rewards_signature_uncached, getter_args=eth_wal) + response = app.data.get(f"rewards-sig-{address}", getter=get_rewards_signature_uncached, getter_args=address) if "status" in response: response.pop("status") if "address" in response: From bf48c5a84efb3f4c38fcf818c6994b1657776922 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Thu, 2 Jan 2025 11:39:01 +1100 Subject: [PATCH 037/138] feat: add bls key parser --- util/parse.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/util/parse.py b/util/parse.py index 062af34..f68a432 100644 --- a/util/parse.py +++ b/util/parse.py @@ -13,6 +13,11 @@ eth_regex = "0x[0-9a-fA-F]{40}" +def parse_bls_pubkey(bls_pubkey: (str, str)): + x, y = bls_pubkey + return f"{x:064x}{y:064x}" + + def raw_eth_addr(k, v): if re.fullmatch(eth_regex, v): if not eth_utils.is_address(v): From c02f36d327ffe2e941fa82b1059dd59425ff9b65 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Thu, 2 Jan 2025 11:39:46 +1100 Subject: [PATCH 038/138] feat: create get_arbitrum_events_since_timestamp db reader --- db/read.py | 45 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/db/read.py b/db/read.py index fe820ba..a3beda4 100644 --- a/db/read.py +++ b/db/read.py @@ -1,10 +1,8 @@ import sqlite3 from contextlib import closing -import eth_utils - from db.dataclasses import DBNode, DBContributionMain, DBNetworkInfo, DBContributionContract, \ - DBContributionContractContribution, SmartContractABI, ArbitrumEvent, ArbitrumInfo, RewardsInfo + DBContributionContractContribution, SmartContractABI, ArbitrumInfo from log import Log from util.parse import eth_format from web3client.event_scanner import ProcessedEvent @@ -283,21 +281,22 @@ def get_smart_contract_address(self, name: str): self.log.perf.end("get_smart_contract_address") return address[0] - def get_arbitrum_events(self, args=None): + def get_arbitrum_events_page(self, args=None): if args is None: args = [1000, 0] self.log.perf.start("get_arbitrum_events") with closing(sqlite3.connect(self.db_path)) as connection: with closing(connection.cursor()) as cursor: - limit = args[0] - skip = args[1] + limit = args[0] if len(args) > 0 else 1000 + skip = args[1] if len(args) > 1 else 0 + cursor.execute( """ SELECT * FROM arbitrum_events ORDER BY block DESC LIMIT ? OFFSET ? """, (limit, skip), ) - events = [ArbitrumEvent(*event) for event in cursor.fetchall()] + events = [ProcessedEvent(*event) for event in cursor.fetchall()] self.log.debug("Arbitrum events: {}".format(len(events))) self.log.perf.end("get_arbitrum_events") @@ -306,6 +305,36 @@ def get_arbitrum_events(self, args=None): return events, limit, skip, total + def get_arbitrum_events_since_timestamp(self, params: [int, list[str] | None]) -> list[ProcessedEvent]: + timestamp = params[0] if len(params) > 0 else None + events_types = params[1] if len(params) > 1 and len(params[1]) > 0 else None + + if timestamp is None or (not isinstance(timestamp, int) and not isinstance(timestamp, float)): + raise ValueError("Invalid timestamp, timestamp must be an integer or float") + + if events_types is not None: + if isinstance(events_types, str): + events_types = [events_types] + elif not isinstance(events_types, list): + raise ValueError("Invalid events_types, events_types must be a list of strings or a string") + + + self.log.perf.start("get_arbitrum_events_since_timestamp") + with closing(sqlite3.connect(self.db_path)) as connection: + with closing(connection.cursor()) as cursor: + if events_types is None: + cursor.execute("SELECT * FROM arbitrum_events WHERE timestamp > ? ORDER BY timestamp DESC", (timestamp,)) + else: + placeholder= '?' # For SQLite. See DBAPI paramstyle. + placeholders= ', '.join(placeholder for unused in events_types) + query= 'SELECT * FROM arbitrum_events WHERE timestamp > ? AND name IN (%s) ORDER BY timestamp DESC' % placeholders + cursor.execute(query, (timestamp, *events_types)) + # cursor.execute("SELECT * FROM arbitrum_events WHERE timestamp > ? AND name IN ({}) ORDER BY timestamp DESC".format(",".join(["?"]*len(events_types))), tuple(events_types)+(timestamp,)) + events = [ProcessedEvent(*event) for event in cursor.fetchall()] + self.log.debug("Arbitrum events: {}".format(len(events))) + self.log.perf.end("get_arbitrum_events_since_timestamp") + return events + def get_arbitrum_info(self): self.log.perf.start("get_arbitrum_info") with closing(sqlite3.connect(self.db_path)) as connection: @@ -327,7 +356,7 @@ def get_arbitrum_events_for_stake_contrat_id(self, contract_id: int): """, (contract_id,), ) - events = [ArbitrumEvent(*event) for event in cursor.fetchall()] + events = [ProcessedEvent(*event) for event in cursor.fetchall()] self.log.debug("Arbitrum events: {}".format(len(events))) self.log.perf.end("get_events_for_stake_contrat_id") return events From 540661e73e48f6ab0e323d3cf166223afc611ce9 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Thu, 2 Jan 2025 11:40:21 +1100 Subject: [PATCH 039/138] feat: add node_add_timestamp to contribution contracts using arbitrum events --- api.py | 2 +- db/dataclasses.py | 1 + db/schema.sql | 2 ++ db/write.py | 6 ++++-- fetcher.py | 38 +++++++++++++++++++++++++++++++------- 5 files changed, 39 insertions(+), 10 deletions(-) diff --git a/api.py b/api.py index 788c15a..7ee2760 100644 --- a/api.py +++ b/api.py @@ -304,7 +304,7 @@ def get_contract_address(contract_name: str): def get_events_handler(count_limit=500, skip=0): limit = min(count_limit, 500) - events, limit, skip, total = app.data.get("events-{}-{}".format(count_limit,skip), getter=app.db_reader.get_arbitrum_events, getter_args=[limit, skip], ttl=10) + events, limit, skip, total = app.data.get("events-{}-{}".format(count_limit,skip), getter=app.db_reader.get_arbitrum_events_page, getter_args=[limit, skip], ttl=10) pagination = {"limit": limit, "skip": skip, "total": total} return {"events": events, "pagination": pagination} diff --git a/db/dataclasses.py b/db/dataclasses.py index 79d2018..286862a 100644 --- a/db/dataclasses.py +++ b/db/dataclasses.py @@ -115,6 +115,7 @@ def __post_init__(self): class DBContributionContract: address: str fee: int + node_add_timestamp: int operator_address: str pubkey_bls: str service_node_pubkey: str diff --git a/db/schema.sql b/db/schema.sql index a553b85..919d716 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -176,6 +176,7 @@ CREATE INDEX arbitrum_events_main_arg_idx ON arbitrum_events(main_arg, block DES CREATE TABLE contribution_contracts ( address TEXT NOT NULL, fee INTEGER NOT NULL, + node_add_timestamp INTEGER, operator_address TEXT NOT NULL, pubkey_bls BLOB NOT NULL, service_node_pubkey BLOB NOT NULL, @@ -186,6 +187,7 @@ CREATE TABLE contribution_contracts ( ); CREATE INDEX contribution_contracts_address_idx ON contribution_contracts(address); +CREATE INDEX contribution_contracts_node_add_timestamp_idx ON contribution_contracts(node_add_timestamp DESC); CREATE TABLE contribution_contracts_contributions ( diff --git a/db/write.py b/db/write.py index 3df8745..68bc9a9 100644 --- a/db/write.py +++ b/db/write.py @@ -484,7 +484,7 @@ def write_arbitrum_events_to_db(self, events: list[ProcessedEvent]): self.log.perf.end("write_arbitrum_events_to_db") def write_contribution_contracts_to_db( - self, contracts: list[ContributionContractDetails], contributions_list: list + self, contracts: list[ContributionContractDetails], contributions_list: list, recent_add_node_event_timestamps: dict[str, int] ): self.log.perf.start("write_contribution_contracts_to_db") @@ -500,18 +500,20 @@ def write_contribution_contracts_to_db( INSERT OR REPLACE INTO contribution_contracts ( address, fee, + node_add_timestamp, operator_address, pubkey_bls, service_node_pubkey, service_node_signature, status ) - VALUES (?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) """, ( ( contract.address, contract.fee, + recent_add_node_event_timestamps.get(contract.pubkey_bls), contract.operator_address, contract.pubkey_bls, contract.service_node_pubkey, diff --git a/fetcher.py b/fetcher.py index b30f7a6..d7b8304 100644 --- a/fetcher.py +++ b/fetcher.py @@ -22,6 +22,7 @@ from oxen.rpc import ServiceNode, OxenRPC, NetworkInfo from util import format_seconds, is_not_empty_string from log.time_keeper import TimeKeeper +from util.parse import parse_bls_pubkey from web3client.abi_manager import ABIManager from web3client.client import Web3Client from web3client.contracts.reward_rate_pool import RewardRatePoolInterface @@ -94,6 +95,8 @@ def __init__(self, name): self.arbitrum_details_last_updated = 0 + self.arbitrum_node_add_events_bls_key_to_timestamp_map = {} + self.web3_client = Web3Client( provider_urls=config.backend.web3_provider_urls, caller_address=config.backend.web3_caller_address, @@ -492,6 +495,8 @@ def update_arbitrum_details(self): end_block, ) + # Writing contract details to db + new_contracts = [] for contract in new_contribution_contracts: self.service_node_contribution_multi[contract.contract_address] = contract @@ -501,13 +506,7 @@ def update_arbitrum_details(self): self.db_writer.write_smart_contract_details_to_db(new_contracts) - contract_details_list, contributions_list = update_contribution_contract_details( - self.web3_client, self.log, list(self.service_node_contribution_multi.values()) - ) - - self.db_writer.write_contribution_contracts_to_db( - contract_details_list, contributions_list - ) + # NOTE: Writes events to db BEFORE writing contribution contracts to db so the events are available for the "recent_add_node_events_since_last_update" function events = self.service_node_rewards.event_scanner.run( last_block=last_event_block_height, @@ -520,6 +519,22 @@ def update_arbitrum_details(self): self.db_writer.write_arbitrum_events_to_db(events) + # Writing contribution contract details to db (if there are any) + + contrib_contract_list = list(self.service_node_contribution_multi.values()) + if len(contrib_contract_list) > 0: + contract_details_list, contributions_list = update_contribution_contract_details( + self.web3_client, self.log, contrib_contract_list + ) + + recent_add_node_event_timestamps = self.get_arbitrum_node_add_events_since_last_update() + + self.db_writer.write_contribution_contracts_to_db( + contract_details_list, contributions_list, recent_add_node_event_timestamps + ) + else: + self.log.info("No contribution contracts to write to db") + self.arbitrum_details_last_updated = time.time() self.log.perf.end("update_arbitrum_details") @@ -527,6 +542,15 @@ def update_arbitrum_details(self): self.log.error("Error fetching and parsing arbitrum details") self.log.exception(e) + def get_arbitrum_node_add_events_since_last_update(self): + recent_add_node_events = self.db_reader.get_arbitrum_events_since_timestamp([self.arbitrum_details_last_updated, ['NewServiceNodeV2']]) + for event in recent_add_node_events: + pubkey_bls_encoded = event.args.get("pubkey") + pubkey_bls = parse_bls_pubkey((pubkey_bls_encoded["X"], pubkey_bls_encoded["Y"])) + self.arbitrum_node_add_events_bls_key_to_timestamp_map["0x{}".format(pubkey_bls)] = event.timestamp + + return self.arbitrum_node_add_events_bls_key_to_timestamp_map + app = App(config.backend.fetcher_name if config.backend.fetcher_name else __name__) app.run() From c8a71729656d07f0e0c43e814d03849af93cf6ae Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Thu, 2 Jan 2025 15:24:26 +1100 Subject: [PATCH 040/138] feat: create rpc usage tracker --- api.py | 33 +++++++++++++--- config_defaults.py | 3 ++ fetcher.py | 15 ++++---- oxen/omq.py | 96 +++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 134 insertions(+), 13 deletions(-) diff --git a/api.py b/api.py index 7ee2760..43982ea 100644 --- a/api.py +++ b/api.py @@ -5,9 +5,8 @@ import time import eth_utils import subprocess - from eth_typing import ChecksumAddress - +from uwsgidecorators import timer import config from db.read import DBReader from log import Log @@ -55,10 +54,12 @@ def __init__(self, name): ) self.rpc = OxenRPC( - self.log, - rpc_url, - rpc_cache, + logger=self.log, + rpc_url=rpc_url, + cache_seconds=rpc_cache, + usage_tracking=config.backend.rpc_api_usage_logging, ) + self.loop_sleep_refresh_rate_seconds = rpc_cache if rpc_cache > 0 else 5 self.data = DataManager(stale_time_seconds=config.backend.stale_time_seconds) @@ -476,3 +477,25 @@ def bootstrap(): bootstrap() + + +if config.backend.rpc_api_usage_logging: + def log_rpc_usage(signum): + app.logger.info("Logging RPC usage for {}".format(signum)) + app.rpc.usage_tracker.log_usage() + + @timer(config.backend.rpc_api_usage_logging_interval, target="worker1") + def log_rpc_usage_w1(signum): + log_rpc_usage(signum) + + @timer(config.backend.rpc_api_usage_logging_interval, target="worker2") + def log_rpc_usage_w2(signum): + log_rpc_usage(signum) + + @timer(config.backend.rpc_api_usage_logging_interval, target="worker3") + def log_rpc_usage_w3(signum): + log_rpc_usage(signum) + + @timer(config.backend.rpc_api_usage_logging_interval, target="worker4") + def log_rpc_usage_w4(signum): + log_rpc_usage(signum) \ No newline at end of file diff --git a/config_defaults.py b/config_defaults.py index 0a2e595..e652227 100644 --- a/config_defaults.py +++ b/config_defaults.py @@ -34,6 +34,8 @@ class Backend: api_name: str = "api" rpc_api: str = "" rpc_api_cache: int = 2 + rpc_api_usage_logging: bool = False + rpc_api_usage_logging_interval: int = 600 """ REGISTRATION CONFIG """ @@ -60,6 +62,7 @@ class Backend: performance_logging: bool = False rpc_fetcher: str = "" rpc_fetcher_cache: int = 2 + rpc_fetcher_usage_logging: bool = False stale_time_seconds: int = 30 stale_time_seconds_contract_abis: int = 300 thread_pool_max_workers: int = 50 diff --git a/fetcher.py b/fetcher.py index d7b8304..59f40d6 100644 --- a/fetcher.py +++ b/fetcher.py @@ -87,10 +87,12 @@ def __init__(self, name): ) self.rpc = OxenRPC( - self.log, - rpc_url, - rpc_cache, + logger=self.log, + rpc_url=rpc_url, + cache_seconds=rpc_cache, + usage_tracking=config.backend.rpc_fetcher_usage_logging, ) + self.loop_sleep_refresh_rate_seconds = rpc_cache if rpc_cache > 0 else 5 self.arbitrum_details_last_updated = 0 @@ -200,12 +202,10 @@ def run(self): self.time_keeper.end("arb_update") if network.immutable_block_height > network_last_commited_height: - self.time_keeper.add("db_migrate") + self.time_keeper.add("db_migrate_and_update_exit_list") self.db_writer.write_nodes_to_main_db(network.immutable_block_height) - self.time_keeper.end("db_migrate") - self.time_keeper.add("exit_list_update") self.update_exit_list() - self.time_keeper.end("exit_list_update") + self.time_keeper.end("db_migrate_and_update_exit_list") if (network.block_height - 1) > network_last_fetched_height: self.time_keeper.add("net_update") @@ -214,6 +214,7 @@ def run(self): self.log.perf.end("loop") self.time_keeper.log_time_keeper() + self.rpc.usage_tracker.log_usage() now = time.time() arb_next_update = ( diff --git a/oxen/omq.py b/oxen/omq.py index 60dfda8..b8bd2d8 100644 --- a/oxen/omq.py +++ b/oxen/omq.py @@ -1,8 +1,95 @@ +import logging import oxenmq import json import sys from datetime import datetime, timedelta + +class RPCUsageTracker: + def __init__(self, enabled: bool, log: logging): + self.log = log + if enabled: + self.log.warning("RPC usage tracking enabled. This is not recommended for production use.") + self.uses_success = {} + self.uses_failed = {} + self.uses_cached = {} + self.add_success = self.add_success_enabled + self.add_failed = self.add_failed_enabled + self.add_cached = self.add_cached_enabled + self.log_usage = self.log_usage_enabled + + else: + self.add_success = self._noop + self.add_failed = self._noop + self.add_cached = self._noop + self.log_usage = self._noop + + def _noop(self, *args, **kwargs): + pass + + def add_success_enabled(self, endpoint: str): + self.uses_success.setdefault(endpoint, []).append(datetime.now().timestamp()) + + def add_failed_enabled(self, endpoint: str): + self.uses_failed.setdefault(endpoint, []).append(datetime.now().timestamp()) + + def add_cached_enabled(self, endpoint: str): + self.uses_cached.setdefault(endpoint, []).append(datetime.now().timestamp()) + + def log_usage_enabled(self): + self.log.info("RPC usage tracking: (s/f/c {}/{}/{})".format(len(self.uses_success), len(self.uses_failed), len(self.uses_cached))) + unique_endpoints: dict[str, dict[str, list[float]]] = {} + + for endpoint, timestamps in self.uses_success.items(): + unique_endpoints.setdefault(endpoint, {"success": [], "failed": [], "cached": []}) + unique_endpoints[endpoint]["success"].extend(timestamps) + + for endpoint, timestamps in self.uses_failed.items(): + unique_endpoints.setdefault(endpoint, {"success": [], "failed": [], "cached": []}) + unique_endpoints[endpoint]["failed"].extend(timestamps) + + for endpoint, timestamps in self.uses_cached.items(): + unique_endpoints.setdefault(endpoint, {"success": [], "failed": [], "cached": []}) + unique_endpoints[endpoint]["cached"].extend(timestamps) + + for endpoint, timestamps in unique_endpoints.items(): + stats_success = timestamps["success"] + stats_failed = timestamps["failed"] + stats_cached = timestamps["cached"] + + total_success = len(stats_success) + total_failure = len(stats_failed) + total_cached = len(stats_cached) + + total = total_success + total_failure + total_cached + if total == 0: + continue + + timestamps_all = stats_success + stats_failed + stats_cached + timestamps_all.sort() + + ts_first = timestamps_all[0] + ts_last = timestamps_all[-1] + ts_delta = ts_last - ts_first + + if ts_delta == 0: + continue + + avg_rpm = total / ts_delta + + now = datetime.now().timestamp() + + last_hour_timestamps = [t for t in timestamps_all if t > now - 3600] + last_10_minutes_timestamps = [t for t in last_hour_timestamps if t > now - 600] + + avg_rpm_last_hour = len(last_hour_timestamps) / 3600 + avg_rpm_last_10_minutes = len(last_10_minutes_timestamps) / 600 + + self.log.info(f"Endpoint: {endpoint}") + self.log.info(f" Total: {total} ({total_success} success, {total_failure} failure, {total_cached} cached)") + self.log.info(f" RPM: {avg_rpm} avg (last hour: {avg_rpm_last_hour}, last 10m: {avg_rpm_last_10_minutes})") + + omq, oxend = None, None @@ -53,7 +140,8 @@ def __init__( cache_key="", args=None, fail_okay=False, - timeout=10 + timeout=10, + rpc_usage_tracker: RPCUsageTracker = RPCUsageTracker(False, None), ): self.endpoint = endpoint self.cache_key = self.endpoint + cache_key @@ -75,6 +163,7 @@ def __init__( oxend, self.endpoint, [] if self.args is None else [self.args], timeout=timeout ) self.cache_seconds = cache_seconds + self.rpc_usage_tracker = rpc_usage_tracker def get(self): """If the result is already available, returns it immediately (and can safely be called multiple times. @@ -85,6 +174,7 @@ def get(self): result = self.future.get() self.future = None if result[0] != b"200": + self.rpc_usage_tracker.add_failed(self.endpoint) raise RuntimeError( "Request for {} failed: got {}".format(self.endpoint, result) ) @@ -95,9 +185,13 @@ def get(self): cache_expiry[self.cache_key] = datetime.now() + timedelta( seconds=self.cache_seconds ) + self.rpc_usage_tracker.add_success(self.endpoint) except RuntimeError as e: if not self.fail_okay: print("Something getting wrong: {}".format(e), file=sys.stderr) self.future = None + else: + self.rpc_usage_tracker.add_cached(self.endpoint) return self.json + From 65079d3bc2445c2c79e8617006289487fd1503fd Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Thu, 2 Jan 2025 15:25:02 +1100 Subject: [PATCH 041/138] chore: simplify oxen rpc to use shared future json method --- oxen/rpc.py | 56 +++++++++++++++++++---------------------------------- 1 file changed, 20 insertions(+), 36 deletions(-) diff --git a/oxen/rpc.py b/oxen/rpc.py index 012e148..aa47889 100644 --- a/oxen/rpc.py +++ b/oxen/rpc.py @@ -1,6 +1,6 @@ import logging from typing import TypedDict -from oxen.omq import FutureJSON, omq_connection +from oxen.omq import FutureJSON, omq_connection, RPCUsageTracker from dataclasses import dataclass @@ -66,78 +66,63 @@ class NetworkInfo: class OxenRPC: - def __init__(self, logger: logging, rpc_url: str, cache_seconds: float | None = None): + def __init__(self, logger: logging, rpc_url: str, cache_seconds: float | None = None, usage_tracking: bool = False): self.log = logger self.rpc_url = rpc_url self.cache_seconds = cache_seconds + self.usage_tracker = RPCUsageTracker(usage_tracking, self.log) - def get_accrued_rewards(self) -> FutureJSON: + def FutureJSON(self,endpoint: str, args: dict | None = None, cache_seconds: float | None = None): omq, oxend = omq_connection(self.rpc_url) return FutureJSON( - omq, - oxend, + omq=omq, + oxend=oxend, + endpoint=endpoint, + args=args, + cache_seconds=cache_seconds if cache_seconds is not None else self.cache_seconds, + rpc_usage_tracker=self.usage_tracker, + ) + + def get_accrued_rewards(self) -> FutureJSON: + return self.FutureJSON( "rpc.get_accrued_rewards", args={"addresses": []}, - cache_seconds=self.cache_seconds, ) def bls_rewards_request(self, eth_address: str) -> FutureJSON: - omq, oxend = omq_connection(self.rpc_url) eth_address_for_rpc = eth_address.lower() if eth_address_for_rpc.startswith("0x"): eth_address_for_rpc = eth_address_for_rpc[2:] - result = FutureJSON( - omq, - oxend, + result = self.FutureJSON( "rpc.bls_rewards_request", args={"address": eth_address_for_rpc}, - cache_seconds=self.cache_seconds, ) return result def bls_exit_liquidation_request(self, ed25519_pubkey: bytes, liquidate: bool) -> FutureJSON: - omq, oxend = omq_connection(self.rpc_url) - return FutureJSON( - omq, - oxend, + return self.FutureJSON( "rpc.bls_exit_liquidation_request", args={"pubkey": ed25519_pubkey.hex(), "liquidate": liquidate}, - cache_seconds=self.cache_seconds, ) def bls_exit_liquidation_list(self) -> FutureJSON: - omq, oxend = omq_connection(self.rpc_url) - return FutureJSON( - omq, - oxend, + return self.FutureJSON( "rpc.bls_exit_liquidation_list", - cache_seconds=self.cache_seconds, ) def get_info(self) -> FutureJSON: - omq, oxend = omq_connection(self.rpc_url) - return FutureJSON( - omq, - oxend, + return self.FutureJSON( "rpc.get_info", - cache_seconds=self.cache_seconds, ) def get_last_block_header(self) -> FutureJSON: - omq, oxend = omq_connection(self.rpc_url) - return FutureJSON( - omq, - oxend, + return self.FutureJSON( "rpc.get_last_block_header", args={"fill_pow_hash": False, "get_tx_hashes": False}, - cache_seconds=self.cache_seconds, ) def get_service_nodes(self) -> FutureJSON: - omq, oxend = omq_connection(self.rpc_url) - return FutureJSON( - omq, - oxend, + return self.FutureJSON( "rpc.get_service_nodes", args={ "all": True, @@ -169,7 +154,6 @@ def get_service_nodes(self) -> FutureJSON: # ) # }, }, - cache_seconds=self.cache_seconds, ) def get_network_info_from_network(self): From 8c6d5e9f01f3320d8f1012855862432b237f873f Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Thu, 2 Jan 2025 17:16:14 +1100 Subject: [PATCH 042/138] chore: clean up rpc usage logging formatting --- api.py | 3 +- oxen/omq.py | 111 +++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 94 insertions(+), 20 deletions(-) diff --git a/api.py b/api.py index 43982ea..5146209 100644 --- a/api.py +++ b/api.py @@ -481,8 +481,7 @@ def bootstrap(): if config.backend.rpc_api_usage_logging: def log_rpc_usage(signum): - app.logger.info("Logging RPC usage for {}".format(signum)) - app.rpc.usage_tracker.log_usage() + app.rpc.usage_tracker.log_usage("Logging RPC usage for {}".format(signum)) @timer(config.backend.rpc_api_usage_logging_interval, target="worker1") def log_rpc_usage_w1(signum): diff --git a/oxen/omq.py b/oxen/omq.py index b8bd2d8..55ff515 100644 --- a/oxen/omq.py +++ b/oxen/omq.py @@ -1,9 +1,50 @@ import logging +from collections import defaultdict + import oxenmq import json import sys from datetime import datetime, timedelta +def format_table(table: list[list[str| float | int]]): + """ + Formats a table of values into a string. + :param table: list of lists of values + :return: string + """ + # stringify every value in the table + table = [[str(v) for v in row] for row in table] + # find the maximum width of each column + column_widths = [max(len(row[i]) for row in table) for i in range(len(table[0]))] + # pad each column to the maximum width + padded_table = [[row[i].ljust(column_widths[i]) for i in range(len(row))] for row in table] + # join each row with a space separator + padded_table = [" ".join(row) for row in padded_table] + # join each row with a newline separator + return "\n".join(padded_table) + +def bin_histogram_timestamps(timestamps, bin_size_seconds): + """ + Bins a histogram of timestamps into a histogram of bins of size 'bin_size_seconds'. + + :param timestamps: list of timestamps + :param bin_size_seconds: size of each bin in seconds + :return: dict of bins to counts + """ + bins = defaultdict(int) + for t in timestamps: + t_int = int(t) + bin_key = t_int // bin_size_seconds + bins[bin_key] += 1 + + max_count_adjusted = 0 + max_count_timestamp = 0 + for k, v in bins.items(): + if v > max_count_adjusted: + max_count_adjusted = v / bin_size_seconds + max_count_timestamp = k * bin_size_seconds + + return bins, max_count_timestamp, max_count_adjusted class RPCUsageTracker: def __init__(self, enabled: bool, log: logging): @@ -36,8 +77,8 @@ def add_failed_enabled(self, endpoint: str): def add_cached_enabled(self, endpoint: str): self.uses_cached.setdefault(endpoint, []).append(datetime.now().timestamp()) - def log_usage_enabled(self): - self.log.info("RPC usage tracking: (s/f/c {}/{}/{})".format(len(self.uses_success), len(self.uses_failed), len(self.uses_cached))) + def log_usage_enabled(self, msg: str): + msg += "\nRPC usage tracking: (s/f/c {}/{}/{})\n".format(len(self.uses_success), len(self.uses_failed), len(self.uses_cached)) unique_endpoints: dict[str, dict[str, list[float]]] = {} for endpoint, timestamps in self.uses_success.items(): @@ -53,6 +94,7 @@ def log_usage_enabled(self): unique_endpoints[endpoint]["cached"].extend(timestamps) for endpoint, timestamps in unique_endpoints.items(): + log_lines = [] stats_success = timestamps["success"] stats_failed = timestamps["failed"] stats_cached = timestamps["cached"] @@ -70,25 +112,58 @@ def log_usage_enabled(self): ts_first = timestamps_all[0] ts_last = timestamps_all[-1] - ts_delta = ts_last - ts_first + total_time_seconds = ts_last - ts_first - if ts_delta == 0: + if total_time_seconds == 0: continue - avg_rpm = total / ts_delta - - now = datetime.now().timestamp() - - last_hour_timestamps = [t for t in timestamps_all if t > now - 3600] - last_10_minutes_timestamps = [t for t in last_hour_timestamps if t > now - 600] - - avg_rpm_last_hour = len(last_hour_timestamps) / 3600 - avg_rpm_last_10_minutes = len(last_10_minutes_timestamps) / 600 - - self.log.info(f"Endpoint: {endpoint}") - self.log.info(f" Total: {total} ({total_success} success, {total_failure} failure, {total_cached} cached)") - self.log.info(f" RPM: {avg_rpm} avg (last hour: {avg_rpm_last_hour}, last 10m: {avg_rpm_last_10_minutes})") - + current_time = datetime.now().timestamp() + + # ---- 1. Average RPS over all time ---- + rps_avg = total / total_time_seconds + + # ---- 2. Average RPS in the last hour ---- + one_hour_ago = current_time - 3600 + last_hour_timestamps = [ts for ts in timestamps_all if ts >= one_hour_ago] + # If you want a simple “count / 3600”, do: + rps_avg_last_hour = len(last_hour_timestamps) / 3600.0 + + # ---- 3. Average RPS in the last 10 minutes (600 seconds) ---- + ten_minutes_ago = current_time - 600 + last_10m_timestamps = [ts for ts in last_hour_timestamps if ts >= ten_minutes_ago] + rps_avg_last_10_minutes = len(last_10m_timestamps) / 600.0 + + # ---- 4. Average RPS in the last 1 minute (60 seconds) ---- + one_minute_ago = current_time - 60 + last_1m_timestamps = [ts for ts in last_10m_timestamps if ts >= one_minute_ago] + rps_avg_last_1_minute = len(last_1m_timestamps) / 60.0 + + # ---- 5. Peak RPS over the past hour ---- + (h_1h_sec, rps_peak_time_1h_sec, rps_peak_1h_sec) = bin_histogram_timestamps(last_hour_timestamps, bin_size_seconds=1) + (h_1h_min, rps_peak_time_1h_min, rps_peak_1h_min) = bin_histogram_timestamps(last_hour_timestamps, bin_size_seconds=60) + + # ---- 6. Peak RPS over the past hour binned every 10 minutes---- + (h_10m_sec, rps_peak_time_10m_sec, rps_peak_10m_sec) = bin_histogram_timestamps(last_10m_timestamps, bin_size_seconds=1) + (h_10m_min, rps_peak_time_10m_min, rps_peak_10m_min) = bin_histogram_timestamps(last_10m_timestamps, bin_size_seconds=60) + + log_lines.append(f"{endpoint} | Total: {total} ({total_success} success, {total_failure} failure, {total_cached} cached)") + + stats = [ + ["Period", "#", "RPS", "Peak RPS (1s bin)", "Peak RPS Time (1s bin)", "Peak RPS (1m bin)", + "Peak RPS Time (1m bin)"], + [f"Life ({total_time_seconds:.0f}s)", f"{total}", f"{rps_avg:.2f}", "", "", "", ""], + [f"< 1h", len(last_hour_timestamps), f"{rps_avg_last_hour:.2f}", f"{rps_peak_1h_sec:.2f}", + rps_peak_time_1h_sec, f"{rps_peak_1h_min:.2f}", rps_peak_time_1h_min], + [f"< 10m", len(last_10m_timestamps), f"{rps_avg_last_10_minutes:.2f}", f"{rps_peak_10m_sec:.2f}", + rps_peak_time_10m_sec, f"{rps_peak_10m_min:.2f}", rps_peak_time_10m_min], + [f"< 1m", len(last_1m_timestamps), f"{rps_avg_last_1_minute:.2f}", "", "", "", ""]] + + log_lines.append(format_table(stats)) + + # self.log.info(f" Requests in past {total} ({total_success} success, {total_failure} failure, {total_cached} cached)") + # self.log.info(f" Avg requests per second: {rps_avg:.2f} avg (last hour: {rps_avg_last_hour:.2f}, last 10m: {rps_avg_last_10_minutes:.2f})") + msg += ("\n".join(log_lines)) + self.log.info(msg) omq, oxend = None, None From 0ba118d9cd1460456c96dbe285892e7bb7887684 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Thu, 2 Jan 2025 17:20:31 +1100 Subject: [PATCH 043/138] fix: add default message to rpc usage --- oxen/omq.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oxen/omq.py b/oxen/omq.py index 55ff515..8a77a49 100644 --- a/oxen/omq.py +++ b/oxen/omq.py @@ -77,7 +77,7 @@ def add_failed_enabled(self, endpoint: str): def add_cached_enabled(self, endpoint: str): self.uses_cached.setdefault(endpoint, []).append(datetime.now().timestamp()) - def log_usage_enabled(self, msg: str): + def log_usage_enabled(self, msg: str = ""): msg += "\nRPC usage tracking: (s/f/c {}/{}/{})\n".format(len(self.uses_success), len(self.uses_failed), len(self.uses_cached)) unique_endpoints: dict[str, dict[str, list[float]]] = {} From c6cf1f86ff3850a67ab5df53ff1816cc7e163474 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 3 Jan 2025 10:17:00 +1100 Subject: [PATCH 044/138] fix: extend timeout on prc rewards bls request --- oxen/rpc.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/oxen/rpc.py b/oxen/rpc.py index aa47889..a9dcd12 100644 --- a/oxen/rpc.py +++ b/oxen/rpc.py @@ -72,7 +72,7 @@ def __init__(self, logger: logging, rpc_url: str, cache_seconds: float | None = self.cache_seconds = cache_seconds self.usage_tracker = RPCUsageTracker(usage_tracking, self.log) - def FutureJSON(self,endpoint: str, args: dict | None = None, cache_seconds: float | None = None): + def FutureJSON(self,endpoint: str, args: dict | None = None, cache_seconds: float | None = None, timeout: int | None = None): omq, oxend = omq_connection(self.rpc_url) return FutureJSON( omq=omq, @@ -80,6 +80,7 @@ def FutureJSON(self,endpoint: str, args: dict | None = None, cache_seconds: floa endpoint=endpoint, args=args, cache_seconds=cache_seconds if cache_seconds is not None else self.cache_seconds, + timeout=timeout, rpc_usage_tracker=self.usage_tracker, ) @@ -96,6 +97,7 @@ def bls_rewards_request(self, eth_address: str) -> FutureJSON: result = self.FutureJSON( "rpc.bls_rewards_request", args={"address": eth_address_for_rpc}, + timeout=20, ) return result From 33542a3e334198f5f59db6ac2bd752e67aaecf65 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Sun, 5 Jan 2025 11:11:40 +1100 Subject: [PATCH 045/138] fix: add timeout tracking to rpc usage tracker and log reasons to file --- api.py | 1 + fetcher.py | 1 + oxen/omq.py | 87 +++++++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 80 insertions(+), 9 deletions(-) diff --git a/api.py b/api.py index 5146209..d8c19b2 100644 --- a/api.py +++ b/api.py @@ -482,6 +482,7 @@ def bootstrap(): if config.backend.rpc_api_usage_logging: def log_rpc_usage(signum): app.rpc.usage_tracker.log_usage("Logging RPC usage for {}".format(signum)) + app.rpc.usage_tracker.write_failure_reasons_to_file(f"rpc-usage-failure-reasons-{signum}.txt") @timer(config.backend.rpc_api_usage_logging_interval, target="worker1") def log_rpc_usage_w1(signum): diff --git a/fetcher.py b/fetcher.py index 59f40d6..0c14954 100644 --- a/fetcher.py +++ b/fetcher.py @@ -215,6 +215,7 @@ def run(self): self.log.perf.end("loop") self.time_keeper.log_time_keeper() self.rpc.usage_tracker.log_usage() + self.rpc.usage_tracker.write_failure_reasons_to_file(f"rpc-usage-failure-reasons-fetcher.txt") now = time.time() arb_next_update = ( diff --git a/oxen/omq.py b/oxen/omq.py index 8a77a49..3674962 100644 --- a/oxen/omq.py +++ b/oxen/omq.py @@ -51,15 +51,20 @@ def __init__(self, enabled: bool, log: logging): self.log = log if enabled: self.log.warning("RPC usage tracking enabled. This is not recommended for production use.") + self.uses_executed = {} self.uses_success = {} self.uses_failed = {} self.uses_cached = {} + self.fail_reasons = {} + + self.add_executed = self.add_executed_enabled self.add_success = self.add_success_enabled self.add_failed = self.add_failed_enabled self.add_cached = self.add_cached_enabled self.log_usage = self.log_usage_enabled else: + self.add_executed = self._noop self.add_success = self._noop self.add_failed = self._noop self.add_cached = self._noop @@ -68,19 +73,29 @@ def __init__(self, enabled: bool, log: logging): def _noop(self, *args, **kwargs): pass + def add_executed_enabled(self, endpoint: str): + if endpoint not in self.uses_executed: + self.uses_executed[endpoint] = 0 + self.uses_executed[endpoint] += 1 + def add_success_enabled(self, endpoint: str): self.uses_success.setdefault(endpoint, []).append(datetime.now().timestamp()) - def add_failed_enabled(self, endpoint: str): + def add_failed_enabled(self, endpoint: str, reason: str): self.uses_failed.setdefault(endpoint, []).append(datetime.now().timestamp()) + self.fail_reasons.setdefault(endpoint, []).append(reason) def add_cached_enabled(self, endpoint: str): self.uses_cached.setdefault(endpoint, []).append(datetime.now().timestamp()) def log_usage_enabled(self, msg: str = ""): - msg += "\nRPC usage tracking: (s/f/c {}/{}/{})\n".format(len(self.uses_success), len(self.uses_failed), len(self.uses_cached)) unique_endpoints: dict[str, dict[str, list[float]]] = {} + g_executed = 0 + g_successes = 0 + g_failures = 0 + g_cached = 0 + for endpoint, timestamps in self.uses_success.items(): unique_endpoints.setdefault(endpoint, {"success": [], "failed": [], "cached": []}) unique_endpoints[endpoint]["success"].extend(timestamps) @@ -103,10 +118,35 @@ def log_usage_enabled(self, msg: str = ""): total_failure = len(stats_failed) total_cached = len(stats_cached) - total = total_success + total_failure + total_cached - if total == 0: + + g_successes += total_success + g_failures += total_failure + g_cached += total_cached + + total_executed = self.uses_executed.get(endpoint, 0) + g_executed += total_executed + + total_completed = total_success + total_failure + total_cached + if total_completed == 0: continue + success_rate = total_success / total_completed + failure_rate = total_failure / total_completed + cache_rate = total_cached / total_completed + + fail_reasons = self.fail_reasons.get(endpoint, []) + + fail_timeout = 0 + fail_other = 0 + for reason in fail_reasons: + if reason == "Timeout": + fail_timeout += 1 + else: + fail_other += 1 + + fail_timeout_rate = fail_timeout / total_failure if total_failure > 0 else 0 + fail_other_rate = fail_other / total_failure if total_failure > 0 else 0 + timestamps_all = stats_success + stats_failed + stats_cached timestamps_all.sort() @@ -120,7 +160,7 @@ def log_usage_enabled(self, msg: str = ""): current_time = datetime.now().timestamp() # ---- 1. Average RPS over all time ---- - rps_avg = total / total_time_seconds + rps_avg = total_completed / total_time_seconds # ---- 2. Average RPS in the last hour ---- one_hour_ago = current_time - 3600 @@ -146,12 +186,14 @@ def log_usage_enabled(self, msg: str = ""): (h_10m_sec, rps_peak_time_10m_sec, rps_peak_10m_sec) = bin_histogram_timestamps(last_10m_timestamps, bin_size_seconds=1) (h_10m_min, rps_peak_time_10m_min, rps_peak_10m_min) = bin_histogram_timestamps(last_10m_timestamps, bin_size_seconds=60) - log_lines.append(f"{endpoint} | Total: {total} ({total_success} success, {total_failure} failure, {total_cached} cached)") + log_lines.append(f"\n{endpoint} | {total_executed} executed | {total_completed} completed ({success_rate:.2%} success ({total_success}) | {failure_rate:.2%} failure ({total_failure}) | {cache_rate:.2%} cached ({total_cached}))") + log_lines.append(f"Timeout failures: {fail_timeout_rate:.2%} of failures ({fail_timeout} / {total_failure}))") + log_lines.append(f"Other failures: {fail_other_rate:.2%} of failures ({fail_other} / {total_failure}))") stats = [ ["Period", "#", "RPS", "Peak RPS (1s bin)", "Peak RPS Time (1s bin)", "Peak RPS (1m bin)", "Peak RPS Time (1m bin)"], - [f"Life ({total_time_seconds:.0f}s)", f"{total}", f"{rps_avg:.2f}", "", "", "", ""], + [f"Life ({total_time_seconds:.0f}s)", f"{total_completed}", f"{rps_avg:.2f}", "", "", "", ""], [f"< 1h", len(last_hour_timestamps), f"{rps_avg_last_hour:.2f}", f"{rps_peak_1h_sec:.2f}", rps_peak_time_1h_sec, f"{rps_peak_1h_min:.2f}", rps_peak_time_1h_min], [f"< 10m", len(last_10m_timestamps), f"{rps_avg_last_10_minutes:.2f}", f"{rps_peak_10m_sec:.2f}", @@ -163,7 +205,21 @@ def log_usage_enabled(self, msg: str = ""): # self.log.info(f" Requests in past {total} ({total_success} success, {total_failure} failure, {total_cached} cached)") # self.log.info(f" Avg requests per second: {rps_avg:.2f} avg (last hour: {rps_avg_last_hour:.2f}, last 10m: {rps_avg_last_10_minutes:.2f})") msg += ("\n".join(log_lines)) - self.log.info(msg) + + g_completed = g_successes + g_failures + g_cached + g_success_rate = g_successes / g_completed if g_completed > 0 else 0 + g_failure_rate = g_failures / g_completed if g_completed > 0 else 0 + g_cache_rate = g_cached / g_completed if g_completed > 0 else 0 + + header_msg = f"\nRPC usage tracking | {g_executed} executed | {g_completed} completed ({g_success_rate:.2%} success ({g_successes}) | {g_failure_rate:.2%} failure ({g_failures}) | {g_cache_rate:.2%} cached ({g_cached}))" + self.log.info(header_msg + msg) + + def write_failure_reasons_to_file(self, filename: str): + with open(filename, "w") as f: + for endpoint, timestamps in self.fail_reasons.items(): + f.write(f"{endpoint}\n") + for reason in timestamps: + f.write(f" {reason}\n") omq, oxend = None, None @@ -244,12 +300,12 @@ def get(self): """If the result is already available, returns it immediately (and can safely be called multiple times. Otherwise waits for the result, parses as json, and caches it. Returns None if the request fails """ + self.rpc_usage_tracker.add_executed(self.endpoint) if self.json is None and self.future is not None: try: result = self.future.get() self.future = None if result[0] != b"200": - self.rpc_usage_tracker.add_failed(self.endpoint) raise RuntimeError( "Request for {} failed: got {}".format(self.endpoint, result) ) @@ -265,6 +321,19 @@ def get(self): if not self.fail_okay: print("Something getting wrong: {}".format(e), file=sys.stderr) self.future = None + self.rpc_usage_tracker.add_failed(self.endpoint, e) + + except TimeoutError as e: + if not self.fail_okay: + print("Timeout: {}".format(e), file=sys.stderr) + self.future = None + self.rpc_usage_tracker.add_failed(self.endpoint, "Timeout") + + except Exception as e: + if not self.fail_okay: + print("Something getting wrong: {}".format(e), file=sys.stderr) + self.future = None + self.rpc_usage_tracker.add_failed(self.endpoint, e) else: self.rpc_usage_tracker.add_cached(self.endpoint) From 84b7bb26e3f80ce2db52f5f0c00342be7f54eee8 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Sun, 5 Jan 2025 11:12:18 +1100 Subject: [PATCH 046/138] feat: add contribution contract by sn key api endpoint --- api.py | 16 ++++++++++++++++ util/data.py | 4 ++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/api.py b/api.py index d8c19b2..2e7c7af 100644 --- a/api.py +++ b/api.py @@ -233,6 +233,22 @@ def get_open_contract_details(): {"contracts": get_contribution_contracts_cached()} ) +def get_contribution_contract_for_sn_pubkey_uncached(sn_pubkey: bytes): + cached_contracts = get_contribution_contracts_cached() + for contract in cached_contracts: + print(f"contract.service_node_pubkey: {contract.service_node_pubkey}") + print(f"sn_pubkey: {sn_pubkey}") + if contract.service_node_pubkey == sn_pubkey: + return contract + return None + +@app.route("/contract/contribution/") +def get_contribution_contract_for_sn_pubkey_cached(sn_pubkey: bytes): + key = sn_pubkey.hex() + return json_response( + {"contract": app.data.get("contract-sn-{}".format(key), getter=get_contribution_contract_for_sn_pubkey_uncached, getter_args=key, ttl=5)} + ) + def get_related_contribution_contracts_for_eth_address_uncached(eth_wal: str): contracts = get_contribution_contracts_cached() diff --git a/util/data.py b/util/data.py index 42b4761..e9a79d1 100644 --- a/util/data.py +++ b/util/data.py @@ -8,8 +8,8 @@ def __init__(self, stale_time_seconds: int = 0): self.cache_expiry = {} self.default_stale_time_seconds = stale_time_seconds - def get(self, key, getter=Optional[Callable], getter_args=None, ttl=0): - if ttl == 0: + def get(self, key, getter=Optional[Callable], getter_args=None, ttl=None): + if ttl is None or ttl < 0: ttl = self.default_stale_time_seconds now = time.time() if key in self.cache and self.cache_expiry[key] > now: From 705d31a92ab443031ceab98f54494f38d8acb22d Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Sun, 5 Jan 2025 12:11:46 +1100 Subject: [PATCH 047/138] feat: add service_node_rewards_contract_id_bls_key_map from contract to db and update cache times in api --- api.py | 79 ++++++++++++++++++++++++++++++--------------------- db/read.py | 16 +++++++++++ db/schema.sql | 9 ++++++ db/write.py | 39 +++++++++++++++++++++++++ fetcher.py | 20 ++++++++++++- util/data.py | 4 +-- 6 files changed, 132 insertions(+), 35 deletions(-) diff --git a/api.py b/api.py index 2e7c7af..dc0704d 100644 --- a/api.py +++ b/api.py @@ -96,15 +96,26 @@ def get_network_info_uncached(): network_info["median_operator_fee"] = get_median_operator_fee() return network_info +def get_next_block_timestamp_est(): + network_info = get_network_info_cached() + return network_info["pulse_target_timestamp"] + +def get_network_info_cached(): + return app.data.get("network_info", getter=get_network_info_uncached) + def json_response(vals): """ Takes a dict, adds some general info fields to it, and jsonifies it for a flask route function return value. The dict gets passed through `hexify` first to convert any bytes values to hex. + + Note: because network_info is cached, it can be called earlier in the route and both network_info + dict will be the same in both places, and basically guaranteed cached at this stage. """ hexify(vals) - network = app.data.get("network_info", getter=get_network_info_uncached) - return flask.jsonify({**vals, "network": network, "t": time.time()}) + + network_info = get_network_info_cached() + return flask.jsonify({**vals, "network": network_info, "t": time.time()}) @app.route("/info") @@ -115,18 +126,19 @@ def get_network_info(): def get_nodes_cached(): return app.data.get("nodes", getter=app.db_reader.get_nodes) +def get_nodes_response_uncached(): + return json_response({"nodes": get_nodes_cached()}) @app.route("/nodes") -def get_nodes(): - return json_response({"nodes": get_nodes_cached()}) +def route_get_nodes(): + return app.data.get("nodes_res", getter=get_nodes_response_uncached) -# TODO: Get from contract def get_nodes_bls_keys_uncached(): - return [node.pubkey_bls for node in get_nodes_cached()] + return json_response({"bls_keys": app.db_reader.get_service_node_rewards_contract_id_bls_key_map()}) @app.route("/nodes/bls") -def get_nodes_bls_keys(): - return json_response({"bls_keys": app.data.get("nodes_bls_keys", getter=get_nodes_bls_keys_uncached)}) +def route_get_nodes_bls_keys(): + return app.data.get("nodes_bls_keys_res", getter=get_nodes_bls_keys_uncached) """ ////////////////////////////////////////////////////////////// @@ -156,7 +168,7 @@ def get_related_stakes_for_eth_address_cached(address: ChecksumAddress): # TODO: might make sense to investigate storing contributor and operator addresses in the db as blobs and compare with bytes @app.route("/stakes/") @app.route("/nodes/") -def get_stakes_for_eth_address(eth_wal: str): +def route_get_stakes_for_eth_address(eth_wal: str): try: address = eth_format(eth_wal) return json_response({"stakes": get_related_stakes_for_eth_address_cached(address), "contracts": get_related_contribution_contracts_for_eth_address_cached(address)}) @@ -172,7 +184,7 @@ def get_stakes_for_eth_address(eth_wal: str): @app.route("/stakes/") @app.route("/nodes/") -def get_stakes_for_sn_pubkey(sn_pubkey: bytes): +def route_get_stakes_for_sn_pubkey(sn_pubkey: bytes): try: nodes = get_nodes_cached() related_nodes = [node for node in nodes if node.pubkey_ed25519 == sn_pubkey] @@ -196,32 +208,34 @@ def get_cached_allowed_contract_names(): return app.data.get( "allowed_contract_names", getter=get_and_refresh_allowed_contract_names, - ttl=config.backend.stale_time_seconds_contract_abis, + ttl=config.backend.stale_time_seconds_contract_abis ) @app.route("/contract/names") -def get_abi_names(): +def route_get_abi_names(): return json_response({"names": list(get_cached_allowed_contract_names())}) @app.route("/contract/abis") -def get_abis(): +def route_get_abis(): return json_response( - {"abis": app.data.get("abis", getter=app.db_reader.get_smart_contract_abis)} + {"abis": app.data.get("abis_all", getter=app.db_reader.get_smart_contract_abis, + ttl=config.backend.stale_time_seconds_contract_abis)} ) @app.route("/contract/addresses") def get_contract_addresses(): return json_response( - {"addresses": app.data.get("addresses", getter=app.db_reader.get_smart_contract_addresses)} + {"addresses": app.data.get("addresses_all", getter=app.db_reader.get_smart_contract_addresses)} ) @app.route("/contract/addresses/core") def get_contract_addresses_core(): return json_response( - {"addresses": app.data.get("addresses_core", getter=app.db_reader.get_smart_contract_addresses_core)} + {"addresses": app.data.get("addresses_core", getter=app.db_reader.get_smart_contract_addresses_core, + ttl=config.backend.stale_time_seconds_contract_abis)} ) def get_contribution_contracts_cached(): @@ -236,8 +250,6 @@ def get_open_contract_details(): def get_contribution_contract_for_sn_pubkey_uncached(sn_pubkey: bytes): cached_contracts = get_contribution_contracts_cached() for contract in cached_contracts: - print(f"contract.service_node_pubkey: {contract.service_node_pubkey}") - print(f"sn_pubkey: {sn_pubkey}") if contract.service_node_pubkey == sn_pubkey: return contract return None @@ -289,7 +301,8 @@ def get_abi(contract_name: str): return json_response( { "contract": app.data.get( - "abi", getter=app.db_reader.get_smart_contract_abi, getter_args=contract_name + "abi-{}".format(contract_name), getter=app.db_reader.get_smart_contract_abi, getter_args=contract_name, + ttl=config.backend.stale_time_seconds_contract_abis ) } ) @@ -303,7 +316,7 @@ def get_contract_address(contract_name: str): return json_response( { "address": app.data.get( - "address", + "address-{}".format(contract_name), getter=app.db_reader.get_smart_contract_address, getter_args=contract_name, ) @@ -350,7 +363,8 @@ def get_stake_events(contract_id: int): """ -def handle_get_exit_and_liquidation(ed25519_pubkey: bytes, liquidate: bool): +def handle_get_exit_and_liquidation(params: [bytes, bool]): + ed25519_pubkey, liquidate = params[0], params[1] try: response = app.rpc.bls_exit_liquidation_request(ed25519_pubkey, liquidate).get() if response is None: @@ -362,15 +376,18 @@ def handle_get_exit_and_liquidation(ed25519_pubkey: bytes, liquidate: bool): except TimeoutError: return flask.abort(408) # Request timeout +def handle_get_exit_and_liquidation_cached(params: [bytes, bool]): + return app.data.get(f"exit-{params[0]}-{params[1]}", getter=handle_get_exit_and_liquidation, getter_args=params, invalidate_timestamp=get_next_block_timestamp_est()) + @app.route("/exit/") def get_exit(ed25519_pubkey: bytes): - return handle_get_exit_and_liquidation(ed25519_pubkey, liquidate=False) + return handle_get_exit_and_liquidation_cached([ed25519_pubkey, False]) @app.route("/liquidation/") def get_liquidation(ed25519_pubkey: bytes): - return handle_get_exit_and_liquidation(ed25519_pubkey, liquidate=True) + return handle_get_exit_and_liquidation_cached([ed25519_pubkey, True]) def get_exit_liquidation_list_uncached(): @@ -379,7 +396,7 @@ def get_exit_liquidation_list_uncached(): @app.route("/exit_liquidation_list") def get_exit_liquidation_list(): return json_response( - {"result": app.data.get("exit_liquidation_list", getter=get_exit_liquidation_list_uncached)} + {"result": app.data.get("exit_liquidation_list", getter=get_exit_liquidation_list_uncached, invalidate_timestamp=get_next_block_timestamp_est())} ) @@ -403,16 +420,14 @@ def get_rewards(eth_wal: str): if flask.request.method == "GET": # We cache all rewards info for all wallets so we don't need to multiple reads in a short period of time - rewards_info = app.data.get(f"rewards_info", getter=app.db_reader.get_rewards_info) + rewards_info = app.data.get(f"rewards_info", getter=app.db_reader.get_rewards_info, invalidate_timestamp=get_next_block_timestamp_est()) return json_response({"rewards": rewards_info.get(address, 0)}) if flask.request.method == "POST": try: - response = app.data.get(f"rewards-sig-{address}", getter=get_rewards_signature_uncached, getter_args=address) - if "status" in response: - response.pop("status") - if "address" in response: - response.pop("address") + response = app.data.get(f"rewards-sig-{address}", getter=get_rewards_signature_uncached, getter_args=address, invalidate_timestamp=get_next_block_timestamp_est()) + response.pop("status") if "status" in response else None + response.pop("address") if "address" in response else None return json_response({"rewards": response}) except ValueError as e: return flask.abort(400, str(e)) @@ -480,7 +495,7 @@ def sn_pubkey_registrations(sn_pubkey: bytes) -> flask.Response: result = json_response( { "registrations": app.data.get( - f"sn-{sn_pubkey}", + f"registration-sn-{sn_pubkey}", getter=app.db_reader_registrations.get_registrations_by_pubkey, getter_args=sn_pubkey, ) @@ -497,7 +512,7 @@ def bootstrap(): if config.backend.rpc_api_usage_logging: def log_rpc_usage(signum): - app.rpc.usage_tracker.log_usage("Logging RPC usage for {}".format(signum)) + app.rpc.usage_tracker.log_usage(" For signum {}".format(signum)) app.rpc.usage_tracker.write_failure_reasons_to_file(f"rpc-usage-failure-reasons-{signum}.txt") @timer(config.backend.rpc_api_usage_logging_interval, target="worker1") diff --git a/db/read.py b/db/read.py index a3beda4..5610ed6 100644 --- a/db/read.py +++ b/db/read.py @@ -361,3 +361,19 @@ def get_arbitrum_events_for_stake_contrat_id(self, contract_id: int): self.log.perf.end("get_events_for_stake_contrat_id") return events + def get_service_node_rewards_contract_id_bls_key_map(self): + self.log.perf.start("get_service_node_rewards_contract_id_bls_key_map") + with closing(sqlite3.connect(self.db_path)) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute( + """ + SELECT contract_id, pubkey_bls FROM service_node_rewards_contract_id_bls_key_map + """ + ) + contract_id_map = { + pubkey_bls: contract_id + for contract_id, pubkey_bls in cursor.fetchall() + } + self.log.debug("Service node rewards contract id bls key map: {}".format(len(contract_id_map))) + self.log.perf.end("get_service_node_rewards_contract_id_bls_key_map") + return contract_id_map \ No newline at end of file diff --git a/db/schema.sql b/db/schema.sql index 919d716..f7a26cb 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -219,3 +219,12 @@ CREATE TABLE smart_contracts ( foreign key (name) references smart_contract_abis(name) ); + +CREATE TABLE service_node_rewards_contract_id_bls_key_map ( + contract_id INTEGER NOT NULL, + pubkey_bls BLOB NOT NULL, + + PRIMARY KEY (contract_id) +); + +CREATE INDEX service_node_rewards_contract_id_bls_key_map_pubkey_bls_idx ON service_node_rewards_contract_id_bls_key_map(pubkey_bls); diff --git a/db/write.py b/db/write.py index 68bc9a9..f38468b 100644 --- a/db/write.py +++ b/db/write.py @@ -696,3 +696,42 @@ def write_arbitrum_info_to_db(self, current_block, service_node_rewards_balance, connection.commit() self.log.perf.end("write_arbitrum_info_to_db") + + def write_service_node_rewards_contract_id_bls_key_map(self, contract_id_map: dict[str, str]): + self.log.perf.start("write_service_node_rewards_contract_id_bls_key_map") + with closing(sqlite3.connect(self.db_path)) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + self.log.debug("Inserting {} service node rewards contract ids".format(len(contract_id_map))) + self.log.perf.start("write_service_node_rewards_contract_id_bls_key_map -> insert contract ids") + + cursor.execute("DELETE FROM service_node_rewards_contract_id_bls_key_map") + + cursor.executemany( + """ + INSERT INTO service_node_rewards_contract_id_bls_key_map ( + contract_id, + pubkey_bls + ) + VALUES (?, ?) + """, + ( + ( + int(contract_id), + pubkey_bls, + ) + for pubkey_bls, contract_id in contract_id_map.items() + ), + ) + + inserted_contract_id_rows = cursor.rowcount + + self.log.perf.end("write_service_node_rewards_contract_id_bls_key_map -> insert contract ids") + self.log.debug( + "Inserted {} rows into service_node_rewards_contract_id_bls_key_map".format( + inserted_contract_id_rows + ) + ) + + connection.commit() + self.log.perf.end("write_service_node_rewards_contract_id_bls_key_map") \ No newline at end of file diff --git a/fetcher.py b/fetcher.py index 0c14954..438589a 100644 --- a/fetcher.py +++ b/fetcher.py @@ -296,6 +296,21 @@ def update_network_details_and_nodes( self.log.info("Scheduled task finish") self.log.perf.end("scheduled_task") + def fetch_service_node_rewards_contract_id_bls_key_map(self): + self.log.perf.start("fetch_service_node_rewards_contract_id_bls_key_map") + contract_id_map = {} + try: + contract_id_map = get_service_node_rewards_contract_id_map(self.service_node_rewards) + self.log.debug( + "Found {} service node rewards contract ids".format(len(contract_id_map)) + ) + except Exception as e: + self.log.error("Error fetching and parsing service node rewards contract id bls key map") + self.log.exception(e) + finally: + self.log.perf.end("fetch_service_node_rewards_contract_id_bls_key_map") + return contract_id_map + def fetch_service_node_list(self): self.log.perf.start("fetch_service_node_list") current_height = None @@ -312,7 +327,7 @@ def fetch_service_node_list(self): self.log.debug("Fetched {} service nodes".format(len(nodes))) # TODO: remove once contract_id is available via rpc.get_service_nodes - contract_id_map = get_service_node_rewards_contract_id_map(self.service_node_rewards) + contract_id_map = self.db_reader.get_service_node_rewards_contract_id_bls_key_map() for node in nodes: pubkey_bls = None @@ -484,6 +499,9 @@ def update_arbitrum_details(self): current_block = self.web3_client.web3.eth.block_number end_block = current_block - 1 + contract_id_map = self.fetch_service_node_rewards_contract_id_bls_key_map() + self.db_writer.write_service_node_rewards_contract_id_bls_key_map(contract_id_map) + service_node_rewards_balance = self.token_contract.balance_of(self.service_node_rewards.contract_address) reward_rate_pool_balance = self.token_contract.balance_of(self.reward_rate_pool.contract_address) self.log.debug("Arbitrum info: service node rewards balance {}, reward rate pool balance {}".format(service_node_rewards_balance, reward_rate_pool_balance)) diff --git a/util/data.py b/util/data.py index e9a79d1..feca69f 100644 --- a/util/data.py +++ b/util/data.py @@ -8,7 +8,7 @@ def __init__(self, stale_time_seconds: int = 0): self.cache_expiry = {} self.default_stale_time_seconds = stale_time_seconds - def get(self, key, getter=Optional[Callable], getter_args=None, ttl=None): + def get(self, key, getter=Optional[Callable], getter_args=None, ttl=None, invalidate_timestamp=None): if ttl is None or ttl < 0: ttl = self.default_stale_time_seconds now = time.time() @@ -17,5 +17,5 @@ def get(self, key, getter=Optional[Callable], getter_args=None, ttl=None): data = getter(getter_args) if getter_args is not None else getter() self.cache[key] = data - self.cache_expiry[key] = now + ttl + self.cache_expiry[key] = min(now + ttl, invalidate_timestamp) if invalidate_timestamp is not None else now + ttl return data From 293348f08d55e22027aba73d2e33922d710d50d8 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Sun, 5 Jan 2025 13:16:20 +1100 Subject: [PATCH 048/138] fix: increase timeout times for exit and rewards sig rpc calls --- oxen/rpc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/oxen/rpc.py b/oxen/rpc.py index a9dcd12..3e8cc23 100644 --- a/oxen/rpc.py +++ b/oxen/rpc.py @@ -97,7 +97,7 @@ def bls_rewards_request(self, eth_address: str) -> FutureJSON: result = self.FutureJSON( "rpc.bls_rewards_request", args={"address": eth_address_for_rpc}, - timeout=20, + timeout=30, ) return result @@ -105,6 +105,7 @@ def bls_exit_liquidation_request(self, ed25519_pubkey: bytes, liquidate: bool) - return self.FutureJSON( "rpc.bls_exit_liquidation_request", args={"pubkey": ed25519_pubkey.hex(), "liquidate": liquidate}, + timeout=30, ) def bls_exit_liquidation_list(self) -> FutureJSON: From f39084e35b149132e0df9caa9c6b3bd5f06391d5 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 6 Jan 2025 17:01:33 +1100 Subject: [PATCH 049/138] feat: create db snapshotter --- config_defaults.py | 8 ++++ db/snapshot.py | 102 +++++++++++++++++++++++++++++++++++++++++++++ snapshot.py | 49 ++++++++++++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 db/snapshot.py create mode 100644 snapshot.py diff --git a/config_defaults.py b/config_defaults.py index e652227..35a37f1 100644 --- a/config_defaults.py +++ b/config_defaults.py @@ -70,6 +70,14 @@ class Backend: web3_private_key: str | None = None web3_provider_urls: list[str] = ["http://localhost:8545"] # Default hardhat private chain node address) + """ + SNAPSHOT CONFIG + """ + snapshot_task_name: str = "snapshot" + sqlite_db_snapshot: str = "static/backend-snapshot.db" + sqlite_snapshot_time_interval_seconds: int = 600 + snapshot_on_startup: bool = False + # Session mainnet contracts mainnet_backend = Backend() diff --git a/db/snapshot.py b/db/snapshot.py new file mode 100644 index 0000000..e85439f --- /dev/null +++ b/db/snapshot.py @@ -0,0 +1,102 @@ +import os +import sqlite3 +from log import Log + + +class DBSnapshot: + def __init__(self, source_db_path: str, snapshot_db_path: str, excluded_tables: set[str] | None, log_level: int, + perf: bool = False): + assert source_db_path is not None and len(source_db_path) > 0 + assert snapshot_db_path is not None and len(snapshot_db_path) > 0 + + self.path_source = source_db_path + self.path_snapshot = snapshot_db_path + self.path_snapshot_tmp = snapshot_db_path + ".tmp" + self.excluded_tables = excluded_tables if excluded_tables is not None else set() + self.log = Log("db_snapshot", log_level, enable_perf=perf).logger + + os.makedirs(os.path.dirname(self.path_snapshot), exist_ok=True) + + self.log.info("Initializing snapshot task for DB. Source DB: {}, Snapshot DB: {}, Excluded tables: {}".format( + self.path_source, self.path_snapshot, self.excluded_tables)) + + def cleanup(self) -> None: + """ + Removes the temporary snapshot DB if it exists. + """ + self.log.perf.start("cleanup") + self.log.info("Cleaning up tmp snapshot database {}".format(self.path_snapshot_tmp)) + if os.path.exists(self.path_snapshot_tmp): + os.remove(self.path_snapshot_tmp) + self.log.perf.end("cleanup") + + def promote_tmp_db(self) -> None: + """ + Swaps the temporary snapshot DB with the final snapshot DB. + """ + self.log.perf.start("promote_tmp_db") + self.log.info("Swapping tmp snapshot database {} with final snapshot database {}".format(self.path_snapshot_tmp, + self.path_snapshot)) + if os.path.exists(self.path_snapshot): + os.remove(self.path_snapshot) + os.rename(self.path_snapshot_tmp, self.path_snapshot) + self.log.perf.end("promote_tmp_db") + + def snapshot(self) -> None: + """ + Creates all tables from source_db_path into backup_db_path, + but does NOT copy the row data for any tables listed in self.excluded_tables. + """ + + # Open (or create) the new backup DB + self.log.perf.start("backup_db") + self.log.info("Backing up database {} to {}".format(self.path_source, self.path_snapshot)) + + self.cleanup() + + with sqlite3.connect(self.path_snapshot_tmp) as backup_conn: + self.log.perf.start("backup_db_create_tables") + # Attach the source database so we can refer to it as 'old_db' + backup_conn.execute(f"ATTACH DATABASE '{self.path_source}' AS old_db;") + # Gather tables and their CREATE TABLE statements from the source + schema_query = """ + SELECT name, sql + FROM old_db.sqlite_master + WHERE type='table' + AND name NOT LIKE 'sqlite_%' -- skip internal or system tables + """ + tables = backup_conn.execute(schema_query).fetchall() + + # 1. Create all tables in the new DB + for table_name, create_sql in tables: + # Make sure there's a valid CREATE statement + if create_sql: + backup_conn.execute(create_sql) + + self.log.perf.end("backup_db_create_tables") + self.log.perf.start("backup_db_copy_rows") + + # 2. For each table, copy rows unless it's in tables_without_rows + for table_name, _ in tables: + if table_name not in self.excluded_tables: + # Insert rows from the source's table + insert_sql = f""" + INSERT INTO {table_name} + SELECT * FROM old_db.{table_name} + """ + backup_conn.execute(insert_sql) + else: + # We create the table above, but do NOT copy any rows for this table + print(f"Skipping rows for table: {table_name}") + + self.log.perf.end("backup_db_copy_rows") + + backup_conn.commit() + + # Detach the source database + backup_conn.execute("DETACH DATABASE old_db;") + + self.promote_tmp_db() + + self.log.info("Backed up database {} to {}".format(self.path_source, self.path_snapshot)) + self.log.perf.end("backup_db") diff --git a/snapshot.py b/snapshot.py new file mode 100644 index 0000000..618834b --- /dev/null +++ b/snapshot.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +import flask +import subprocess +from uwsgidecorators import timer +import config +from db.snapshot import DBSnapshot +from log import Log + + +class App(flask.Flask): + def __init__(self, name): + super().__init__(__name__) + log = Log(name, enable_perf=config.backend.performance_logging) + log.set_level(config.backend.log_level) + git_rev = subprocess.run( + ["git", "rev-parse", "--short=9", "HEAD"], stdout=subprocess.PIPE, text=True + ) + self.git_rev = git_rev.stdout.strip() if git_rev.returncode == 0 else "(unknown)" + + self.log = log.logger + + assert config.backend.sqlite_snapshot_time_interval_seconds > 60, "Snapshot interval must be greater than 60 seconds" + + self.db_snapshot = DBSnapshot( + source_db_path=config.backend.sqlite_db, + snapshot_db_path=config.backend.sqlite_db_snapshot, + excluded_tables={ + # Staging rows, these are committed to the main db once the immutable height is reached, this can be synced by the end user + "service_nodes_staging", + "service_nodes_contributions_staging", + }, + log_level=config.backend.log_level, + perf=config.backend.performance_logging, + ) + + self.log.info("Snapshot task initialized, will run every {} seconds.".format(config.backend.sqlite_snapshot_time_interval_seconds)) + + +app = App( + config.backend.snapshot_task_name if config.backend.snapshot_task_name else __name__ +) + + +@timer(config.backend.sqlite_snapshot_time_interval_seconds) +def snapshot_db(signum): + app.db_snapshot.snapshot() + +if config.backend.snapshot_on_startup: + snapshot_db(None) \ No newline at end of file From 44f1906a80d33da1d088585e761690c9bb263b7a Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Tue, 7 Jan 2025 15:15:40 +1100 Subject: [PATCH 050/138] fix: request timout in omq --- oxen/omq.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/oxen/omq.py b/oxen/omq.py index 3674962..86083ab 100644 --- a/oxen/omq.py +++ b/oxen/omq.py @@ -274,6 +274,8 @@ def __init__( timeout=10, rpc_usage_tracker: RPCUsageTracker = RPCUsageTracker(False, None), ): + if timeout is None: + timeout = 10 self.endpoint = endpoint self.cache_key = self.endpoint + cache_key self.fail_okay = fail_okay @@ -291,7 +293,7 @@ def __init__( self.json = None self.args = args self.future = omq.request_future( - oxend, self.endpoint, [] if self.args is None else [self.args], timeout=timeout + oxend, self.endpoint, [] if self.args is None else [self.args], request_timeout=timedelta(seconds=timeout) ) self.cache_seconds = cache_seconds self.rpc_usage_tracker = rpc_usage_tracker From 691557ef4a6283fc7c75111b63fae0e47c00cd89 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Tue, 7 Jan 2025 15:16:18 +1100 Subject: [PATCH 051/138] fix: only allow exit signature requests from exitable nodes and rewards signatures from addresses with rewards --- api.py | 113 +++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 85 insertions(+), 28 deletions(-) diff --git a/api.py b/api.py index dc0704d..164cde6 100644 --- a/api.py +++ b/api.py @@ -7,6 +7,8 @@ import subprocess from eth_typing import ChecksumAddress from uwsgidecorators import timer +from werkzeug.exceptions import GatewayTimeout + import config from db.read import DBReader from log import Log @@ -235,7 +237,7 @@ def get_contract_addresses(): def get_contract_addresses_core(): return json_response( {"addresses": app.data.get("addresses_core", getter=app.db_reader.get_smart_contract_addresses_core, - ttl=config.backend.stale_time_seconds_contract_abis)} + ttl=config.backend.stale_time_seconds_contract_abis )} ) def get_contribution_contracts_cached(): @@ -365,41 +367,66 @@ def get_stake_events(contract_id: int): def handle_get_exit_and_liquidation(params: [bytes, bool]): ed25519_pubkey, liquidate = params[0], params[1] + if ed25519_pubkey not in get_exitable_ed25519_keys_cached(): + return flask.abort(404, f"No exit available for {ed25519_pubkey.hex()}") + + response = app.rpc.bls_exit_liquidation_request(ed25519_pubkey, liquidate).get() + if response is None: + raise GatewayTimeout("Failed to get exit signature") + if "status" in response: + response.pop("status") + + return json_response({"result": response}) + + +def handle_get_exit_and_liquidation_cached(params: [bytes, bool]): try: - response = app.rpc.bls_exit_liquidation_request(ed25519_pubkey, liquidate).get() - if response is None: - return flask.abort(504) # Gateway timeout - if "status" in response: - response.pop("status") - result = json_response({"result": response}) - return result + return app.data.get(f"exit-{params[0]}-{params[1]}", getter=handle_get_exit_and_liquidation, getter_args=params, + invalidate_timestamp=get_next_block_timestamp_est()) + except GatewayTimeout as e: + app.logger.error(f"Exception: {e}") + return flask.abort(504) # Gateway timeout except TimeoutError: return flask.abort(408) # Request timeout - -def handle_get_exit_and_liquidation_cached(params: [bytes, bool]): - return app.data.get(f"exit-{params[0]}-{params[1]}", getter=handle_get_exit_and_liquidation, getter_args=params, invalidate_timestamp=get_next_block_timestamp_est()) + except Exception as e: + app.logger.error(f"Exception: {e}") + return flask.abort(500, e) @app.route("/exit/") -def get_exit(ed25519_pubkey: bytes): +def route_get_exit(ed25519_pubkey: bytes): return handle_get_exit_and_liquidation_cached([ed25519_pubkey, False]) @app.route("/liquidation/") -def get_liquidation(ed25519_pubkey: bytes): +def route_get_liquidation(ed25519_pubkey: bytes): return handle_get_exit_and_liquidation_cached([ed25519_pubkey, True]) def get_exit_liquidation_list_uncached(): return app.rpc.bls_exit_liquidation_list().get() + +def get_exit_liquidation_list_cached(): + return app.data.get("exit_liquidation_list", getter=get_exit_liquidation_list_uncached, + invalidate_timestamp=get_next_block_timestamp_est()) + + @app.route("/exit_liquidation_list") -def get_exit_liquidation_list(): +def route_get_exit_liquidation_list(): return json_response( - {"result": app.data.get("exit_liquidation_list", getter=get_exit_liquidation_list_uncached, invalidate_timestamp=get_next_block_timestamp_est())} + {"result": get_exit_liquidation_list_cached()} ) +def get_exitable_ed25519_keys_uncached(): + return set([bytes.fromhex(x.get("service_node_pubkey")) for x in get_exit_liquidation_list_cached()]) + + +def get_exitable_ed25519_keys_cached(): + return app.data.get("exitable_ed25519_keys", getter=get_exitable_ed25519_keys_uncached, + invalidate_timestamp=get_next_block_timestamp_est()) + """ ////////////////////////////////////////////////////////////// // // @@ -408,31 +435,61 @@ def get_exit_liquidation_list(): ////////////////////////////////////////////////////////////// """ -def get_rewards_signature_uncached(address: ChecksumAddress): + +def get_rewards_signature_uncached(eth_wal: str): + address = eth_format(eth_wal) response = app.rpc.bls_rewards_request(address).get() if response is None: raise TimeoutError("Failed to get rewards signature") + + response.pop("status") if "status" in response else None + response.pop("address") if "address" in response else None + return response -@app.route("/rewards/", methods=["GET", "POST"]) -def get_rewards(eth_wal: str): + +def get_rewards_info_cached(): + # We cache all rewards info for all wallets so we don't need to multiple reads in a short period of time + return app.data.get(f"rewards_info", getter=app.db_reader.get_rewards_info, + invalidate_timestamp=get_next_block_timestamp_est()) + + +def get_rewards_info_for_address_cached(eth_wal: str): address = eth_format(eth_wal) + rewards_info = get_rewards_info_cached() + return rewards_info.get(address, 0) + +def get_rewards_info_response(eth_wal: str): + return json_response({"rewards": get_rewards_info_for_address_cached(eth_wal)}) + + +def get_rewards_signature_response(eth_wal: str): + try: + rewards = get_rewards_info_for_address_cached(eth_wal) + if rewards == 0: + return flask.abort(404, f"No rewards available for {eth_wal}") + + return json_response({"rewards": app.data.get(f"rewards-sig-{eth_wal}", getter=get_rewards_signature_uncached, + getter_args=eth_wal, + invalidate_timestamp=get_next_block_timestamp_est())}) + except ValueError as e: + return flask.abort(400, str(e)) + + +@app.route("/rewards/", methods=["GET", "POST"]) +def get_rewards(eth_wal: str): if flask.request.method == "GET": - # We cache all rewards info for all wallets so we don't need to multiple reads in a short period of time - rewards_info = app.data.get(f"rewards_info", getter=app.db_reader.get_rewards_info, invalidate_timestamp=get_next_block_timestamp_est()) - return json_response({"rewards": rewards_info.get(address, 0)}) + return app.data.get(f"rewards-info-response-{eth_wal}", getter=get_rewards_info_response, getter_args=eth_wal, + invalidate_timestamp=get_next_block_timestamp_est()) if flask.request.method == "POST": try: - response = app.data.get(f"rewards-sig-{address}", getter=get_rewards_signature_uncached, getter_args=address, invalidate_timestamp=get_next_block_timestamp_est()) - response.pop("status") if "status" in response else None - response.pop("address") if "address" in response else None - return json_response({"rewards": response}) - except ValueError as e: - return flask.abort(400, str(e)) + return app.data.get(f"rewards-sig-response-{eth_wal}", getter=get_rewards_signature_response, + getter_args=eth_wal, invalidate_timestamp=get_next_block_timestamp_est()) except TimeoutError: - return flask.abort(408) # Request timeout + # We don't want to cache a 408 response + return flask.abort(408) return flask.abort(405) # Method not allowed From 9e8354bc47f2d53bd414b1ef954c7674a2a9fac1 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Thu, 9 Jan 2025 14:04:45 +1100 Subject: [PATCH 052/138] feat: add l2 info to network info --- api.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/api.py b/api.py index 164cde6..33c4921 100644 --- a/api.py +++ b/api.py @@ -92,14 +92,15 @@ def get_median_operator_fee(): def get_network_info_uncached(): network_info = app.db_reader.get_network_info() + arbitrum_info = app.db_reader.get_arbitrum_info() if network_info is None: return None network_info = dataclasses.asdict(network_info) network_info["median_operator_fee"] = get_median_operator_fee() - return network_info + return network_info, arbitrum_info def get_next_block_timestamp_est(): - network_info = get_network_info_cached() + network_info, arbitrum_info = get_network_info_cached() return network_info["pulse_target_timestamp"] def get_network_info_cached(): @@ -116,7 +117,9 @@ def json_response(vals): """ hexify(vals) - network_info = get_network_info_cached() + network_info, arbitrum_info = get_network_info_cached() + network_info["l2_height"] = arbitrum_info.block + network_info["l2_height_timestamp"] = arbitrum_info.timestamp return flask.jsonify({**vals, "network": network_info, "t": time.time()}) From 27f2873919daefa76ee5fdfc56a3cc454a91b3ed Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Thu, 9 Jan 2025 15:36:56 +1100 Subject: [PATCH 053/138] feat: add contract bls keys to stakes endpoint --- api.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/api.py b/api.py index 33c4921..a1f7ba8 100644 --- a/api.py +++ b/api.py @@ -138,12 +138,14 @@ def get_nodes_response_uncached(): def route_get_nodes(): return app.data.get("nodes_res", getter=get_nodes_response_uncached) -def get_nodes_bls_keys_uncached(): - return json_response({"bls_keys": app.db_reader.get_service_node_rewards_contract_id_bls_key_map()}) + +def get_nodes_bls_keys_cached(): + return app.data.get("contract_node_bls_keys_added", getter=app.db_reader.get_service_node_rewards_contract_id_bls_key_map) + @app.route("/nodes/bls") def route_get_nodes_bls_keys(): - return app.data.get("nodes_bls_keys_res", getter=get_nodes_bls_keys_uncached) + return json_response({"bls_keys": get_nodes_bls_keys_cached()}) """ ////////////////////////////////////////////////////////////// @@ -176,7 +178,7 @@ def get_related_stakes_for_eth_address_cached(address: ChecksumAddress): def route_get_stakes_for_eth_address(eth_wal: str): try: address = eth_format(eth_wal) - return json_response({"stakes": get_related_stakes_for_eth_address_cached(address), "contracts": get_related_contribution_contracts_for_eth_address_cached(address)}) + return json_response({"stakes": get_related_stakes_for_eth_address_cached(address), "contracts": get_related_contribution_contracts_for_eth_address_cached(address), "added_bls_keys": get_nodes_bls_keys_cached()}) except ValueError as e: app.logger.error(f"Exception: {e}") @@ -249,7 +251,7 @@ def get_contribution_contracts_cached(): @app.route("/contract/contribution") def get_open_contract_details(): return json_response( - {"contracts": get_contribution_contracts_cached()} + {"contracts": get_contribution_contracts_cached(), "added_bls_keys": get_nodes_bls_keys_cached()} ) def get_contribution_contract_for_sn_pubkey_uncached(sn_pubkey: bytes): From 85a642a384fcd71ccf0e30d3b6496ad9da5d0210 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Thu, 16 Jan 2025 10:17:32 +1100 Subject: [PATCH 054/138] fix: contract add timestamps and new token addresses --- config_defaults.py | 22 ++++++------ config_validate.py | 2 +- db/write.py | 4 +-- fetcher.py | 40 ++++++++++++++-------- registrations.py | 4 +++ web3client/abis/{SENT.json => Token.json} | 2 +- web3client/contracts/{sent.py => token.py} | 6 ++-- 7 files changed, 48 insertions(+), 32 deletions(-) rename web3client/abis/{SENT.json => Token.json} (99%) rename web3client/contracts/{sent.py => token.py} (82%) diff --git a/config_defaults.py b/config_defaults.py index 35a37f1..6a39536 100644 --- a/config_defaults.py +++ b/config_defaults.py @@ -1,4 +1,4 @@ -# Default configuration options for SENT staking website backend. +# Default configuration options for the session token staking website backend. # # To override settings add `config.whatever = ...` into `config.py`; this file should not be # modified and simply contains the default values. @@ -23,7 +23,7 @@ class Backend: log_level = logging.INFO log_level_generic = None # Logs from other packages will use log_level if this is not set oxen_wallet_regex: str = "" - sqlite_db: str = "sent-backend.db" + sqlite_db: str = "ssb.db" sqlite_schema: str = "db/schema.sql" rpc_shared: list[str] = "" rpc_shared_cache: int = 2 @@ -42,7 +42,7 @@ class Backend: registration_api_name: str = "registration_api" # NOTE: This can be the same DB as the main API, but you must manually run the registrations/schema.sql script in # the main db so it can be populated with the required tables. - registration_sqlite_db: str = "sent-backend-registrations.db" + registration_sqlite_db: str = "ssb-registrations.db" registration_sqlite_schema: str = "registration/schema.sql" """ @@ -52,7 +52,7 @@ class Backend: # Arbitrum runs at ~4 blocks per second, and the rpc node has a limit of 30m, so scan for 120 blocks arbitrum_rescan_safety_blocks: int = 120 addr_reward_rate_pool: str = "0x0000000000000000000000000000000000000000" - addr_sent: str = "0x0000000000000000000000000000000000000000" + addr_token: str = "0x0000000000000000000000000000000000000000" addr_sn_contrib: str = "0x0000000000000000000000000000000000000000" addr_sn_contrib_factory: str = "0x0000000000000000000000000000000000000000" addr_sn_rewards: str = "0x0000000000000000000000000000000000000000" @@ -104,14 +104,14 @@ class Backend: # Session stagenet.v3 contracts stagenet_backend = Backend() -stagenet_backend.addr_reward_rate_pool = "0x38cD8D3F93d591C18cf26B3Be4CB2c872aC37953" -stagenet_backend.addr_sent = "0x70c1f36C9cEBCa51B9344121D284D85BE36CD6bB" -stagenet_backend.addr_sn_contrib_factory = "0x66d0D4f71267b3150DafF7bD486AC5E097E7E4C6" -stagenet_backend.addr_sn_rewards = "0x4abfFB7f922767f22c7aa6524823d93FDDaB54b1" -stagenet_backend.oxen_wallet_regex = f"ST[{B58_ALPHABET}]{{95}}" -stagenet_backend.rpc_shared = "tcp://localhost:6786" -stagenet_backend.sqlite_db = "ssb-stagenet.db" stagenet_backend.web3_provider_urls = ["http://10.24.0.2/arb_sepolia"] +stagenet_backend.addr_reward_rate_pool = "0xaAD853fE7091728dac0DAa7b69990ee68abFC636" +stagenet_backend.addr_token = "0x7D7fD4E91834A96cD9Fb2369E7f4EB72383bbdEd" +stagenet_backend.addr_sn_contrib_factory = "0x36Ee2Da54a7E727cC996A441826BBEdda6336B71" +stagenet_backend.addr_sn_rewards = "0x9d8aB00880CBBdc2Dcd29C179779469A82E7be35" +stagenet_backend.oxen_wallet_regex = f"ST[{B58_ALPHABET}]{{95}}" +stagenet_backend.rpc_shared = "tcp://localhost:6786" +stagenet_backend.sqlite_db = "ssb-stagenet.db" # Assign the active backend to be used in the sent-staking-backend backend = stagenet_backend diff --git a/config_validate.py b/config_validate.py index c9b3123..0b70248 100644 --- a/config_validate.py +++ b/config_validate.py @@ -45,7 +45,7 @@ def validate_config(conf: config): # Assert all contract addresses are valid valid_address_assertion(conf.backend.addr_sn_contrib, "addr_sn_contrib") - valid_address_assertion(conf.backend.addr_sent, "addr_sent") + valid_address_assertion(conf.backend.addr_token, "addr_sent") valid_address_assertion(conf.backend.addr_sn_rewards, "addr_sn_rewards") valid_address_assertion(conf.backend.addr_reward_rate_pool, "addr_reward_rate_pool") diff --git a/db/write.py b/db/write.py index f38468b..ed85c0e 100644 --- a/db/write.py +++ b/db/write.py @@ -484,7 +484,7 @@ def write_arbitrum_events_to_db(self, events: list[ProcessedEvent]): self.log.perf.end("write_arbitrum_events_to_db") def write_contribution_contracts_to_db( - self, contracts: list[ContributionContractDetails], contributions_list: list, recent_add_node_event_timestamps: dict[str, int] + self, contracts: list[ContributionContractDetails], contributions_list: list, add_event_timestamps: dict[str, int] ): self.log.perf.start("write_contribution_contracts_to_db") @@ -513,7 +513,7 @@ def write_contribution_contracts_to_db( ( contract.address, contract.fee, - recent_add_node_event_timestamps.get(contract.pubkey_bls), + add_event_timestamps.get(contract.pubkey_bls), contract.operator_address, contract.pubkey_bls, contract.service_node_pubkey, diff --git a/fetcher.py b/fetcher.py index 438589a..2aa5e23 100644 --- a/fetcher.py +++ b/fetcher.py @@ -33,7 +33,7 @@ ServiceNodeContributionFactory, ) from web3client.contracts.service_node_rewards import ServiceNodeRewardsInterface -from web3client.contracts.sent import SENTInterface +from web3client.contracts.token import TokenInterface from oxen.omq import omq_connection @@ -97,7 +97,8 @@ def __init__(self, name): self.arbitrum_details_last_updated = 0 - self.arbitrum_node_add_events_bls_key_to_timestamp_map = {} + # looks like { pubkey_bls : { add: [], exit: []}} + self.arbitrum_node_events_bls_key_to_events_timestamps_map: dict[str, dict[str, list[int]]] = {} self.web3_client = Web3Client( provider_urls=config.backend.web3_provider_urls, @@ -107,8 +108,10 @@ def __init__(self, name): abi_manager=ABIManager(db_writer=self.db_writer, abi_dir=config.backend.abi_dir), ) - self.token_contract = SENTInterface( - web3_client=self.web3_client, contract_address=config.backend.addr_sent + self.log.info(f"Using contract addresses:\n Token: {config.backend.addr_token}\n SN Rewards: {config.backend.addr_sn_rewards}\n Reward Rate Pool: {config.backend.addr_reward_rate_pool}") + + self.token_contract = TokenInterface( + web3_client=self.web3_client, contract_address=config.backend.addr_token ) self.service_node_rewards = ServiceNodeRewardsInterface( web3_client=self.web3_client, @@ -547,10 +550,9 @@ def update_arbitrum_details(self): self.web3_client, self.log, contrib_contract_list ) - recent_add_node_event_timestamps = self.get_arbitrum_node_add_events_since_last_update() - + node_add_timestamps = self.update_arbitrum_node_event_timestamps() self.db_writer.write_contribution_contracts_to_db( - contract_details_list, contributions_list, recent_add_node_event_timestamps + contract_details_list, contributions_list, node_add_timestamps ) else: self.log.info("No contribution contracts to write to db") @@ -562,14 +564,24 @@ def update_arbitrum_details(self): self.log.error("Error fetching and parsing arbitrum details") self.log.exception(e) - def get_arbitrum_node_add_events_since_last_update(self): - recent_add_node_events = self.db_reader.get_arbitrum_events_since_timestamp([self.arbitrum_details_last_updated, ['NewServiceNodeV2']]) - for event in recent_add_node_events: - pubkey_bls_encoded = event.args.get("pubkey") - pubkey_bls = parse_bls_pubkey((pubkey_bls_encoded["X"], pubkey_bls_encoded["Y"])) - self.arbitrum_node_add_events_bls_key_to_timestamp_map["0x{}".format(pubkey_bls)] = event.timestamp - return self.arbitrum_node_add_events_bls_key_to_timestamp_map + def update_arbitrum_node_event_timestamps(self): + recent_node_events = self.db_reader.get_arbitrum_events_since_timestamp([self.arbitrum_details_last_updated, ['NewServiceNodeV2', 'ServiceNodeExit', 'ServiceNodeLiquidated']]) + for event in recent_node_events: + pubkey_bls_encoded = event.args.get("pubkey") + pubkey_bls = "0x{}".format(parse_bls_pubkey((pubkey_bls_encoded["X"], pubkey_bls_encoded["Y"]))) + if event.name == "NewServiceNodeV2": + self.arbitrum_node_events_bls_key_to_events_timestamps_map.setdefault(pubkey_bls, {}).setdefault("add", []).append(event.timestamp) + elif event.name == "ServiceNodeExit" or event.name == "ServiceNodeLiquidated": + self.arbitrum_node_events_bls_key_to_events_timestamps_map.setdefault(pubkey_bls, {}).setdefault("exit", []).append(event.timestamp) + + node_add_timestamps = {} + for pubkey_bls, event_timestamps in self.arbitrum_node_events_bls_key_to_events_timestamps_map.items(): + add_events = event_timestamps.get("add", []) + exit_events = event_timestamps.get("exit", []) + node_add_timestamps[pubkey_bls] = max(add_events) if len(add_events) > 0 and len(add_events) > len(exit_events) else None + + return node_add_timestamps app = App(config.backend.fetcher_name if config.backend.fetcher_name else __name__) diff --git a/registrations.py b/registrations.py index 13a9f74..b9cebdf 100644 --- a/registrations.py +++ b/registrations.py @@ -98,6 +98,9 @@ def get_network_info(): ////////////////////////////////////////////////////////////// """ +# NOTE: the /api prefix route here is to allow for local testing + +@app.route("/api/store/", methods=["GET", "POST"]) @app.route("/registrations/", methods=["POST"]) @app.route("/store/", methods=["GET", "POST"]) def store_registration(sn_pubkey: bytes): @@ -143,3 +146,4 @@ def store_registration(sn_pubkey: bytes): ) return json_response({"success": True, "registration": params}) + diff --git a/web3client/abis/SENT.json b/web3client/abis/Token.json similarity index 99% rename from web3client/abis/SENT.json rename to web3client/abis/Token.json index d2646c2..4e14626 100644 --- a/web3client/abis/SENT.json +++ b/web3client/abis/Token.json @@ -1,6 +1,6 @@ { "_format": "hh-sol-artifact-1", - "contractName": "SENT", + "contractName": "Token", "sourceName": "contracts/SENT.sol", "abi": [ { diff --git a/web3client/contracts/sent.py b/web3client/contracts/token.py similarity index 82% rename from web3client/contracts/sent.py rename to web3client/contracts/token.py index d1ea404..5a8abc1 100644 --- a/web3client/contracts/sent.py +++ b/web3client/contracts/token.py @@ -2,11 +2,11 @@ from web3client.contracts.contract import ContractInterface -class SENTInterface(ContractInterface): - abi_name = "SENT" +class TokenInterface(ContractInterface): + abi_name = "Token" def __init__(self, web3_client: Web3Client, contract_address: str): - super().__init__(web3_client, contract_address, SENTInterface.abi_name) + super().__init__(web3_client, contract_address, TokenInterface.abi_name) def transfer(self, amount: int, address_to: str): """ From d6ee872cd646af869879a46fb3de7e04996218c8 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Thu, 16 Jan 2025 10:18:01 +1100 Subject: [PATCH 055/138] fix: handle median node fee when no nodes --- api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.py b/api.py index a1f7ba8..5a100a6 100644 --- a/api.py +++ b/api.py @@ -87,7 +87,7 @@ def get_and_refresh_allowed_contract_names(): def get_median_operator_fee(): # remove nodes that only have a single contributor nodes = [n for n in get_nodes_cached() if len(n.contributors) > 1] - return statistics.median([n.operator_fee for n in nodes]) + return statistics.median([n.operator_fee for n in nodes]) if len(nodes) > 0 else 0 def get_network_info_uncached(): From 2c13ee3e076c63423aab9b7222fa8087297d4b4b Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Sat, 18 Jan 2025 18:17:33 +1100 Subject: [PATCH 056/138] fix: decrease ttl for contribution contracts --- api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api.py b/api.py index 5a100a6..26b5a07 100644 --- a/api.py +++ b/api.py @@ -246,7 +246,7 @@ def get_contract_addresses_core(): ) def get_contribution_contracts_cached(): - return app.data.get("contracts", getter=app.db_reader.get_contribution_contracts) + return app.data.get("contracts", getter=app.db_reader.get_contribution_contracts, ttl=2) @app.route("/contract/contribution") def get_open_contract_details(): @@ -255,7 +255,7 @@ def get_open_contract_details(): ) def get_contribution_contract_for_sn_pubkey_uncached(sn_pubkey: bytes): - cached_contracts = get_contribution_contracts_cached() + cached_contracts = app.db_reader.get_contribution_contracts() for contract in cached_contracts: if contract.service_node_pubkey == sn_pubkey: return contract @@ -265,7 +265,7 @@ def get_contribution_contract_for_sn_pubkey_uncached(sn_pubkey: bytes): def get_contribution_contract_for_sn_pubkey_cached(sn_pubkey: bytes): key = sn_pubkey.hex() return json_response( - {"contract": app.data.get("contract-sn-{}".format(key), getter=get_contribution_contract_for_sn_pubkey_uncached, getter_args=key, ttl=5)} + {"contract": app.data.get("contract-sn-{}".format(key), getter=get_contribution_contract_for_sn_pubkey_uncached, getter_args=key, ttl=2)} ) def get_related_contribution_contracts_for_eth_address_uncached(eth_wal: str): From 963e770eafd1419495b85a4fddda21bd0fd56f8c Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Sat, 18 Jan 2025 18:48:33 +1100 Subject: [PATCH 057/138] fix: decrease ttl for network info --- api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.py b/api.py index 26b5a07..3e0d489 100644 --- a/api.py +++ b/api.py @@ -104,7 +104,7 @@ def get_next_block_timestamp_est(): return network_info["pulse_target_timestamp"] def get_network_info_cached(): - return app.data.get("network_info", getter=get_network_info_uncached) + return app.data.get("network_info", getter=get_network_info_uncached, ttl=1) def json_response(vals): From adcdd2c248d2a8a08e798a9b70ac09c238798ea1 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Sat, 18 Jan 2025 19:02:37 +1100 Subject: [PATCH 058/138] feat: change arbitrum update loop to use goal refetch time instead of last refetch time --- fetcher.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/fetcher.py b/fetcher.py index 2aa5e23..afdd7a8 100644 --- a/fetcher.py +++ b/fetcher.py @@ -95,7 +95,7 @@ def __init__(self, name): self.loop_sleep_refresh_rate_seconds = rpc_cache if rpc_cache > 0 else 5 - self.arbitrum_details_last_updated = 0 + self.arbitrum_details_next_update_time = 0 # looks like { pubkey_bls : { add: [], exit: []}} self.arbitrum_node_events_bls_key_to_events_timestamps_map: dict[str, dict[str, list[int]]] = {} @@ -197,8 +197,7 @@ def run(self): ) if ( - time.time() - self.arbitrum_details_last_updated - > config.backend.refresh_rate_seconds_arbitrum + time.time() >= self.arbitrum_details_next_update_time ): self.time_keeper.add("arb_update") self.update_arbitrum_details() @@ -221,15 +220,11 @@ def run(self): self.rpc.usage_tracker.write_failure_reasons_to_file(f"rpc-usage-failure-reasons-fetcher.txt") now = time.time() - arb_next_update = ( - self.arbitrum_details_last_updated - + config.backend.refresh_rate_seconds_arbitrum - ) sleep_seconds = max( self.loop_sleep_refresh_rate_seconds, min( - arb_next_update, + self.arbitrum_details_next_update_time, network.pulse_target_timestamp, ) - now, @@ -241,10 +236,10 @@ def run(self): format_seconds(now + sleep_seconds, 0), ( "network_update" - if sleep_seconds == network.pulse_target_timestamp + if sleep_seconds == network.pulse_target_timestamp - now else ( "arb_update" - if sleep_seconds == arb_next_update - now + if sleep_seconds == self.arbitrum_details_next_update_time - now else "min_refresh" ) ), @@ -557,7 +552,7 @@ def update_arbitrum_details(self): else: self.log.info("No contribution contracts to write to db") - self.arbitrum_details_last_updated = time.time() + self.arbitrum_details_next_update_time = time.time() + config.backend.refresh_rate_seconds_arbitrum self.log.perf.end("update_arbitrum_details") except Exception as e: @@ -566,7 +561,7 @@ def update_arbitrum_details(self): def update_arbitrum_node_event_timestamps(self): - recent_node_events = self.db_reader.get_arbitrum_events_since_timestamp([self.arbitrum_details_last_updated, ['NewServiceNodeV2', 'ServiceNodeExit', 'ServiceNodeLiquidated']]) + recent_node_events = self.db_reader.get_arbitrum_events_since_timestamp([self.arbitrum_details_next_update_time, ['NewServiceNodeV2', 'ServiceNodeExit', 'ServiceNodeLiquidated']]) for event in recent_node_events: pubkey_bls_encoded = event.args.get("pubkey") pubkey_bls = "0x{}".format(parse_bls_pubkey((pubkey_bls_encoded["X"], pubkey_bls_encoded["Y"]))) From f9bebd1013b2c13c36a2322e8b182a1acf795158 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 20 Jan 2025 15:37:14 +1100 Subject: [PATCH 059/138] feat: add all contracts associated to an address to contribution contract endpoint --- api.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/api.py b/api.py index 3e0d489..2759617 100644 --- a/api.py +++ b/api.py @@ -254,18 +254,16 @@ def get_open_contract_details(): {"contracts": get_contribution_contracts_cached(), "added_bls_keys": get_nodes_bls_keys_cached()} ) -def get_contribution_contract_for_sn_pubkey_uncached(sn_pubkey: bytes): +def get_contribution_contracts_for_sn_pubkey_uncached(sn_pubkey: bytes): cached_contracts = app.db_reader.get_contribution_contracts() - for contract in cached_contracts: - if contract.service_node_pubkey == sn_pubkey: - return contract - return None + contracts = [contract for contract in cached_contracts if contract.service_node_pubkey == sn_pubkey] + return contracts @app.route("/contract/contribution/") def get_contribution_contract_for_sn_pubkey_cached(sn_pubkey: bytes): key = sn_pubkey.hex() return json_response( - {"contract": app.data.get("contract-sn-{}".format(key), getter=get_contribution_contract_for_sn_pubkey_uncached, getter_args=key, ttl=2)} + {"contracts": app.data.get("contract-sn-{}".format(key), getter=get_contribution_contracts_for_sn_pubkey_uncached, getter_args=key, ttl=2)} ) def get_related_contribution_contracts_for_eth_address_uncached(eth_wal: str): From 62e6af7a8fcbf330d35506de4f9e9ae2ce1d59ae Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Tue, 21 Jan 2025 14:57:14 +1100 Subject: [PATCH 060/138] chore: adjust cache logic for nodes and registrations --- api.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/api.py b/api.py index 2759617..c4cbae8 100644 --- a/api.py +++ b/api.py @@ -84,11 +84,13 @@ def get_and_refresh_allowed_contract_names(): app.url_map.converters["eth_wallet"] = EthConverter -def get_median_operator_fee(): +def get_median_operator_fee_uncached(): # remove nodes that only have a single contributor nodes = [n for n in get_nodes_cached() if len(n.contributors) > 1] return statistics.median([n.operator_fee for n in nodes]) if len(nodes) > 0 else 0 +def get_median_operator_fee_cached(): + return app.data.get("median_operator_fee", getter=get_median_operator_fee_uncached, ttl=3600) def get_network_info_uncached(): network_info = app.db_reader.get_network_info() @@ -96,7 +98,7 @@ def get_network_info_uncached(): if network_info is None: return None network_info = dataclasses.asdict(network_info) - network_info["median_operator_fee"] = get_median_operator_fee() + network_info["median_operator_fee"] = get_median_operator_fee_cached() return network_info, arbitrum_info def get_next_block_timestamp_est(): @@ -131,12 +133,10 @@ def get_network_info(): def get_nodes_cached(): return app.data.get("nodes", getter=app.db_reader.get_nodes) -def get_nodes_response_uncached(): - return json_response({"nodes": get_nodes_cached()}) @app.route("/nodes") def route_get_nodes(): - return app.data.get("nodes_res", getter=get_nodes_response_uncached) + return json_response({"nodes": get_nodes_cached()}) def get_nodes_bls_keys_cached(): @@ -524,7 +524,7 @@ def operator_registrations(operator: str): return json_response( { "registrations": app.data.get( - f"op-{operator_bytes}", + f"registrations-op-{operator_bytes}", getter=app.db_reader_registrations.get_registrations_for_operator, getter_args=operator_bytes, ) From 8d86aac179bf23db34c83578de5497458dc4cebc Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Tue, 21 Jan 2025 14:58:41 +1100 Subject: [PATCH 061/138] feat: add manual finalize data for contributor contracts and created and registered timestamps --- arbitrum.py | 9 +++-- config_defaults.py | 3 +- db/dataclasses.py | 3 ++ db/schema.sql | 3 ++ db/write.py | 10 ++++-- fetcher.py | 36 ++++++++++++------- .../contracts/service_node_contribution.py | 3 +- .../service_node_contribution_factory.py | 3 +- web3client/contracts/service_node_rewards.py | 3 +- web3client/event_scanner.py | 3 +- 10 files changed, 55 insertions(+), 21 deletions(-) diff --git a/arbitrum.py b/arbitrum.py index cd755ea..70ba8c9 100644 --- a/arbitrum.py +++ b/arbitrum.py @@ -39,6 +39,7 @@ def get_new_contribution_contracts( ] logger.perf.end("create_contribution_contract_instances") logger.debug("Found {} new contract events".format(len(events))) + logger.debug("Found {} new contracts".format(len(contracts))) logger.perf.end("get_new_contribution_contracts") return contracts, events @@ -47,6 +48,7 @@ def get_new_contribution_contracts( class ContributionContractDetails: address: str | None fee: int | None + manual_finalize: bool | None operator_address: str | None pubkey_bls: str | None service_node_pubkey: str | None @@ -128,14 +130,17 @@ def update_contribution_contract_details( status = responses[i + 4] + manual_finalize = responses[i + 5] + contract_details.append( ContributionContractDetails( address=contract_address, - service_node_pubkey=f"{params[0]:032x}", - service_node_signature=f"{params[1]:032x}{params[2]:032x}", fee=params[3], + manual_finalize=manual_finalize, operator_address=operator_address, pubkey_bls="0x{:0128x}".format((pubkey_bls_data[0] << 256) + pubkey_bls_data[1]), + service_node_pubkey=f"{params[0]:032x}", + service_node_signature=f"{params[1]:032x}{params[2]:032x}", status=status, ) ) diff --git a/config_defaults.py b/config_defaults.py index 6a39536..ac6f793 100644 --- a/config_defaults.py +++ b/config_defaults.py @@ -50,7 +50,8 @@ class Backend: """ abi_dir = "web3client/abis" # Arbitrum runs at ~4 blocks per second, and the rpc node has a limit of 30m, so scan for 120 blocks - arbitrum_rescan_safety_blocks: int = 120 + arbitrum_rescan_safety_blocks: int = 60 + arbitrum_scan_start_chunk_size: int = 20 addr_reward_rate_pool: str = "0x0000000000000000000000000000000000000000" addr_token: str = "0x0000000000000000000000000000000000000000" addr_sn_contrib: str = "0x0000000000000000000000000000000000000000" diff --git a/db/dataclasses.py b/db/dataclasses.py index 286862a..c34bdf4 100644 --- a/db/dataclasses.py +++ b/db/dataclasses.py @@ -114,7 +114,10 @@ def __post_init__(self): @dataclass class DBContributionContract: address: str + created_timestamp: int fee: int + last_added_timestamp: int + manual_finalize: bool node_add_timestamp: int operator_address: str pubkey_bls: str diff --git a/db/schema.sql b/db/schema.sql index f7a26cb..f51bea2 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -175,7 +175,10 @@ CREATE INDEX arbitrum_events_main_arg_idx ON arbitrum_events(main_arg, block DES CREATE TABLE contribution_contracts ( address TEXT NOT NULL, + created_timestamp INTEGER, fee INTEGER NOT NULL, + last_added_timestamp INTEGER, + manual_finalize BOOLEAN NOT NULL, node_add_timestamp INTEGER, operator_address TEXT NOT NULL, pubkey_bls BLOB NOT NULL, diff --git a/db/write.py b/db/write.py index ed85c0e..ff2c5e8 100644 --- a/db/write.py +++ b/db/write.py @@ -484,7 +484,7 @@ def write_arbitrum_events_to_db(self, events: list[ProcessedEvent]): self.log.perf.end("write_arbitrum_events_to_db") def write_contribution_contracts_to_db( - self, contracts: list[ContributionContractDetails], contributions_list: list, add_event_timestamps: dict[str, int] + self, contracts: list[ContributionContractDetails], contributions_list: list, add_event_timestamps: dict[str, int], node_last_added_timestamps: dict[str,int], create_contract_timestamps: dict[str, int] ): self.log.perf.start("write_contribution_contracts_to_db") @@ -499,7 +499,10 @@ def write_contribution_contracts_to_db( """ INSERT OR REPLACE INTO contribution_contracts ( address, + created_timestamp, fee, + last_added_timestamp, + manual_finalize, node_add_timestamp, operator_address, pubkey_bls, @@ -507,12 +510,15 @@ def write_contribution_contracts_to_db( service_node_signature, status ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( ( contract.address, + create_contract_timestamps.get(contract.address), contract.fee, + node_last_added_timestamps.get(contract.pubkey_bls), + contract.manual_finalize, add_event_timestamps.get(contract.pubkey_bls), contract.operator_address, contract.pubkey_bls, diff --git a/fetcher.py b/fetcher.py index afdd7a8..6ad308a 100644 --- a/fetcher.py +++ b/fetcher.py @@ -100,6 +100,9 @@ def __init__(self, name): # looks like { pubkey_bls : { add: [], exit: []}} self.arbitrum_node_events_bls_key_to_events_timestamps_map: dict[str, dict[str, list[int]]] = {} + # dict of contract address to timestamp of when it was created + self.contribution_contract_creation_timestamps: dict[str, int] = {} + self.web3_client = Web3Client( provider_urls=config.backend.web3_provider_urls, caller_address=config.backend.web3_caller_address, @@ -108,7 +111,7 @@ def __init__(self, name): abi_manager=ABIManager(db_writer=self.db_writer, abi_dir=config.backend.abi_dir), ) - self.log.info(f"Using contract addresses:\n Token: {config.backend.addr_token}\n SN Rewards: {config.backend.addr_sn_rewards}\n Reward Rate Pool: {config.backend.addr_reward_rate_pool}") + self.log.info(f"Using contract addresses:\n Token: {config.backend.addr_token}\n SN Rewards: {config.backend.addr_sn_rewards}\n Reward Rate Pool: {config.backend.addr_reward_rate_pool}\n SN Contribution Factory: {config.backend.addr_sn_contrib_factory}") self.token_contract = TokenInterface( web3_client=self.web3_client, contract_address=config.backend.addr_token @@ -117,6 +120,7 @@ def __init__(self, name): web3_client=self.web3_client, contract_address=config.backend.addr_sn_rewards, scanner_safety_blocks=config.backend.arbitrum_rescan_safety_blocks, + scan_start_chunk_size=config.backend.arbitrum_scan_start_chunk_size ) self.reward_rate_pool = RewardRatePoolInterface( web3_client=self.web3_client, contract_address=config.backend.addr_reward_rate_pool @@ -125,6 +129,7 @@ def __init__(self, name): web3_client=self.web3_client, contract_address=config.backend.addr_sn_contrib_factory, scanner_safety_blocks=config.backend.arbitrum_rescan_safety_blocks, + scan_start_chunk_size=config.backend.arbitrum_scan_start_chunk_size ) self.service_node_contribution = ServiceNodeContributionInterface( web3_client=self.web3_client, @@ -545,9 +550,9 @@ def update_arbitrum_details(self): self.web3_client, self.log, contrib_contract_list ) - node_add_timestamps = self.update_arbitrum_node_event_timestamps() + node_add_timestamps, node_last_added_timestamps = self.update_arbitrum_node_event_timestamps() self.db_writer.write_contribution_contracts_to_db( - contract_details_list, contributions_list, node_add_timestamps + contract_details_list, contributions_list, node_add_timestamps, node_last_added_timestamps, self.contribution_contract_creation_timestamps ) else: self.log.info("No contribution contracts to write to db") @@ -561,22 +566,29 @@ def update_arbitrum_details(self): def update_arbitrum_node_event_timestamps(self): - recent_node_events = self.db_reader.get_arbitrum_events_since_timestamp([self.arbitrum_details_next_update_time, ['NewServiceNodeV2', 'ServiceNodeExit', 'ServiceNodeLiquidated']]) + recent_node_events = self.db_reader.get_arbitrum_events_since_timestamp([self.arbitrum_details_next_update_time, ['NewServiceNodeV2', 'ServiceNodeExit', 'ServiceNodeLiquidated', 'NewServiceNodeContributionContract']]) for event in recent_node_events: - pubkey_bls_encoded = event.args.get("pubkey") - pubkey_bls = "0x{}".format(parse_bls_pubkey((pubkey_bls_encoded["X"], pubkey_bls_encoded["Y"]))) - if event.name == "NewServiceNodeV2": - self.arbitrum_node_events_bls_key_to_events_timestamps_map.setdefault(pubkey_bls, {}).setdefault("add", []).append(event.timestamp) - elif event.name == "ServiceNodeExit" or event.name == "ServiceNodeLiquidated": - self.arbitrum_node_events_bls_key_to_events_timestamps_map.setdefault(pubkey_bls, {}).setdefault("exit", []).append(event.timestamp) + if event.name == "NewServiceNodeContributionContract": + self.contribution_contract_creation_timestamps[event.main_arg] = event.timestamp + else: + pubkey_bls_encoded = event.args.get("pubkey") + pubkey_bls = "0x{}".format(parse_bls_pubkey((pubkey_bls_encoded["X"], pubkey_bls_encoded["Y"]))) + if event.name == "NewServiceNodeV2": + self.arbitrum_node_events_bls_key_to_events_timestamps_map.setdefault(pubkey_bls, {}).setdefault("add", []).append(event.timestamp) + elif event.name == "ServiceNodeExit" or event.name == "ServiceNodeLiquidated": + self.arbitrum_node_events_bls_key_to_events_timestamps_map.setdefault(pubkey_bls, {}).setdefault("exit", []).append(event.timestamp) + node_add_timestamps = {} + node_last_added_timestamps = {} for pubkey_bls, event_timestamps in self.arbitrum_node_events_bls_key_to_events_timestamps_map.items(): add_events = event_timestamps.get("add", []) exit_events = event_timestamps.get("exit", []) - node_add_timestamps[pubkey_bls] = max(add_events) if len(add_events) > 0 and len(add_events) > len(exit_events) else None + last_added_timestamp = max(add_events) if len(add_events) > 0 else None + node_last_added_timestamps[pubkey_bls] = last_added_timestamp + node_add_timestamps[pubkey_bls] = last_added_timestamp if len(add_events) > len(exit_events) else None - return node_add_timestamps + return node_add_timestamps, node_last_added_timestamps app = App(config.backend.fetcher_name if config.backend.fetcher_name else __name__) diff --git a/web3client/contracts/service_node_contribution.py b/web3client/contracts/service_node_contribution.py index f91b0f8..b84f858 100644 --- a/web3client/contracts/service_node_contribution.py +++ b/web3client/contracts/service_node_contribution.py @@ -103,7 +103,7 @@ def get_contributions(self): @staticmethod def add_details_fetch_to_batch_added_batches(): - return 5 + return 6 def add_details_fetch_to_batch(self, batch): batch.add(self.contract.functions.serviceNodeParams()) @@ -111,3 +111,4 @@ def add_details_fetch_to_batch(self, batch): batch.add(self.contract.functions.blsPubkey()) batch.add(self.contract.functions.getContributions()) batch.add(self.contract.functions.status()) + batch.add(self.contract.functions.manualFinalize()) diff --git a/web3client/contracts/service_node_contribution_factory.py b/web3client/contracts/service_node_contribution_factory.py index 2b80ea2..bcaf8e8 100644 --- a/web3client/contracts/service_node_contribution_factory.py +++ b/web3client/contracts/service_node_contribution_factory.py @@ -6,7 +6,7 @@ class ServiceNodeContributionFactory(ContractInterface): abi_name = "ServiceNodeContributionFactory" - def __init__(self, web3_client: Web3Client, contract_address: str, scanner_safety_blocks: int): + def __init__(self, web3_client: Web3Client, contract_address: str, scanner_safety_blocks: int, scan_start_chunk_size: int): super().__init__(web3_client, contract_address, ServiceNodeContributionFactory.abi_name) self.event_scanner = EventScanner( provider_url=web3_client.provider_url, @@ -18,4 +18,5 @@ def __init__(self, web3_client: Web3Client, contract_address: str, scanner_safet # and we are unlikely to exceed the response size limit of the JSON-RPC server max_chunk_scan_size=10_000_000, safety_blocks=scanner_safety_blocks, + optimal_chunk_size=scan_start_chunk_size ) diff --git a/web3client/contracts/service_node_rewards.py b/web3client/contracts/service_node_rewards.py index 2e9e445..ac43a2d 100644 --- a/web3client/contracts/service_node_rewards.py +++ b/web3client/contracts/service_node_rewards.py @@ -18,7 +18,7 @@ def __init__(self): class ServiceNodeRewardsInterface(ContractInterface): abi_name = "ServiceNodeRewards" - def __init__(self, web3_client: Web3Client, contract_address: str, scanner_safety_blocks: int): + def __init__(self, web3_client: Web3Client, contract_address: str, scanner_safety_blocks: int, scan_start_chunk_size: int): super().__init__(web3_client, contract_address, ServiceNodeRewardsInterface.abi_name) self.event_scanner = EventScanner( provider_url=web3_client.provider_url, @@ -34,6 +34,7 @@ def __init__(self, web3_client: Web3Client, contract_address: str, scanner_safet # and we are unlikely to exceed the response size limit of the JSON-RPC server max_chunk_scan_size=10_000_000, safety_blocks=scanner_safety_blocks, + optimal_chunk_size=scan_start_chunk_size ) def get_all_service_node_contract_ids(self): diff --git a/web3client/event_scanner.py b/web3client/event_scanner.py index c30f4d2..d380191 100644 --- a/web3client/event_scanner.py +++ b/web3client/event_scanner.py @@ -57,6 +57,7 @@ def __init__( max_request_retries: int = 5, request_retry_seconds: float = 5, safety_blocks: int = 10, + optimal_chunk_size: int = 20, ): """ :param events: List of web3 Event we scan @@ -93,7 +94,7 @@ def __init__( self.chunk_size_increase = 10.0 # Start low at 20, this value is set at the end of the scan so we can use the optimal chunk size in future scans - self.optimal_chunk_size = 20 + self.optimal_chunk_size = optimal_chunk_size def scan_chunk(self, start_block, end_block) -> Tuple[int, list]: """Read and process events between to block numbers. From 4aa4b234eae93fd4488f17f90a97fa52668e0048 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 27 Jan 2025 13:40:07 +1100 Subject: [PATCH 062/138] feat: add reserved slots data to contracts --- arbitrum.py | 15 ++++++- db/write.py | 6 ++- .../contracts/service_node_contribution.py | 39 +------------------ 3 files changed, 19 insertions(+), 41 deletions(-) diff --git a/arbitrum.py b/arbitrum.py index 70ba8c9..80b9e41 100644 --- a/arbitrum.py +++ b/arbitrum.py @@ -118,13 +118,24 @@ def update_contribution_contract_details( contributions_beneficiaries = contributions[1] contributions_amounts = contributions[2] + reserved = responses[i + 6] + reserved_addresses = reserved[0] + reserved_amounts = reserved[1] + + reserved_slots = {} + + for j in range(len(reserved_addresses)): + reserved_slots[reserved_addresses[j]] = reserved_amounts[j] + for j in range(len(contributions_addresses)): + address = contributions_addresses[j] contributions_list.append( { "contract_address": contract_address, - "address": contributions_addresses[j], + "address": address, "amount": contributions_amounts[j], "beneficiary_address": contributions_beneficiaries[j], + "reserved": reserved_slots.get(address, 0), } ) @@ -151,7 +162,7 @@ def update_contribution_contract_details( return contract_details, contributions_list -def get_block_timestamp(web3_client:Web3Client, block_num: int): +def get_block_timestamp(web3_client: Web3Client, block_num: int): """Get block timestamp""" try: block_info = web3_client.web3.eth.get_block(block_num) diff --git a/db/write.py b/db/write.py index ff2c5e8..1b3dec5 100644 --- a/db/write.py +++ b/db/write.py @@ -573,9 +573,10 @@ def write_contribution_contracts_to_db( address, amount, beneficiary_address, - contract_address + contract_address, + reserved ) - VALUES (?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?) """, ( ( @@ -583,6 +584,7 @@ def write_contribution_contracts_to_db( contribution["amount"], contribution["beneficiary_address"], contribution["contract_address"], + contribution["reserved"], ) for contribution in contributions_list ), diff --git a/web3client/contracts/service_node_contribution.py b/web3client/contracts/service_node_contribution.py index b84f858..d5126f2 100644 --- a/web3client/contracts/service_node_contribution.py +++ b/web3client/contracts/service_node_contribution.py @@ -65,45 +65,9 @@ def get_bls_pubkey(self): pks = self.contract.functions.blsPubkey().call() return "0x{:0128x}".format((pks[0] << 256) + pks[1]) - def get_service_node_params(self): - """ - Get the parameters of the service node. - :return: Dictionary containing service node parameters. - """ - params = self.contract.functions.serviceNodeParams().call() - return { - "serviceNodePubkey": f"{params[0]:032x}", - "serviceNodeSignature": f"{params[1]:032x}{params[2]:032x}", - "fee": params[3], - } - - def get_operator(self): - """ - returns the service node operator - """ - return self.contract.functions.operator().call() - - def get_contributions(self): - # (address[] memory addrs, address[] memory beneficiaries, uint256[] memory contribs) - contributions = self.contract.functions.getContributions().call() - addresses = contributions[0] - beneficiaries = contributions[1] - contributions = contributions[2] - - contributions_list = [] - for i in range(len(addresses)): - contributions_list.append( - { - "address": addresses[i], - "amount": contributions[i], - "beneficiary": beneficiaries[i], - } - ) - return contributions_list - @staticmethod def add_details_fetch_to_batch_added_batches(): - return 6 + return 7 def add_details_fetch_to_batch(self, batch): batch.add(self.contract.functions.serviceNodeParams()) @@ -112,3 +76,4 @@ def add_details_fetch_to_batch(self, batch): batch.add(self.contract.functions.getContributions()) batch.add(self.contract.functions.status()) batch.add(self.contract.functions.manualFinalize()) + batch.add(self.contract.functions.getReserved()) From c43b2018325c2287eec6403cc1e74ebe16165654 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 27 Jan 2025 13:41:19 +1100 Subject: [PATCH 063/138] feat: add more detailed cache controls to cache manager --- api.py | 99 ++++++++++++++++++++++++++---------------------- registrations.py | 4 +- util/cache.py | 58 ++++++++++++++++++++++++++++ util/data.py | 21 ---------- 4 files changed, 114 insertions(+), 68 deletions(-) create mode 100644 util/cache.py delete mode 100644 util/data.py diff --git a/api.py b/api.py index c4cbae8..23907b1 100644 --- a/api.py +++ b/api.py @@ -10,11 +10,12 @@ from werkzeug.exceptions import GatewayTimeout import config +from db.dataclasses import ArbitrumInfo, DBNetworkInfo from db.read import DBReader from log import Log from oxen.rpc import OxenRPC from registration.read import DBReaderRegistrations -from util.data import DataManager +from util.cache import Cache from util.parse import Hex64Converter, hexify, EthConverter, eth_format @@ -64,7 +65,7 @@ def __init__(self, name): self.loop_sleep_refresh_rate_seconds = rpc_cache if rpc_cache > 0 else 5 - self.data = DataManager(stale_time_seconds=config.backend.stale_time_seconds) + self.cache = Cache(stale_time_seconds=config.backend.stale_time_seconds) self.allowed_contract_names = set() @@ -90,13 +91,13 @@ def get_median_operator_fee_uncached(): return statistics.median([n.operator_fee for n in nodes]) if len(nodes) > 0 else 0 def get_median_operator_fee_cached(): - return app.data.get("median_operator_fee", getter=get_median_operator_fee_uncached, ttl=3600) + return app.cache.get("median_operator_fee", getter=get_median_operator_fee_uncached, ttl=3600) -def get_network_info_uncached(): +def get_network_info_uncached() -> tuple[dict | None, ArbitrumInfo]: network_info = app.db_reader.get_network_info() arbitrum_info = app.db_reader.get_arbitrum_info() if network_info is None: - return None + return None, arbitrum_info network_info = dataclasses.asdict(network_info) network_info["median_operator_fee"] = get_median_operator_fee_cached() return network_info, arbitrum_info @@ -106,10 +107,10 @@ def get_next_block_timestamp_est(): return network_info["pulse_target_timestamp"] def get_network_info_cached(): - return app.data.get("network_info", getter=get_network_info_uncached, ttl=1) + return app.cache.get("network_info", getter=get_network_info_uncached, ttl=1) -def json_response(vals): +def json_response(vals, include_network_info=True): """ Takes a dict, adds some general info fields to it, and jsonifies it for a flask route function return value. The dict gets passed through `hexify` first to convert any bytes values to hex. @@ -119,10 +120,15 @@ def json_response(vals): """ hexify(vals) - network_info, arbitrum_info = get_network_info_cached() - network_info["l2_height"] = arbitrum_info.block - network_info["l2_height_timestamp"] = arbitrum_info.timestamp - return flask.jsonify({**vals, "network": network_info, "t": time.time()}) + data = {**vals, "t": time.time()} + + if include_network_info: + network_info, arbitrum_info = get_network_info_cached() + network_info["l2_height"] = arbitrum_info.block + network_info["l2_height_timestamp"] = arbitrum_info.timestamp + data["network"] = network_info + + return flask.jsonify(data) @app.route("/info") @@ -131,7 +137,7 @@ def get_network_info(): def get_nodes_cached(): - return app.data.get("nodes", getter=app.db_reader.get_nodes) + return app.cache.get("nodes", getter=app.db_reader.get_nodes) @app.route("/nodes") @@ -140,7 +146,7 @@ def route_get_nodes(): def get_nodes_bls_keys_cached(): - return app.data.get("contract_node_bls_keys_added", getter=app.db_reader.get_service_node_rewards_contract_id_bls_key_map) + return app.cache.get("contract_node_bls_keys_added", getter=app.db_reader.get_service_node_rewards_contract_id_bls_key_map) @app.route("/nodes/bls") @@ -170,7 +176,7 @@ def get_related_stakes_for_eth_address_uncached(address: ChecksumAddress): return related_nodes def get_related_stakes_for_eth_address_cached(address: ChecksumAddress): - return app.data.get("related-stakes-{}".format(address), getter=get_related_stakes_for_eth_address_uncached, getter_args=address) + return app.cache.get("related-stakes-{}".format(address), getter=get_related_stakes_for_eth_address_uncached, getter_args=address) # TODO: might make sense to investigate storing contributor and operator addresses in the db as blobs and compare with bytes @app.route("/stakes/") @@ -212,7 +218,7 @@ def route_get_stakes_for_sn_pubkey(sn_pubkey: bytes): def get_cached_allowed_contract_names(): - return app.data.get( + return app.cache.get( "allowed_contract_names", getter=get_and_refresh_allowed_contract_names, ttl=config.backend.stale_time_seconds_contract_abis @@ -227,26 +233,26 @@ def route_get_abi_names(): @app.route("/contract/abis") def route_get_abis(): return json_response( - {"abis": app.data.get("abis_all", getter=app.db_reader.get_smart_contract_abis, - ttl=config.backend.stale_time_seconds_contract_abis)} + {"abis": app.cache.get("abis_all", getter=app.db_reader.get_smart_contract_abis, + ttl=config.backend.stale_time_seconds_contract_abis)} ) @app.route("/contract/addresses") def get_contract_addresses(): return json_response( - {"addresses": app.data.get("addresses_all", getter=app.db_reader.get_smart_contract_addresses)} + {"addresses": app.cache.get("addresses_all", getter=app.db_reader.get_smart_contract_addresses)} ) @app.route("/contract/addresses/core") def get_contract_addresses_core(): return json_response( - {"addresses": app.data.get("addresses_core", getter=app.db_reader.get_smart_contract_addresses_core, - ttl=config.backend.stale_time_seconds_contract_abis )} + {"addresses": app.cache.get("addresses_core", getter=app.db_reader.get_smart_contract_addresses_core, + ttl=config.backend.stale_time_seconds_contract_abis)} ) def get_contribution_contracts_cached(): - return app.data.get("contracts", getter=app.db_reader.get_contribution_contracts, ttl=2) + return app.cache.get("contracts", getter=app.db_reader.get_contribution_contracts, ttl=2) @app.route("/contract/contribution") def get_open_contract_details(): @@ -263,7 +269,7 @@ def get_contribution_contracts_for_sn_pubkey_uncached(sn_pubkey: bytes): def get_contribution_contract_for_sn_pubkey_cached(sn_pubkey: bytes): key = sn_pubkey.hex() return json_response( - {"contracts": app.data.get("contract-sn-{}".format(key), getter=get_contribution_contracts_for_sn_pubkey_uncached, getter_args=key, ttl=2)} + {"contracts": app.cache.get("contract-sn-{}".format(key), getter=get_contribution_contracts_for_sn_pubkey_uncached, getter_args=key, ttl=2)} ) def get_related_contribution_contracts_for_eth_address_uncached(eth_wal: str): @@ -280,7 +286,7 @@ def get_related_contribution_contracts_for_eth_address_uncached(eth_wal: str): return related_contracts def get_related_contribution_contracts_for_eth_address_cached(eth_wal: str): - return app.data.get("related-contracts-{}".format(eth_wal), getter=get_related_contribution_contracts_for_eth_address_uncached, getter_args=eth_wal) + return app.cache.get("related-contracts-{}".format(eth_wal), getter=get_related_contribution_contracts_for_eth_address_uncached, getter_args=eth_wal) @app.route("/contract/contribution/") def get_contribution_contracts_for_wallet(eth_wal: str): @@ -305,7 +311,7 @@ def get_abi(contract_name: str): return json_response( { - "contract": app.data.get( + "contract": app.cache.get( "abi-{}".format(contract_name), getter=app.db_reader.get_smart_contract_abi, getter_args=contract_name, ttl=config.backend.stale_time_seconds_contract_abis ) @@ -320,7 +326,7 @@ def get_contract_address(contract_name: str): return json_response( { - "address": app.data.get( + "address": app.cache.get( "address-{}".format(contract_name), getter=app.db_reader.get_smart_contract_address, getter_args=contract_name, @@ -339,7 +345,7 @@ def get_contract_address(contract_name: str): def get_events_handler(count_limit=500, skip=0): limit = min(count_limit, 500) - events, limit, skip, total = app.data.get("events-{}-{}".format(count_limit,skip), getter=app.db_reader.get_arbitrum_events_page, getter_args=[limit, skip], ttl=10) + events, limit, skip, total = app.cache.get("events-{}-{}".format(count_limit, skip), getter=app.db_reader.get_arbitrum_events_page, getter_args=[limit, skip], ttl=10) pagination = {"limit": limit, "skip": skip, "total": total} return {"events": events, "pagination": pagination} @@ -348,16 +354,19 @@ def get_events_handler(count_limit=500, skip=0): def get_events(count: int, skip: int): return json_response(get_events_handler(count, skip)) +def get_arbitrum_info_uncached(): + return app.db_reader.get_arbitrum_info() + @app.route("/arbitrum-info") def get_arbitrum_info(): - return json_response({"info": app.data.get("arbitrum-info", getter=app.db_reader.get_arbitrum_info)}) + return json_response({"info": app.cache.get("arbitrum-info", getter=get_arbitrum_info_uncached)}) @app.route("/stake-events/") def get_stake_events(contract_id: int): if contract_id < 0: return flask.abort(400, "Invalid contract ID") - return json_response({"events": app.data.get("stake-events-{}".format(contract_id), getter=app.db_reader.get_arbitrum_events_for_stake_contrat_id, getter_args=contract_id)}) + return json_response({"events": app.cache.get("stake-events-{}".format(contract_id), getter=app.db_reader.get_arbitrum_events_for_stake_contrat_id, getter_args=contract_id)}) """ ////////////////////////////////////////////////////////////// @@ -384,8 +393,8 @@ def handle_get_exit_and_liquidation(params: [bytes, bool]): def handle_get_exit_and_liquidation_cached(params: [bytes, bool]): try: - return app.data.get(f"exit-{params[0]}-{params[1]}", getter=handle_get_exit_and_liquidation, getter_args=params, - invalidate_timestamp=get_next_block_timestamp_est()) + return app.cache.get(f"exit-{params[0]}-{params[1]}", getter=handle_get_exit_and_liquidation, getter_args=params, + invalidate_timestamp=get_next_block_timestamp_est()) except GatewayTimeout as e: app.logger.error(f"Exception: {e}") return flask.abort(504) # Gateway timeout @@ -411,8 +420,8 @@ def get_exit_liquidation_list_uncached(): def get_exit_liquidation_list_cached(): - return app.data.get("exit_liquidation_list", getter=get_exit_liquidation_list_uncached, - invalidate_timestamp=get_next_block_timestamp_est()) + return app.cache.get("exit_liquidation_list", getter=get_exit_liquidation_list_uncached, + invalidate_timestamp=get_next_block_timestamp_est()) @app.route("/exit_liquidation_list") @@ -427,8 +436,8 @@ def get_exitable_ed25519_keys_uncached(): def get_exitable_ed25519_keys_cached(): - return app.data.get("exitable_ed25519_keys", getter=get_exitable_ed25519_keys_uncached, - invalidate_timestamp=get_next_block_timestamp_est()) + return app.cache.get("exitable_ed25519_keys", getter=get_exitable_ed25519_keys_uncached, + invalidate_timestamp=get_next_block_timestamp_est()) """ ////////////////////////////////////////////////////////////// @@ -453,8 +462,8 @@ def get_rewards_signature_uncached(eth_wal: str): def get_rewards_info_cached(): # We cache all rewards info for all wallets so we don't need to multiple reads in a short period of time - return app.data.get(f"rewards_info", getter=app.db_reader.get_rewards_info, - invalidate_timestamp=get_next_block_timestamp_est()) + return app.cache.get(f"rewards_info", getter=app.db_reader.get_rewards_info, + invalidate_timestamp=get_next_block_timestamp_est()) def get_rewards_info_for_address_cached(eth_wal: str): @@ -473,9 +482,9 @@ def get_rewards_signature_response(eth_wal: str): if rewards == 0: return flask.abort(404, f"No rewards available for {eth_wal}") - return json_response({"rewards": app.data.get(f"rewards-sig-{eth_wal}", getter=get_rewards_signature_uncached, - getter_args=eth_wal, - invalidate_timestamp=get_next_block_timestamp_est())}) + return json_response({"rewards": app.cache.get(f"rewards-sig-{eth_wal}", getter=get_rewards_signature_uncached, + getter_args=eth_wal, + invalidate_timestamp=get_next_block_timestamp_est())}) except ValueError as e: return flask.abort(400, str(e)) @@ -483,13 +492,13 @@ def get_rewards_signature_response(eth_wal: str): @app.route("/rewards/", methods=["GET", "POST"]) def get_rewards(eth_wal: str): if flask.request.method == "GET": - return app.data.get(f"rewards-info-response-{eth_wal}", getter=get_rewards_info_response, getter_args=eth_wal, - invalidate_timestamp=get_next_block_timestamp_est()) + return app.cache.get(f"rewards-info-response-{eth_wal}", getter=get_rewards_info_response, getter_args=eth_wal, + invalidate_timestamp=get_next_block_timestamp_est()) if flask.request.method == "POST": try: - return app.data.get(f"rewards-sig-response-{eth_wal}", getter=get_rewards_signature_response, - getter_args=eth_wal, invalidate_timestamp=get_next_block_timestamp_est()) + return app.cache.get(f"rewards-sig-response-{eth_wal}", getter=get_rewards_signature_response, + getter_args=eth_wal, invalidate_timestamp=get_next_block_timestamp_est()) except TimeoutError: # We don't want to cache a 408 response return flask.abort(408) @@ -523,7 +532,7 @@ def operator_registrations(operator: str): return json_response( { - "registrations": app.data.get( + "registrations": app.cache.get( f"registrations-op-{operator_bytes}", getter=app.db_reader_registrations.get_registrations_for_operator, getter_args=operator_bytes, @@ -554,7 +563,7 @@ def sn_pubkey_registrations(sn_pubkey: bytes) -> flask.Response: """ result = json_response( { - "registrations": app.data.get( + "registrations": app.cache.get( f"registration-sn-{sn_pubkey}", getter=app.db_reader_registrations.get_registrations_by_pubkey, getter_args=sn_pubkey, diff --git a/registrations.py b/registrations.py index b9cebdf..646839e 100644 --- a/registrations.py +++ b/registrations.py @@ -11,7 +11,7 @@ from registration.read import DBReaderRegistrations from registration.validation import check_reg_keys_sigs from registration.write import DBWriterRegistrations -from util.data import DataManager +from util.cache import Cache from util.parse import ( parse_query_params, byte_decoder, @@ -62,7 +62,7 @@ def __init__(self, name): perf=config.backend.performance_logging, ) - self.data = DataManager(stale_time_seconds=config.backend.stale_time_seconds) + self.cache = Cache(stale_time_seconds=config.backend.stale_time_seconds) self.allowed_contract_names = set() diff --git a/util/cache.py b/util/cache.py new file mode 100644 index 0000000..fce3195 --- /dev/null +++ b/util/cache.py @@ -0,0 +1,58 @@ +import logging +import time +from copy import copy +from typing import Optional, Callable + +from log import Log + + +class Cache: + def __init__(self, stale_time_seconds: int = 0, log_level: int = logging.INFO): + self.log = Log("data_manager", log_level).logger + self.default_stale_time_seconds = stale_time_seconds + self.store = {} + self.cache_expiry = {} + + def get(self, key, getter=Optional[Callable], getter_args=None, ttl=None, invalidate_timestamp=None): + if ttl is None or ttl < 0: + ttl = self.default_stale_time_seconds + now = time.time() + if key in self.store and self.cache_expiry[key] > now: + return self.store[key] + + self.clear_stale(now) + + data = getter(getter_args) if getter_args is not None else getter() + self.store[key] = data + self.cache_expiry[key] = min(now + ttl, invalidate_timestamp) if invalidate_timestamp is not None else now + ttl + return data + + def get_cached_only(self, key: str): + now = time.time() + if key in self.store and self.cache_expiry[key] > now: + return self.store[key] + return None + + def set_cache_value(self, key: str, data=None, ttl=None, invalidate_timestamp=None): + if ttl is None or ttl < 0: + ttl = self.default_stale_time_seconds + + now = time.time() + self.store[key] = data + self.cache_expiry[key] = min(now + ttl, invalidate_timestamp) if invalidate_timestamp is not None else now + ttl + + def set_expiry_ttl(self, key: str, ttl: int): + self.cache_expiry[key] = time.time() + ttl + + def set_expiry_timestamp(self, key: str, timestamp: int): + self.cache_expiry[key] = timestamp + + def get_stale_timestamp(self, key: str): + return self.cache_expiry[key] + + def clear_stale(self, now): + # NOTE: must be a copy as the dictionary is modified during iteration + for key, expiry in copy(self.cache_expiry).items(): + if expiry < now: + del self.store[key] + del self.cache_expiry[key] diff --git a/util/data.py b/util/data.py deleted file mode 100644 index feca69f..0000000 --- a/util/data.py +++ /dev/null @@ -1,21 +0,0 @@ -import time -from typing import Optional, Callable - - -class DataManager: - def __init__(self, stale_time_seconds: int = 0): - self.cache = {} - self.cache_expiry = {} - self.default_stale_time_seconds = stale_time_seconds - - def get(self, key, getter=Optional[Callable], getter_args=None, ttl=None, invalidate_timestamp=None): - if ttl is None or ttl < 0: - ttl = self.default_stale_time_seconds - now = time.time() - if key in self.cache and self.cache_expiry[key] > now: - return self.cache[key] - - data = getter(getter_args) if getter_args is not None else getter() - self.cache[key] = data - self.cache_expiry[key] = min(now + ttl, invalidate_timestamp) if invalidate_timestamp is not None else now + ttl - return data From c365304ef67fbf8154e2d74b76cb53bf2963944a Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 27 Jan 2025 13:42:22 +1100 Subject: [PATCH 064/138] feat: create flask rate limiter --- registrations.py | 13 ++++++++++ registrations_tests.py | 54 ++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 5 +++- util/flask.py | 43 +++++++++++++++++++++++++++++++++ 4 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 registrations_tests.py create mode 100644 util/flask.py diff --git a/registrations.py b/registrations.py index 646839e..8a07377 100644 --- a/registrations.py +++ b/registrations.py @@ -4,6 +4,10 @@ import eth_utils import subprocess + +from werkzeug.middleware.proxy_fix import ProxyFix +from util.flask import FlaskReqLimiter + import config from db.util import is_db_initialized, init_db @@ -66,11 +70,20 @@ def __init__(self, name): self.allowed_contract_names = set() + self.log.info(f"IP Rate limit: {config.backend.registration_api_rate_limit} per {config.backend.registration_api_rate_limit_period} seconds") + app = App( config.backend.registration_api_name if config.backend.registration_api_name else __name__ ) +# Enables more reliable proxy pass through for rate limiting +app.wsgi_app = ProxyFix(app.wsgi_app) +app.req_limiter = FlaskReqLimiter(max_reqs_per_sec=config.backend.registration_api_rate_limit, rate_limit_period=config.backend.registration_api_rate_limit_period) + +@app.before_request +def rate_limit(): + return app.req_limiter.rate_limit() app.url_map.converters["hex64"] = Hex64Converter app.url_map.converters["eth_wallet"] = EthConverter diff --git a/registrations_tests.py b/registrations_tests.py new file mode 100644 index 0000000..978fc58 --- /dev/null +++ b/registrations_tests.py @@ -0,0 +1,54 @@ +import time +import pytest +from registrations import app + + +@pytest.fixture() +def client(): + return app.test_client() + + +@pytest.mark.skip(reason="This is a utility function called by other tests") +def test_rate_limit(client, endpoint, method, reset=True, rate_limit=60, rate_limit_period=60): + if reset: + app.req_limiter.store = {} + app.req_limiter.max_reqs_per_sec = rate_limit + app.req_limiter.rate_limit_period = rate_limit_period + + req_fn = getattr(client, method) + + for _ in range(rate_limit): + r = req_fn(endpoint) + assert r.status_code == 200, "Responds with 200 when rate limit is not exceeded" + + r = req_fn(endpoint) + assert r.status_code == 429, "Responds with 429 when rate limit is exceeded" + + +def test_rate_limit_get(client): + test_rate_limit(client, "/info", "get") + + +def test_rate_limit_post(client): + test_rate_limit(client, f"/store/0000000000000000000000000000000000000000000000000000000000000000", "post") + + +def test_rate_limit_resets(client): + rate_limit_period = 1 + test_rate_limit(client, "/info", "get", rate_limit_period=rate_limit_period) + time.sleep(rate_limit_period) + test_rate_limit(client, "/info", "get", rate_limit_period=rate_limit_period, reset=False) + +def test_rate_limit_separate_ips(client): + test_rate_limit(client, "/info", "get") + + ip = list(app.req_limiter.store.keys())[0] + ip_info = app.req_limiter.store[ip] + + app.req_limiter.store = { + '1.1.1.1': ip_info + } + + test_rate_limit(client, "/info", "get", reset=False) + + assert len(app.req_limiter.store.keys()) == 2 diff --git a/requirements.txt b/requirements.txt index 330c771..eb40939 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,7 @@ eth-utils==5.0.0 Werkzeug==3.0.4 web3==7.2.0 eth-typing==5.0.0 -eth_abi==5.1.0 \ No newline at end of file +eth_abi==5.1.0 +requests==2.32.3 +attrs==24.2.0 +pytest==8.3.4 \ No newline at end of file diff --git a/util/flask.py b/util/flask.py new file mode 100644 index 0000000..4eb3a40 --- /dev/null +++ b/util/flask.py @@ -0,0 +1,43 @@ +import time + +import flask +from flask import request + + +class FlaskReqLimiter: + """ + Flask request limiter + + This is a simple request limiter that can be used to rate limit requests to a Flask app. + It uses a simple in-memory store to keep track of the number of requests per IP address. + + Usage: + ``` + from util.flask import FlaskReqLimiter + + app.req_limiter = FlaskReqLimiter(max_reqs_per_sec=100) + + @app.before_request + def rate_limit(): + return app.req_limiter.rate_limit() + ``` + """ + def __init__(self, max_reqs_per_sec: int = 100, rate_limit_period: int = 60): + self.store = {} + self.max_reqs_per_sec = max_reqs_per_sec + self.rate_limit_period = rate_limit_period + + + def rate_limit(self): + now = time.time() + ip_address = request.remote_addr + requests, expire = self.store.get(ip_address, (0, now + self.rate_limit_period)) + + if now > expire: + self.store[ip_address] = (1, now + self.rate_limit_period) + return + + if requests >= self.max_reqs_per_sec: + return flask.abort(429) + + self.store[ip_address] = (requests + 1, expire) \ No newline at end of file From a852cd47b8cab10a35d96e646ed1ddba5192142f Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 27 Jan 2025 13:43:06 +1100 Subject: [PATCH 065/138] fix: remove unused contract field in registrations db and enforce unique keys --- db/dataclasses.py | 2 -- registration/schema.sql | 3 +-- registration/write.py | 4 +--- registrations.py | 1 - 4 files changed, 2 insertions(+), 8 deletions(-) diff --git a/db/dataclasses.py b/db/dataclasses.py index c34bdf4..43972fc 100644 --- a/db/dataclasses.py +++ b/db/dataclasses.py @@ -177,7 +177,6 @@ class RewardsInfo: @dataclass class Registration: - contract: bytes | None operator: bytes pubkey_bls: bytes pubkey_ed25519: bytes @@ -186,7 +185,6 @@ class Registration: timestamp: float def __post_init__(self): - self.contract = self.contract.hex() if self.contract is not None else None self.operator = eth_format(self.operator) self.pubkey_bls = self.pubkey_bls.hex() self.pubkey_ed25519 = self.pubkey_ed25519.hex() diff --git a/registration/schema.sql b/registration/schema.sql index c947796..02109ae 100644 --- a/registration/schema.sql +++ b/registration/schema.sql @@ -2,7 +2,6 @@ PRAGMA journal_mode=WAL; CREATE TABLE registrations ( - contract BLOB, operator BLOB NOT NULL, pubkey_bls BLOB NOT NULL, pubkey_ed25519 BLOB NOT NULL, @@ -15,7 +14,7 @@ CREATE TABLE registrations ( CHECK(length(sig_ed25519) == 64), CHECK(length(sig_bls) == 128), CHECK(length(operator) == 20), - CHECK(contract IS NULL OR length(contract) == 20) + PRIMARY KEY(pubkey_bls, pubkey_ed25519) ); CREATE INDEX registrations_timestamp_idx ON registrations(timestamp DESC); diff --git a/registration/write.py b/registration/write.py index 4134e10..1c48573 100644 --- a/registration/write.py +++ b/registration/write.py @@ -21,17 +21,15 @@ def write_registration_to_db(self, registration): cursor.execute( """ INSERT OR REPLACE INTO registrations ( - contract, operator, pubkey_bls, pubkey_ed25519, sig_bls, sig_ed25519 ) - VALUES (?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?) """, ( - registration.get("contract"), registration.get("operator"), registration.get("pubkey_bls"), registration.get("pubkey_ed25519"), diff --git a/registrations.py b/registrations.py index 8a07377..56ebc1b 100644 --- a/registrations.py +++ b/registrations.py @@ -140,7 +140,6 @@ def store_registration(sn_pubkey: bytes): "pubkey_bls": byte_decoder(64), "sig_ed25519": byte_decoder(64), "sig_bls": byte_decoder(128), - "-contract": raw_eth_addr, "operator": raw_eth_addr, } ) From 5ff9b39bedbf2ccd5acad8858e052ad58f58ade3 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 27 Jan 2025 13:44:07 +1100 Subject: [PATCH 066/138] fix: performance logger orphan cleanup maths --- log/perf.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/log/perf.py b/log/perf.py index 98833f6..4e19cf1 100644 --- a/log/perf.py +++ b/log/perf.py @@ -1,5 +1,6 @@ import logging import time +from copy import copy class PerformanceLogger: @@ -11,7 +12,7 @@ def __init__(self, logger: logging = None, enabled=True): self.end_timer = self.end_timer_enabled self.times = {} self.cpu_times = {} - self.orphaned_event_age_seconds = 3600 # 1 hour + self.orphaned_event_age_ns = 3600 * 10**9 # 1 hour self.check_for_orphans_interval = 3600 # 1 hour self.last_orphan_prune = 0 else: @@ -32,10 +33,12 @@ def end_timer_enabled(self, label): start_time = self.times.pop(label, None) start_time_cpu = self.cpu_times.pop(label, None) - self._cleanup_orphans() if start_time is not None and start_time_cpu is not None: - elapsed_ms = (time.perf_counter_ns() - start_time) / 1e6 + process_time_ns = time.perf_counter_ns() + self._cleanup_orphans(process_time_ns) + + elapsed_ms = (process_time_ns - start_time) / 1e6 elapsed_cpu_ms = (time.process_time_ns() - start_time_cpu) / 1e6 return elapsed_ms, elapsed_cpu_ms return None, None @@ -51,12 +54,13 @@ def _log_end(self, label, elapsed_ms, elapsed_cpu_ms): else: self.logger.performance(f"No start time recorded for label '{label}'") - def _cleanup_orphans(self): - now = time.time() - if len(self.times) > 0 and now - self.last_orphan_prune > self.check_for_orphans_interval: - self.last_orphan_prune = now - for label, start in self.times.items(): - if now - start > self.orphaned_event_age_seconds: + def _cleanup_orphans(self, process_time_ns): + if len(self.times) > 0 and process_time_ns - self.last_orphan_prune > self.check_for_orphans_interval: + self.last_orphan_prune = process_time_ns + + # NOTE: must be a copy as the dictionary is modified during iteration + for label, start in copy(self.times).items(): + if process_time_ns > start + self.orphaned_event_age_ns: self.times.pop(label) def _noop(self, *args, **kwargs): From e6a28195a1e4c9e3e9bb75b200c5dcabe43b6c91 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 27 Jan 2025 13:54:32 +1100 Subject: [PATCH 067/138] chore: add rate limits --- config_defaults.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/config_defaults.py b/config_defaults.py index ac6f793..2152727 100644 --- a/config_defaults.py +++ b/config_defaults.py @@ -39,11 +39,14 @@ class Backend: """ REGISTRATION CONFIG """ - registration_api_name: str = "registration_api" + registration_api_name: str = "registration_api" # NOTE: This can be the same DB as the main API, but you must manually run the registrations/schema.sql script in # the main db so it can be populated with the required tables. - registration_sqlite_db: str = "ssb-registrations.db" - registration_sqlite_schema: str = "registration/schema.sql" + registration_sqlite_db: str = "ssb-registrations.db" + registration_sqlite_schema: str = "registration/schema.sql" + # Creates a request per period limit by IP address (default is 100 requests per hour) + registration_api_rate_limit: int = 100 + registration_api_rate_limit_period: int = 3600 """ FETCHER CONFIG From 82c3e4e82d292f9dba7d732668e4fc837d476015 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 27 Jan 2025 13:56:06 +1100 Subject: [PATCH 068/138] chore: cleanup v1 backend and start scripts --- __init__.py | 0 api.py | 8 + devnet.py | 4 +- mainnet.py | 3 +- sent.py | 2113 --------------------------------------------------- testnet.py | 4 +- 6 files changed, 11 insertions(+), 2121 deletions(-) create mode 100644 __init__.py delete mode 100644 sent.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api.py b/api.py index 23907b1..30012d7 100644 --- a/api.py +++ b/api.py @@ -572,6 +572,14 @@ def sn_pubkey_registrations(sn_pubkey: bytes) -> flask.Response: ) return result +""" +////////////////////////////////////////////////////////////// +// // +// Utility // +// // +////////////////////////////////////////////////////////////// +""" + def bootstrap(): get_and_refresh_allowed_contract_names() diff --git a/devnet.py b/devnet.py index 4cec498..f45a157 100644 --- a/devnet.py +++ b/devnet.py @@ -1,6 +1,4 @@ -from sent import app, config -import oxenmq +from api import app, config config.devnet = True - config.oxend_rpc = 'ipc://oxend/devnet.sock' diff --git a/mainnet.py b/mainnet.py index 259348a..db7787e 100644 --- a/mainnet.py +++ b/mainnet.py @@ -1,4 +1,3 @@ -from sent import app, config -import oxenmq +from api import app, config config.oxend_rpc = 'ipc://oxend/mainnet.sock' diff --git a/sent.py b/sent.py deleted file mode 100644 index 8066967..0000000 --- a/sent.py +++ /dev/null @@ -1,2113 +0,0 @@ -#!/usr/bin/env python3 -from time import perf_counter - -import asyncio -from concurrent.futures import ThreadPoolExecutor -import uuid - -import flask -import string -import time -import oxenc -import sqlite3 -import re -import nacl.hash -import nacl.bindings as sodium -import eth_utils -import subprocess -import config -import datetime - -from itertools import chain -from eth_typing import ChecksumAddress -from typing import TypedDict, Callable, Any, Union -from functools import partial -from werkzeug.routing import BaseConverter -from nacl.signing import VerifyKey -from omq import FutureJSON, omq_connection -from timer import timer - -from contracts.reward_rate_pool import RewardRatePoolInterface -from contracts.service_node_contribution import ContributorContractInterface -from contracts.service_node_contribution_factory import ServiceNodeContributionFactory -from contracts.service_node_rewards import ServiceNodeRewardsInterface, ServiceNodeRewardsRecipient - -TOKEN_NAME = "SENT" - -class WalletInfo(): - def __init__(self): - self.rewards = 0 # Atomic SENT - self.contract_rewards = 0 - self.contract_claimed = 0 - -def oxen_rpc_get_accrued_rewards(omq, oxend) -> FutureJSON: - result = FutureJSON(omq, oxend, 'rpc.get_accrued_rewards', args={'addresses': []}) - return result - -def oxen_rpc_bls_rewards_request(omq, oxend, eth_address: str) -> FutureJSON: - eth_address_for_rpc = eth_address.lower() - if eth_address_for_rpc.startswith("0x"): - eth_address_for_rpc = eth_address_for_rpc[2:] - result = FutureJSON(omq, oxend, 'rpc.bls_rewards_request', args={'address': eth_address_for_rpc}) - return result - -def oxen_rpc_bls_exit_liquidation(omq, oxend, ed25519_pubkey: bytes, liquidate: bool) -> FutureJSON: - return FutureJSON(omq, oxend, 'rpc.bls_exit_liquidation_request', args={'pubkey': ed25519_pubkey.hex(), 'liquidate': liquidate}) - -def get_oxen_rpc_bls_exit_liquidation_list(omq, oxend): - return FutureJSON(omq, oxend, 'rpc.bls_exit_liquidation_list') - -class App(flask.Flask): - def __init__(self): - super().__init__(__name__) - self.logger.setLevel(config.backend.log_level) - - self.service_node_rewards = ServiceNodeRewardsInterface(config.backend.provider_url, config.backend.sn_rewards_addr) - self.reward_rate_pool = RewardRatePoolInterface(config.backend.provider_url, config.backend.reward_rate_pool_addr) - self.service_node_contribution_factory = ServiceNodeContributionFactory(config.backend.provider_url, config.backend.sn_contrib_factory_addr) - self.service_node_contribution = ContributorContractInterface(config.backend.provider_url) - - self.bls_pubkey_to_contract_id_map: dict[str, int] = {} # (BLS public key -> contract_id) - - self.wallet_to_sn_map: dict[ChecksumAddress, set[int]] = {} # (0x wallet address -> Set of contract_id's of stakes they are contributors to) - self.contract_id_to_sn_map: dict[int, dict] = {} # (contract_id -> Oxen RPC get_service_nodes result (augmented w/ extra metadata like SN contract ID)) - - self.wallet_to_exitable_sn_map: dict[ChecksumAddress, set[int]] = {} # (0x wallet address -> Set of contract_id's of SN's they can liquidate/exit) - self.contract_id_to_exitable_sn_map: dict[int, dict] = {} # (contract_id -> SNInfo) - - self.tmp_db_trigger_wallet_addresses: set[ChecksumAddress] = set() # Wallet addresses that have triggered a db get between scheduled times - self.tracked_wallet_addresses: set[ChecksumAddress] = set() # Tracked wallet addresses to fetch data from the db for - self.wallet_to_historical_stakes_map: dict[ChecksumAddress, set[int]] = {} # (0x wallet address -> Set of contract_ids) - self.contract_id_to_historical_stakes_map: dict[int, Stake] = {} # (contract_id -> Stake Info) - - - self.contributors = {} - - self.contracts_stale_timestamp = 0 - self.contracts = None - - self.wallet_map = {} # (Binary ETH wallet address -> WalletInfo) - git_rev = subprocess.run(["git", "rev-parse", "--short=9", "HEAD"], stdout=subprocess.PIPE, text=True) - self.git_rev = git_rev.stdout.strip() if git_rev.returncode == 0 else "(unknown)" - - sql = sqlite3.connect(config.backend.sqlite_db) - cursor = sql.cursor() - cursor.execute("PRAGMA journal_mode=WAL") - cursor.execute("PRAGMA foreign_keys=ON") - - cursor.execute(""" - CREATE TABLE IF NOT EXISTS registrations ( - id INTEGER PRIMARY KEY NOT NULL, - pubkey_ed25519 BLOB NOT NULL, - pubkey_bls BLOB NOT NULL, - sig_ed25519 BLOB NOT NULL, - sig_bls BLOB NOT NULL, - operator BLOB NOT NULL, - contract BLOB, - timestamp FLOAT NOT NULL DEFAULT ((julianday('now') - 2440587.5)*86400.0), /* unix epoch */ - - CHECK(length(pubkey_ed25519) == 32), - CHECK(length(pubkey_bls) == 64), - CHECK(length(sig_ed25519) == 64), - CHECK(length(sig_bls) == 128), - CHECK(length(operator) == 20), - CHECK(contract IS NULL OR length(contract) == 20) - ) - """) - - cursor.execute(""" - CREATE INDEX IF NOT EXISTS registrations_operator_idx ON registrations(operator); - """) - - cursor.execute(""" - CREATE UNIQUE INDEX IF NOT EXISTS registration_pk_multi_idx ON registrations(pubkey_ed25519, contract IS NULL); - """) - - cursor.execute(""" - CREATE TABLE IF NOT EXISTS contribution_contracts ( - contract_address BLOB PRIMARY KEY NOT NULL, - pubkey_ed25519 BLOB NOT NULL, - pubkey_bls BLOB NOT NULL, - sig_ed25519 BLOB NOT NULL, - operator_address TEXT NOT NULL, - fee INTEGER NOT NULL, - status INTEGER NOT NULL, - total_contributions INTEGER NOT NULL DEFAULT 0, - - timestamp FLOAT NOT NULL DEFAULT ((julianday('now') - 2440587.5)*86400.0), /* unix epoch */ - - CHECK(length(contract_address) == 20) - ); - """) - - cursor.execute(""" - CREATE UNIQUE INDEX IF NOT EXISTS contribution_contract_address_idx ON contribution_contracts(contract_address); - """) - - cursor.execute(""" - CREATE TABLE IF NOT EXISTS contribution_contracts_contributions ( - contract_address BLOB NOT NULL, - address BLOB NOT NULL, - beneficiary_address BLOB NOT NULL, - amount INTEGER NOT NULL, - reserved INTEGER, - - CHECK(length(address) == 20), - - FOREIGN KEY (contract_address) REFERENCES contribution_contracts(contract_address), - PRIMARY KEY (contract_address, address) - ); - """) - - cursor.execute(""" - CREATE UNIQUE INDEX IF NOT EXISTS idx_contribution_contracts_contributions_contract_address_address ON contribution_contracts_contributions(contract_address, address); - """) - - cursor.execute(""" - CREATE UNIQUE INDEX IF NOT EXISTS idx_contribution_contracts_contributions_contract_address_address_amount ON contribution_contracts_contributions(contract_address, address, amount); - """) - - - cursor.execute(""" - CREATE TABLE IF NOT EXISTS stakes ( - id INTEGER PRIMARY KEY NOT NULL, /* Contract ID */ - last_updated INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), - - pubkey_bls BLOB NOT NULL, - deregistration_unlock_height INTEGER, - earned_downtime_blocks INTEGER, - last_reward_block_height INTEGER, - last_uptime_proof INTEGER, - operator_address BLOB NOT NULL, - operator_fee INTEGER, - requested_unlock_height INTEGER, - service_node_pubkey BLOB NOT NULL, - state TEXT NOT NULL - - CHECK(length(operator_address) == 20) - - ) - """) - - cursor.execute(""" - CREATE TABLE IF NOT EXISTS stake_contributions ( - contract_id INTEGER NOT NULL, - address BLOB NOT NULL, - amount INTEGER NOT NULL, - reserved INTEGER, - - CHECK(length(address) == 20), - - FOREIGN KEY (contract_id) REFERENCES stakes(id), - PRIMARY KEY (contract_id, address) - ); - """) - - cursor.execute(""" - CREATE UNIQUE INDEX IF NOT EXISTS idx_stake_contributions_contract_id_address ON stake_contributions(contract_id, address); - """) - - cursor.execute(""" - CREATE UNIQUE INDEX IF NOT EXISTS idx_stake_contributions_contract_id_address_amount ON stake_contributions(contract_id, address, amount); - """) - - - cursor.close() - sql.close() - - -app = App() - -def get_sql(): - if "db" not in flask.g: - flask.g.sql = sqlite3.connect(config.backend.sqlite_db) - return flask.g.sql - -def date_now_str() -> str: - result = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] - return result - -# Validates that input is 64 hex bytes and converts it to 32 bytes. -class Hex64Converter(BaseConverter): - def __init__(self, url_map): - super().__init__(url_map) - self.regex = "[0-9a-fA-F]{64}" - - def to_python(self, value): - return bytes.fromhex(value) - - def to_url(self, value): - return value.hex() - - -eth_regex = "0x[0-9a-fA-F]{40}" -class EthConverter(BaseConverter): - def __init__(self, url_map): - super().__init__(url_map) - self.regex = eth_regex - - -class OxenConverter(BaseConverter): - def __init__(self, url_map): - super().__init__(url_map) - self.regex = config.backend.oxen_wallet_regex - - -class OxenEthConverter(BaseConverter): - def __init__(self, url_map): - super().__init__(url_map) - self.regex = f"{eth_regex}|{config.backend.oxen_wallet_regex}" - - -app.url_map.converters["hex64"] = Hex64Converter -app.url_map.converters["eth_wallet"] = EthConverter -app.url_map.converters["oxen_wallet"] = OxenConverter -app.url_map.converters["either_wallet"] = OxenEthConverter - - -def get_sns_future(omq, oxend) -> FutureJSON: - return FutureJSON( - omq, - oxend, - "rpc.get_service_nodes", - args={ - "all": False, - "fields": { - x: True - for x in ( - "service_node_pubkey", - "requested_unlock_height", - "last_reward_block_height", - "active", - "pubkey_bls", - "funded", - "earned_downtime_blocks", - "service_node_version", - "contributors", - "total_contributed", - "total_reserved", - "staking_requirement", - "portions_for_operator", - "operator_address", - "pubkey_ed25519", - "last_uptime_proof", - "state_height", - "swarm_id", - "is_removable", - "is_liquidatable", - "operator_fee" - ) - }, - }, - ) - -def get_sns(sns_future, info_future): - info = info_future.get() - awaiting_sns, active_sns, inactive_sns = [], [], [] - sn_states = sns_future.get() - sn_states = ( - sn_states["service_node_states"] if "service_node_states" in sn_states else [] - ) - for sn in sn_states: - sn["contribution_open"] = sn["staking_requirement"] - sn["total_reserved"] - sn["contribution_required"] = ( - sn["staking_requirement"] - sn["total_contributed"] - ) - sn["num_contributions"] = sum( - len(x["locked_contributions"]) - for x in sn["contributors"] - if "locked_contributions" in x - ) - - if sn["active"]: - active_sns.append(sn) - elif sn["funded"]: - sn["decomm_blocks_remaining"] = max(sn["earned_downtime_blocks"], 0) - sn["decomm_blocks"] = info["height"] - sn["state_height"] - inactive_sns.append(sn) - else: - awaiting_sns.append(sn) - return awaiting_sns, active_sns, inactive_sns - - -def hexify(container): - """ - Takes a dict or list and mutates it to change any `bytes` values in it to str hex representation - of the bytes, recursively. - """ - if isinstance(container, dict): - it = container.items() - elif isinstance(container, list): - it = enumerate(container) - else: - return - - for i, v in it: - if isinstance(v, bytes): - container[i] = v.hex() - else: - hexify(v) - - -def get_timers_hours(network_type: str): - match network_type: - case 'testnet' | 'stagenet' | 'devnet' | 'localdev' | 'fakechain': - return { - 'deregistration_lock_duration_hours': 48, - 'unlock_duration_hours': 24, - } - case 'mainnet': - return { - 'deregistration_lock_duration_hours': 30 * 24, - 'unlock_duration_hours': 15 * 24, - } - case _: - raise ValueError(f"Unknown network type {network_type}") - - -@app.route("/timers/") -def fetch_network_timers(network_type: str = None): - if network_type is None: - return json_response(get_timers_hours(get_info().get('nettype'))) - else: - return json_response(get_timers_hours(network_type)) - - -# Target block time in seconds -TARGET_BLOCK_TIME = 120 - - -def blocks_in(seconds: int): - """ - Mimics the behavior of the oxend `blocks_in` function. - """ - return int(seconds / TARGET_BLOCK_TIME) - - -def get_info() -> dict: - omq, oxend = omq_connection() - info: dict | None = FutureJSON(omq, oxend, "rpc.get_info").get() - blk_header_result: dict | None = FutureJSON(omq, oxend, 'rpc.get_last_block_header', args={'fill_pow_hash': False, 'get_tx_hashes': False }).get() - - result: dict = {} - result['nettype'] = info['nettype'] - result['hard_fork'] = info['hard_fork'] - result['version'] = info['version'] - result['block_hash'] = info['top_block_hash'] - result['staking_requirement'] = info['staking_requirement'] - result['max_stakers'] = info['max_contributors'] - result['min_operator_contribution'] = info['min_operator_contribution'] - - blk_header = blk_header_result['block_header'] - result['block_timestamp'] = blk_header['timestamp'] - result['block_height'] = blk_header['height'] - result['block_hash'] = blk_header['hash'] - return result - - -def json_response(vals): - """ - Takes a dict, adds some general info fields to it, and jsonifies it for a flask route function - return value. The dict gets passed through `hexify` first to convert any bytes values to hex. - """ - - hexify(vals) - - return flask.jsonify({**vals, "network": get_info(), "t": time.time()}) - - -def get_frequently_update_contract_details(address: str): - try: - contract_interface = app.service_node_contribution.get_contract_instance(address) - - contributions = contract_interface.get_contributions() - status = contract_interface.status() - - total_contributions = 0 - for contribution in contributions: - amount = contribution.get('amount') - total_contributions += amount - - return address, status, contributions, total_contributions - except Exception as e: - app.logger.error("Error occurred while updating contract info: {}".format(e)) - return None - - -def get_base_contract_details(address: str): - contract_interface = app.service_node_contribution.get_contract_instance(address) - # Fetch statuses and other details - # TODO: this does 3 network requests one after the other, we need to improve this - operator = contract_interface.get_operator() - pubkey_bls = contract_interface.get_bls_pubkey() - service_node_params = contract_interface.get_service_node_params() - service_node_pubkey = service_node_params.get('serviceNodePubkey') - service_node_signature = service_node_params.get('serviceNodeSignature') - fee = service_node_params.get('fee') - - # TODO: this does 2 network requests one after the other, we need to improve this - _, status, contributions, total_contributions = get_frequently_update_contract_details(address) - - return address, operator, pubkey_bls, service_node_pubkey, service_node_signature, fee, status, contributions, total_contributions - - -get_frequently_update_contract_details_loop = asyncio.get_event_loop() - -async def update_contributor_contracts(addresses): - results = [] - with ThreadPoolExecutor(max_workers=config.backend.thread_pool_max_workers) as executor: - get_frequently_update_contract_details_loop = asyncio.get_event_loop() - futures = [ - get_frequently_update_contract_details_loop.run_in_executor( - executor, - get_frequently_update_contract_details, - address - ) for address in addresses - ] - for future in asyncio.as_completed(futures): - try: - # This will raise an exception if the thread raised an exception - result = await future - results.append(result) - - except Exception as e: - app.logger.error("Error occurred in thread: {}".format(e)) - return results - - -get_base_contract_details_loop = asyncio.get_event_loop() - - -async def get_base_contract_details_contracts(addresses): - results = [] - with ThreadPoolExecutor(max_workers=config.backend.thread_pool_max_workers) as executor: - get_base_contract_details_loop = asyncio.get_event_loop() - futures = [ - get_base_contract_details_loop.run_in_executor( - executor, - get_base_contract_details, - address - ) for address in addresses - ] - for future in asyncio.as_completed(futures): - try: - # This will raise an exception if the thread raised an exception - result = await future - if result is None: - continue - results.append(result) - except Exception as e: - app.logger.error("Error occurred in thread: {}".format(e)) - return results - - -@timer(30) -def process_new_contribution_contracts(signum): - app.logger.info("{} Process new contribution contracts start".format(date_now_str())) - perf_start = perf_counter() - - new_contracts = app.service_node_contribution_factory.get_latest_contribution_contract_events() - - app.logger.debug('Found {} new contract events'.format(len(new_contracts))) - - addresses = [] - for event in new_contracts: - contract_address = event.args.contributorContract - if contract_address is None: - continue - addresses.append(contract_address) - - results = get_base_contract_details_loop.run_until_complete(get_base_contract_details_contracts(addresses)) - - with app.app_context(), get_sql() as sql: - cursor = sql.cursor() - for details in results: - if details is None: - app.logger.warning("No details for new contract") - continue - - address, operator, pubkey_bls, service_node_pubkey, service_node_signature, fee, status, contributions, total_contributions = details - contract_address_bytes = address_to_bytes(address) - - cursor.execute( - """ - INSERT INTO contribution_contracts (contract_address, pubkey_ed25519, pubkey_bls, sig_ed25519, operator_address, fee, status, total_contributions) VALUES (?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT (contract_address) DO NOTHING - """, - (contract_address_bytes, service_node_pubkey, pubkey_bls, service_node_signature, operator, fee, status, - total_contributions) - ) - - for contributor in contributions: - stake_address = address_to_bytes(contributor.get('address')) - beneficiary_address = address_to_bytes(contributor.get('beneficiary')) - amount = contributor.get('amount') - - cursor.execute( - """ - INSERT INTO contribution_contracts_contributions (contract_address, address, beneficiary_address, amount) VALUES (?, ?, ?, ?) - """, - (contract_address_bytes, stake_address, beneficiary_address, amount) - ) - - sql.commit() - - perf_end = perf_counter() - perf_diff = perf_end - perf_start - - app.logger.info("{} Process new contribution contracts end, took: {}s".format(date_now_str(), perf_diff)) - - -# // Track the status of the multi-contribution contract. At any point in the -# // contract's lifetime, `reset` can be invoked to set the contract back to -# // `WaitForOperatorContrib`. -# enum Status { -# // Contract is initialised w/ no contributions. Call `contributeFunds` -# // to transition into `OpenForPublicContrib` -# WaitForOperatorContrib, # 0 -# -# // Contract has been initially funded by operator. Public and reserved -# // contributors can now call `contributeFunds`. When the contract is -# // collaterialised with exactly the staking requirement, the contract -# // transitions into `WaitForFinalized` state. -# OpenForPublicContrib, # 1 -# -# // Operator must invoke `finalizeNode` to transfer the tokens and the -# // node registration details to the `stakingRewardsContract` to -# // transition to `Finalized` state. -# WaitForFinalized, # 2 -# -# // Contract interactions are blocked until `reset` is called. -# Finalized # 3 -# } - -def parse_contributor_contract_status(status: int): - if status == 0 or status == 1: - return "awaiting_contributors" - elif status == 2: - return "finalizing" - elif status == 3: - return "finalized" - else: - raise ValueError(f"Invalid contributor contract status: {status}") - - -CONTRACT_STATUS_UPDATE_TIME = 30 - - -@timer(CONTRACT_STATUS_UPDATE_TIME) -def update_contract_details(signum): - app.logger.info("{} Update Contract Statuses Start".format(date_now_str())) - perf_start = perf_counter() - - addresses = [] - with app.app_context(), get_sql() as sql: - cursor = sql.cursor() - cursor.execute("SELECT contract_address FROM contribution_contracts") - for address, in cursor: - addresses.append(eth_format(address)) - - result = get_frequently_update_contract_details_loop.run_until_complete(update_contributor_contracts(addresses)) - - with app.app_context(), get_sql() as sql: - cursor = sql.cursor() - for details in result: - if details is None: - app.logger.warning("No details for contract") - continue - address, status, contributions, total_contributions = details - contract_address_bytes = address_to_bytes(address) - cursor.execute( - """ - UPDATE contribution_contracts SET status = ?, total_contributions=? WHERE contract_address = ? - """, - (status, total_contributions, contract_address_bytes) - ) - - for contributor in contributions: - stake_address = address_to_bytes(contributor.get('address')) - beneficiary_address = address_to_bytes(contributor.get('beneficiary')) - amount = contributor.get('amount') - - cursor.execute( - """ - INSERT OR REPLACE INTO contribution_contracts_contributions (contract_address, address, beneficiary_address, amount) VALUES (?, ?, ?, ?) - """, - (contract_address_bytes, stake_address, beneficiary_address, amount) - ) - - sql.commit() - - perf_end = perf_counter() - perf_diff = perf_end - perf_start - - if perf_diff > CONTRACT_STATUS_UPDATE_TIME: - app.logger.warning("{} Update Contract Statuses Finish, took: {} seconds".format(date_now_str(), perf_diff)) - else: - app.logger.info("{} Update Contract Statuses Finish, took: {} seconds".format(date_now_str(), perf_diff)) - - -def get_contribution_contracts(): - if time.time() < app.contracts_stale_timestamp and app.contracts is not None: - return app.contracts - - contracts = {} - with app.app_context(), get_sql() as sql: - cursor = sql.cursor() - cursor.execute( - "SELECT contract_address, pubkey_ed25519, pubkey_bls, sig_ed25519, operator_address, fee, status, total_contributions FROM contribution_contracts") - for contract_address, service_node_pubkey, pubkey_bls, service_node_signature, operator, fee, status, total_contributions in cursor: - contracts[contract_address] = { - "service_node_pubkey": service_node_pubkey, - "pubkey_bls": pubkey_bls, - "service_node_signature": service_node_signature, - "operator": operator, - "fee": fee, - "contract_state": parse_contributor_contract_status(status), - "total_contributions": total_contributions - } - - cursor.execute( - "SELECT contract_address, address, beneficiary_address, amount FROM contribution_contracts_contributions") - for contract_address, address, beneficiary_address, amount in cursor: - if contract_address not in contracts: - continue - contracts[contract_address].setdefault("contributors", []).append({ - "address": address, - "beneficiary": beneficiary_address, - "amount": amount - }) - - app.contracts = contracts - app.contracts_stale_timestamp = time.time() + config.backend.stale_time_seconds - return app.contracts - - -@timer(10) -def fetch_service_nodes(signum): - app.logger.info("{} Update SN Start".format(date_now_str())) - omq, oxend = omq_connection() - - # Generate new state - sn_info_list = get_sns_future(omq, oxend).get()["service_node_states"] - wallet_to_sn_map = {} - sn_map = {} - - if len(app.bls_pubkey_to_contract_id_map) == 0: - app.logger.warning("{} bls_pubkey_to_contract_id_map is empty, fetching contract ids".format(date_now_str())) - update_service_node_contract_ids(None) - - for sn_info in sn_info_list: - # Add the SN contract ID to the sn_info dict - pubkey_bls = sn_info.get('pubkey_bls') - if pubkey_bls is None: - app.logger.warning(f"pubkey_bls is None for sn_info SN: {sn_info}") - continue - contract_id = app.bls_pubkey_to_contract_id_map.get(pubkey_bls) - if contract_id is None: - app.logger.warning(f"Contract ID not found for sn_info SN with BLS pubkey: {pubkey_bls}") - continue - - sn_info["contract_id"] = contract_id - requested_unlock_height = sn_info.get('requested_unlock_height') - sn_info['requested_unlock_height'] = requested_unlock_height if requested_unlock_height != 0 else None - sn_map[contract_id] = sn_info - - contributors = {c["address"]: c["amount"] for c in sn_info["contributors"]} - # Creating (wallet -> [SN's the wallet owns]) table - for wallet_key in contributors.keys(): - # TODO: Validate we want to allow wallet_key to not go through eth_format if len == 40 - formatted_wallet_key = eth_format(wallet_key) if len(wallet_key) == 40 else wallet_key - - if formatted_wallet_key is None: - app.logger.warning(f"Wallet key is None for sn_info SN: {sn_info}") - continue - - wallet_to_sn_map.setdefault(formatted_wallet_key, []).append(contract_id) - - # Apply the new state if there are any - if len(sn_map) > 0: - app.logger.debug(f"Adding {len(sn_map)} service node info to the contract_id_to_sn_map") - app.contract_id_to_sn_map = sn_map - - app.logger.debug(f"Adding {len(wallet_to_sn_map)} wallet to service node map") - app.wallet_to_sn_map = wallet_to_sn_map - - # Get list of SNs that can be liquidated/exited - exit_liquidation_list_json = get_oxen_rpc_bls_exit_liquidation_list(omq, oxend).get() - - exitable_sns = {} - wallet_to_exitable_sn_map = {} - - if exit_liquidation_list_json is not None: - net_info = get_info() - net_type = net_info.get('nettype') - timers = get_timers_hours(net_type) - - for entry in exit_liquidation_list_json: - sn_info = entry.get('info') - - pubkey_bls = sn_info.get('bls_public_key') - if pubkey_bls is None: - app.logger.warning(f"bls_public_key is None for exit_liquidation_list_json SN: {sn_info}") - continue - - contract_id = app.bls_pubkey_to_contract_id_map.get(pubkey_bls) - if contract_id is None: - # If there is no contract ID it means this node has exited the smart contract and this event is being - # confirmed by oxend. This is the last state we'll get for this node from oxend. - # TODO: look at implementing some logic to add the node data to a dict that checks to make sure the db - # is properly updated with the final data we'll receive from oxend about this node. - app.logger.warning(f"Contract ID not found for exit_liquidation_list_json SN with BLS pubkey: {pubkey_bls}") - continue - - sn_info['pubkey_bls'] = pubkey_bls - sn_info['contract_id'] = contract_id - - for item in sn_info.get('contributors'): - if 'version' in item: - item.pop('version') - - exit_type = entry.get('type') - sn_info['exit_type'] = exit_type - sn_info['deregistration_unlock_height'] = entry.get('height') + blocks_in( - timers.get('unlock_duration_hours') * 3600) if exit_type == 'deregister' else None - - requested_unlock_height = sn_info.get('requested_unlock_height') - sn_info['requested_unlock_height'] = requested_unlock_height if requested_unlock_height != 0 else None - - sn_info['service_node_pubkey'] = entry.get('service_node_pubkey') - sn_info['liquidation_height'] = entry.get('liquidation_height') - exitable_sns[contract_id] = sn_info - - for contributor in sn_info.get('contributors'): - wallet_str = eth_format(contributor.get('address')) - - if wallet_str is None: - app.logger.warning(f"Wallet str is None for exit_liquidation_list_json SN: {sn_info}") - continue - - wallet_to_exitable_sn_map.setdefault(wallet_str, set()).add(contract_id) - - # Apply the new state if there are any - if len(exitable_sns) > 0: - app.logger.debug(f"Adding {len(exitable_sns)} exitable SN info to the contract_id_to_exitable_sn_map") - app.contract_id_to_exitable_sn_map = exitable_sns - - app.logger.debug(f"Adding {len(wallet_to_exitable_sn_map)} wallet to exitable SN map") - app.wallet_to_exitable_sn_map = wallet_to_exitable_sn_map - - # Get the accrued rewards values for each wallet - accrued_rewards_json = oxen_rpc_get_accrued_rewards(omq, oxend).get() - if accrued_rewards_json['status'] != 'OK': - app.logger.warning("{} Update SN early exit, accrued rewards request failed: {}".format( - date_now_str(), - accrued_rewards_json)) - return - - balances_key = 'balances' - if balances_key not in accrued_rewards_json: - app.logger.warning("{} Update SN early exit, accrued rewards request failed, 'balances' key was missing: {}".format( - date_now_str(), - accrued_rewards_json)) - return - - # Populate (Binary ETH wallet address -> accrued_rewards) table - for address_hex, rewards in accrued_rewards_json[balances_key].items(): - # Ignore non-ethereum addresses (e.g. left oxen rewards, not relevant) - trimmed_address_hex = address_hex[2:] if address_hex.startswith('0x') else address_hex - if len(trimmed_address_hex) != 40: - continue - - # Convert the address to bytes - address_key = bytes.fromhex(trimmed_address_hex) - - # Create the info for the wallet if it doesn't exist - if address_key not in app.wallet_map: - app.wallet_map[address_key] = WalletInfo() - - # We only update the rewards queried from the Oxen network - # Contract rewards are loaded on demand and cached. - # - # TODO It appears that doing the contract call is quite slow. - app.wallet_map[address_key].rewards = rewards - - app.logger.info("{} Update SN finished".format(date_now_str())) - - -@app.route("/info") -def network_info(): - """ - Do-nothing endpoint that can be called to get just the "network" and "t" values that are - included in every actual endpoint when you don't have any other endpoint to invoke. - """ - return json_response({}) - -def get_rewards_dict_for_wallet(eth_wal): - wallet_str = eth_format(eth_wal) - - # Convert the wallet string into bytes if it is a hex (eth address) - wallet_key = wallet_str - if eth_wal is not None: - trimmed_wallet_str = wallet_str[2:] if wallet_str.startswith('0x') else wallet_str - wallet_key = bytes.fromhex(str(trimmed_wallet_str)) - - # Retrieve the rewards earned by the wallet - result = app.wallet_map[wallet_key] if wallet_key in app.wallet_map else WalletInfo() - - # Query the amount of rewards committed/claimed currently on the contract - # - # NOTE: This is done on demand because it appears to be quite slow, - # iterating the list of wallets in one shot is quite expensive. The result - # is cached in the contract layer to avoid these expensive calls. - # - # This call is completely bypassed if the wallet is not in our wallet map - # which is populated from the Oxen rewards DB. The Oxen DB is the - # authoritative list and this prevents an actor from spamming random - # wallets to bloat out the python runtime memory usage. - if result.rewards > 0: - contract_recipient = app.service_node_rewards.recipients(wallet_key) - app.wallet_map[wallet_key].contract_rewards = contract_recipient.rewards - app.wallet_map[wallet_key].contract_claimed = contract_recipient.claimed - - return result - - -def generate_uuid(original_id): - return str(uuid.uuid5(uuid.NAMESPACE_DNS, original_id)) - - -class Contributor(TypedDict): - address: bytes - amount: int - reserved: int - -class Stake(TypedDict): - contract_id: int | None - contributors: list[Contributor] - deregistration_unlock_height: int | None - earned_downtime_blocks: int - last_reward_block_height: int | None - last_uptime_proof: int | None - operator_address: bytes - operator_fee: int | None - pubkey_bls: bytes - requested_unlock_height: int | None - service_node_pubkey: bytes - staked_balance: int | None - state: str - # Only on multi-contributor contracts - contract: str | None - -class ErrorResponse: - def __init__(self, message: str): - self.error = message - -def parse_stake_info( - stake: dict, - wallet_address: ChecksumAddress, - confirmed_exited: bool = False, -) -> Stake | ErrorResponse: - """ - Parses stake information and returns a standardised dictionary of stake info. - - Args: - stake (dict): The stake data containing various stake attributes. - wallet_address (str): The wallet address of the user. - confirmed_exited (bool, optional): Flag indicating if the stake has been confirmed as exited. Defaults to False. - - Exceptions: - ValueError: If the stake state cannot be determined. - - Returns: - dict: A dictionary containing the parsed stake information. - """ - state = None - deregistration_unlock_height = None - - try: - # Handles exit events - if 'exit_type' in stake: - exit_type = stake.get('exit_type') - if exit_type == 'exit': - state = ( - "Awaiting Exit" - if stake.get('contract_id') and not confirmed_exited - else "Exited" - ) - elif exit_type == 'deregister': - state = "Deregistered" - deregistration_unlock_height = stake.get('deregistration_unlock_height') - else: - raise ValueError(f"Invalid exit type {exit_type}") - # Handles contract events - elif 'contract_state' in stake: - contract_state = stake.get('contract_state') - if contract_state == 'awaiting_contributors': - state = "Awaiting Contributors" - elif contract_state == 'cancelled': - state = "Cancelled" - elif contract_state == 'finalized': - raise ValueError("Finalized nodes must be filtered out before reaching this point") - else: - raise ValueError(f"Invalid contract state {contract_state}") - # Handles running node info - elif 'active' in stake and 'funded' in stake: - state = ( - 'Decommissioned' - if not stake.get("active") and stake.get("funded") - else 'Running' - ) - elif 'state' in stake: - current_state = stake.get('state') - if current_state == 'Deregistered': - deregistration_unlock_height = stake.get('deregistration_unlock_height') - if confirmed_exited and current_state == 'Awaiting Exit': - state = 'Exited' - else: - state = current_state - else: - raise ValueError("Unable to determine node state") - except ValueError as e: - base_msg = f"Value Error while parsing stake state for stake: \n {stake}" - app.logger.error(f"{base_msg} \n Exception: {e}") - return ErrorResponse(base_msg) - except Exception as e: - base_msg = f"Exception while parsing stake state for stake: \n {stake}" - app.logger.error(f"{base_msg} \n Exception: {e}") - return ErrorResponse(base_msg) - - # Process contributors and calculate staked balance - contributors = stake.get('contributors', []) - staked_balance = sum( - contributor.get('amount') - for contributor in contributors - if eth_format(contributor.get('address')) == wallet_address - ) or None - - # `stake-${stake.contract_id}-${stake.service_node_pubkey}-${stake.last_uptime_proof}`; - contract_id = stake.get('contract_id') - contract = stake.get('contract') - contract_formatted = eth_format(contract) if contract is not None else None - pubkey_bls = stake.get('pubkey_bls') - - # TODO: Investigate the best data to use for id generation. these ids MUST be unique for each stake. - # - # A pubkey_bls can have multiple stakes over time, but a pubkey_bls and (contract_id or contract) can only have - # one stake ever. The pubkey_bls needs to be used as its possible for a stake to not have a contract_id or - # contract when it exits - unique_id = generate_uuid("{}-{}".format(pubkey_bls, contract_id if contract_formatted is None else contract_formatted)) - - return { - 'unique_id': unique_id, - 'contract_id': contract_id, - 'contract': contract_formatted, - 'contributors': contributors, - 'deregistration_unlock_height': deregistration_unlock_height, - 'earned_downtime_blocks': stake.get('earned_downtime_blocks'), - 'last_reward_block_height': stake.get('last_reward_block_height'), - 'last_uptime_proof': stake.get('last_uptime_proof'), - 'liquidation_height': stake.get('liquidation_height'), - 'operator_address': stake.get('operator_address'), - 'operator_fee': stake.get('operator_fee'), - 'pubkey_bls': pubkey_bls, - 'requested_unlock_height': stake.get('requested_unlock_height'), - 'service_node_pubkey': stake.get('service_node_pubkey'), - 'staked_balance': staked_balance, - 'state': state, - 'exited': confirmed_exited or state == 'Exited', - } - - -@app.route("/stakes/") -def get_stakes(eth_wal: str): - try: - if not eth_wal or not eth_utils.is_address(eth_wal): - raise ValueError("Invalid wallet address") - - wallet_address = eth_format(eth_wal) - app.tracked_wallet_addresses.add(wallet_address) - - # A contract id can only appear once across the lists - added_contract_ids = set() - - parse_errors = [] - - app.logger.debug(f"Fetching stakes for {wallet_address}") - app.logger.debug(f"wallet_to_sn_map len: {len(app.wallet_to_sn_map)}") - app.logger.debug(f"contract_id_to_sn_map len: {len(app.contract_id_to_sn_map)}") - app.logger.debug(f"wallet_to_exitable_sn_map len: {len(app.wallet_to_exitable_sn_map)}") - app.logger.debug(f"contract_id_to_exitable_sn_map len: {len(app.contract_id_to_exitable_sn_map)}") - - def handle_stakes( - address_to_stakes_map: dict[ChecksumAddress, set[int]], - contract_id_to_stake_map: dict[int, Stake], - output_list: list[Stake], - confirmed_exited=False, - ): - app.logger.debug(f"added_contract_ids: {added_contract_ids}") - for contract_id in address_to_stakes_map.get(wallet_address, []): - app.logger.debug(f"contract_id: {contract_id}") - if contract_id not in added_contract_ids: - stake = contract_id_to_stake_map.get(contract_id) - parsed_stake = parse_stake_info(stake, wallet_address, confirmed_exited) - if isinstance(parsed_stake, ErrorResponse): - parse_errors.append({ - 'contract_id': contract_id, - 'error': parsed_stake.error - }) - else: - output_list.append(parsed_stake) - added_contract_ids.add(contract_id) - - stakes = [] - handle_stakes(app.wallet_to_exitable_sn_map, app.contract_id_to_exitable_sn_map, stakes) - handle_stakes(app.wallet_to_sn_map, app.contract_id_to_sn_map, stakes) - - if wallet_address not in app.wallet_to_historical_stakes_map: - # NOTE: This db call is only triggered once per wallet address, this is reset after the scheduled db read. - get_db_stakes_for_wallet(wallet_address) - - historical_stakes = [] - handle_stakes(app.wallet_to_historical_stakes_map, app.contract_id_to_historical_stakes_map, historical_stakes, - confirmed_exited=True) - - contracts = [ - { - "contract": addr, - **details - } - for addr, details in get_contribution_contracts().items() - if details.get('contract_state') == 'awaiting_contributors' and details.get('operator') == wallet_address - ] - - if len(app.bls_pubkey_to_contract_id_map) == 0: - app.logger.warning("{} bls_pubkey_to_contract_id_map is empty, fetching contract ids".format(date_now_str())) - update_service_node_contract_ids(None) - - - for contract in contracts: - pubkey_bls = contract.get('pubkey_bls') - if pubkey_bls is None: - app.logger.warning(f"pubkey_bls is None for contract: {contract}") - continue - contract_id = app.bls_pubkey_to_contract_id_map.get(pubkey_bls) - contract['contract_id'] = contract_id - contract['operator_fee'] = contract.get('fee') - contract['operator_address'] = contract.get('operator') - - stakes.append(parse_stake_info(contract, wallet_address)) - - return json_response({ - "contracts": contracts, - "historical_stakes": historical_stakes, - "stakes": stakes, - "wallet": vars(get_rewards_dict_for_wallet(wallet_address)), - "error_stakes": parse_errors if len(parse_errors) > 0 else None - }) - except ValueError as e: - app.logger.error(f"Exception: {e}") - return flask.abort(400, e) - except Exception as e: - app.logger.error(f"Exception: {e}") - return flask.abort(500, e) - - -@app.route("/nodes") -def get_nodes(): - """ - Returns a list of all nodes that are running. - """ - nodes = [] - for node in app.contract_id_to_sn_map.values(): - nodes.append(parse_stake_info(node, node['operator_address'])) - return json_response({"nodes": nodes}) - - -# export enum NODE_STATE { -# RUNNING = 'Running', -# AWAITING_CONTRIBUTORS = 'Awaiting Contributors', -# CANCELLED = 'Cancelled', -# DECOMMISSIONED = 'Decommissioned', -# DEREGISTERED = 'Deregistered', -# AWAITING_EXIT = 'Awaiting Exit', -# EXITED = 'Exited', -# } -@app.route("/nodes/") -@app.route("/nodes/") -def get_nodes_for_wallet(oxen_wal=None, eth_wal=None): - assert oxen_wal is not None or eth_wal is not None - wallet_str = eth_format(eth_wal) if eth_wal is not None else oxen_wal - - sns = [] - nodes = [] - for sn_index in app.wallet_to_sn_map.get(wallet_str, []): - sn_info = app.contract_id_to_sn_map[sn_index] - sns.append(sn_info) - balance = {c["address"]: c["amount"] for c in sn_info["contributors"]}.get(wallet_str, 0) - state = 'Decommissioned' if not sn_info["active"] and sn_info["funded"] else 'Running' - nodes.append({ - 'balance': balance, - 'contributors': sn_info["contributors"], - 'last_uptime_proof': sn_info["last_uptime_proof"], - 'contract_id': sn_info["contract_id"], - 'operator_address': sn_info["operator_address"], - 'operator_fee': sn_info["operator_fee"], - 'requested_unlock_height': sn_info["requested_unlock_height"], - 'last_reward_block_height':sn_info["last_reward_block_height"], - 'service_node_pubkey': sn_info["service_node_pubkey"], - 'pubkey_bls': sn_info["pubkey_bls"], - 'decomm_blocks_remaining': max(sn_info["earned_downtime_blocks"], 0), - 'state': state, - }) - - contracts = [] - if wallet_str in app.contributors: - for address in app.contributors[wallet_str]: - details = app.contracts[address] - contracts.append({ - 'contract_address': address, - 'details': details - }) - - # Setup the result - result = json_response({ - "wallet": vars(get_rewards_dict_for_wallet(wallet_str)), - "service_nodes": sns, - "contracts": contracts, - "nodes": nodes, - }) - - return result - -@app.route("/nodes/open") -def get_contributable_contracts(): - nodes = get_contribution_contracts() - return json_response({ - "nodes": [ - { - "contract": addr, - **details - } - for addr, details in nodes.items() - if details.get('contract_state') == 'awaiting_contributors' - ] - }) - -@app.route("/rewards/", methods=["GET", "POST"]) -def get_rewards(eth_wal: str): - if flask.request.method == "GET": - result = json_response({ - "wallet": vars(get_rewards_dict_for_wallet(eth_wal)), - }) - return result - - if flask.request.method == "POST": - omq, oxend = omq_connection() - try: - response = oxen_rpc_bls_rewards_request(omq, oxend, eth_format(eth_wal)).get() - if response is None: - return flask.abort(504) # Gateway timeout - if 'status' in response: - response.pop('status') - if 'address' in response: - response.pop('address') - result = json_response({ - 'result': response - }) - return result - except TimeoutError: - return flask.abort(408) # Request timeout - - return flask.abort(405) # Method not allowed - -@app.route("/exit/") -def get_exit(ed25519_pubkey: bytes): - omq, oxend = omq_connection() - try: - response = oxen_rpc_bls_exit_liquidation(omq, oxend, ed25519_pubkey, liquidate=False).get() - if response is None: - return flask.abort(504) # Gateway timeout - if 'status' in response: - response.pop('status') - result = json_response({ - 'result': response - }) - return result - except TimeoutError: - return flask.abort(408) # Request timeout - -@app.route("/exit_liquidation_list") -def get_exit_liquidation_list(): - try: - array = [] - for item in app.contract_id_to_exitable_sn_map.values(): - array.append(item) - - result = json_response({ - 'result': array - }) - return result - except TimeoutError: - return flask.abort(408) # Request timeout - -@app.route("/liquidation/") -def get_liquidation(ed25519_pubkey: bytes): - omq, oxend = omq_connection() - try: - response = oxen_rpc_bls_exit_liquidation(omq, oxend, ed25519_pubkey, liquidate=True).get() - if response is None: - return flask.abort(504) # Gateway timeout - if 'status' in response: - response.pop('status') - result = json_response({ - 'result': response - }) - return result - except TimeoutError: - return flask.abort(408) # Request timeout - - -def handle_stakes_row( - wallet_to_historical_stakes_map: dict[ChecksumAddress, set[int]], - contract_id_to_historical_stakes_map: dict[int, Stake], - sql_cur: sqlite3.Cursor, -): - for row in sql_cur: - ( - contract_id, - last_updated, - pubkey_bls, - deregistration_unlock_height, - earned_downtime_blocks, - last_reward_block_height, - last_uptime_proof, - operator_address, - operator_fee, - requested_unlock_height, - service_node_pubkey, - state, - contributor_address, - contributor_amount, - ) = row - - - contributor_adr = eth_format(contributor_address) - - if contract_id not in contract_id_to_historical_stakes_map: - stake = { - 'pubkey_bls': pubkey_bls, - 'contract_id': contract_id, - 'deregistration_unlock_height': deregistration_unlock_height, - 'earned_downtime_blocks': earned_downtime_blocks, - 'last_reward_block_height': last_reward_block_height, - 'last_uptime_proof': last_uptime_proof, - 'operator_address': eth_format(operator_address), - 'operator_fee': operator_fee, - 'requested_unlock_height': requested_unlock_height, - 'service_node_pubkey': service_node_pubkey, - 'state': state, - 'contributors': [], - } - contract_id_to_historical_stakes_map[contract_id] = stake - else: - stake = contract_id_to_historical_stakes_map[contract_id] - - # Add contributor info - contributor = { - 'address': contributor_adr, - 'amount': contributor_amount, - } - stake['contributors'].append(contributor) - wallet_to_historical_stakes_map.setdefault(contributor_adr, set()).add(contract_id) - - -def get_db_stakes_for_wallet(wallet_address: ChecksumAddress): - """ - This exists to get the stakes for a wallet that is not in the tracked list. This should only be used when we need - the data but the timed database read hasn't executed yet with the address in the tracked list. A wallet address - can only call this once in between scheduled database read times. - """ - app.logger.debug("{} get_db_stakes_for_wallet: {}".format(date_now_str(), wallet_address)) - if wallet_address in app.tmp_db_trigger_wallet_addresses: - return - - app.tmp_db_trigger_wallet_addresses.add(wallet_address) - - with app.app_context(), get_sql() as sql: - cur = sql.cursor() - cur.execute( - """ - SELECT s.*, - sc_contributors.address AS contributor_address, - sc_contributors.amount AS contributor_amount - FROM stakes s - JOIN stake_contributions sc_requestor ON sc_requestor.contract_id = s.id - JOIN stake_contributions sc_contributors ON sc_contributors.contract_id = s.id - WHERE sc_requestor.address = ?; - """, - (address_to_bytes(wallet_address),), - ) - - handle_stakes_row(app.wallet_to_historical_stakes_map, app.contract_id_to_historical_stakes_map, cur) - - -def address_to_bytes(address: str) -> bytes: - if address.startswith("0x"): - return bytes.fromhex(address[2:]) - else: - return bytes.fromhex(address) - - -def chunks(lst, n): - """Yield successive n-sized chunks from lst.""" - for i in range(0, len(lst), n): - yield lst[i:i + n] - - -@timer(15) -def get_db_stakes(signum): - app.logger.info("{} Get stakes db start".format(date_now_str())) - wallet_to_historical_stakes_map: dict[ChecksumAddress, set[int]] = {} # (Wallet address -> Set of contract_ids) - contract_id_to_historical_stakes_map: dict[int, Stake] = {} # (contract_ids -> Stake Info) - - for address_chunk in chunks(list(app.tracked_wallet_addresses), 999): # SQLite default parameter limit is 999 - placeholders = ','.join(['?'] * len(address_chunk)) - with app.app_context(), get_sql() as sql: - cur = sql.cursor() - cur.execute(f""" - SELECT DISTINCT s.*, - sc_contributors.address AS contributor_address, - sc_contributors.amount AS contributor_amount - FROM stakes s - JOIN stake_contributions sc_requestor ON sc_requestor.contract_id = s.id - JOIN stake_contributions sc_contributors ON sc_contributors.contract_id = s.id - WHERE sc_requestor.address IN ({placeholders}); - """, - address_chunk, - ) - - handle_stakes_row(wallet_to_historical_stakes_map, contract_id_to_historical_stakes_map, cur) - - if len(wallet_to_historical_stakes_map) > 0: - app.wallet_to_historical_stakes_map = wallet_to_historical_stakes_map - app.contract_id_to_historical_stakes_map = contract_id_to_historical_stakes_map - - app.tmp_db_trigger_wallet_addresses.clear() - - app.logger.info("{} Get stakes db finish".format(date_now_str())) - - -# Debug function to load all contributor addresses into the tracked wallet addresses set -def load_contributor_addresses_into_tracked_wallet_addresses(): - with app.app_context(), get_sql() as sql: - cur = sql.cursor() - cur.execute("SELECT DISTINCT address FROM stake_contributions") - for row in cur: - app.tracked_wallet_addresses.add(row[0]) - -# Debug function to recover non-contributor stakes. This gets all stakes that are not in the stake_contributions table and adds them to the stake_contributions using the operator address as the contributor address. -def recover_non_contributor_stakes(): - with app.app_context(), get_sql() as sql: - cur = sql.cursor() - added_stakes = set() - cur.execute("SELECT DISTINCT contract_id, address FROM stake_contributions") - for contract_id, address in cur: - added_stakes.add((contract_id, address)) - - cur.execute("SELECT DISTINCT id, operator_address FROM stakes") - stakes_to_recover = set() - for contract_id, operator_address in cur: - if (contract_id, operator_address) in added_stakes: - continue - stakes_to_recover.add((contract_id, operator_address)) - - for contract_id, operator_address in stakes_to_recover: - app.logger.debug(f"Recovering stake for contract_id: {contract_id}, operator_address: {operator_address}") - cur.execute( - """ - INSERT OR REPLACE INTO stake_contributions (contract_id, address, amount) - VALUES (?, ?, ?) - """, - ( - contract_id, - operator_address, - 20000000000000, - ) - ) - -@timer(30) -def update_service_node_contract_ids(signum) -> None: - """ - Update the map of service node contract ids to BLS public keys. This fetches the list of all service nodes from the - Service Node Rewards contract and maps them to their corresponding contract ids. - """ - app.logger.info("{} Updating service node contract ids".format(date_now_str())) - [ids, bls_keys] = app.service_node_rewards.allServiceNodeIDs() - app.logger.debug(f"Added {len(ids)} service node contract ids") - app.bls_pubkey_to_contract_id_map = {f"{x:064x}{y:064x}": contract_id for contract_id, (x, y) in zip(ids, bls_keys)} - app.logger.info("{} Updating service node contract ids finish. Nodes: {}".format(date_now_str(), - len(app.bls_pubkey_to_contract_id_map))) - - -@timer(60) -def insert_updated_db_stakes(signum): - """ - Inserts or updates the stakes in the database. - """ - app.logger.info("{} Insert or update stakes db start".format(date_now_str())) - added_contract_ids = set() - with app.app_context(), get_sql() as sql: - cur = sql.cursor() - for node in chain(app.contract_id_to_exitable_sn_map.values(), app.contract_id_to_sn_map.values()): - stake = parse_stake_info(node, node.get('operator_address')) - contract_id = stake.get('contract_id') - if contract_id in added_contract_ids: - continue - added_contract_ids.add(contract_id) - cur.execute( - """ - INSERT OR REPLACE INTO stakes (id, last_updated, pubkey_bls, deregistration_unlock_height, earned_downtime_blocks, last_reward_block_height, last_uptime_proof, operator_address, operator_fee, requested_unlock_height, service_node_pubkey, state) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - contract_id, - datetime.datetime.now().timestamp(), - stake['pubkey_bls'], - stake['deregistration_unlock_height'], - stake['earned_downtime_blocks'], - stake['last_reward_block_height'], - stake['last_uptime_proof'], - address_to_bytes(stake['operator_address']), - stake['operator_fee'], - stake['requested_unlock_height'], - stake['service_node_pubkey'], - stake['state'], - ) - ) - - # Create the stake contributions entry - for contributor in stake['contributors']: - cur.execute( - """ - INSERT OR REPLACE INTO stake_contributions (contract_id, address, amount) - VALUES (?, ?, ?) - """, - ( - contract_id, - address_to_bytes(contributor['address']), - contributor.get('amount'), - ) - ) - added_contract_ids.clear() - app.logger.info("{} Insert or update stakes db finish".format(date_now_str())) - -# Decodes `x` into a bytes of length `length`. `x` should be hex or base64 encoded, without -# whitespace. Both regular and "URL-safe" base64 are accepted. Padding is optional for base64 -# values. Throws ParseError if the input is invalid or of the wrong size. `length` must be at -# least 5 (smaller byte values are harder or even ambiguous to distinguish between hex and base64). -def decode_bytes(k, x, length): - assert length >= 5 - - hex_len = length * 2 - b64_unpadded = (length * 4 + 2) // 3 - b64_padded = (length + 2) // 3 * 4 - - app.logger.debug(f"{len(x)}, {hex_len}") - if len(x) == hex_len and all(c in string.hexdigits for c in x): - return bytes.fromhex(x) - if len(x) in (b64_unpadded, b64_padded): - if oxenc.is_base64(x): - return oxenc.from_base64(x) - if "-" in x or "_" in x: # Looks like (maybe) url-safe b64 - x = x.replace("/", "_").replace("+", "-") - if oxenc.is_base64(x): - return oxenc.from_base64(x) - raise ParseError(k, f"expected {hex_len} hex or {b64_unpadded} base64 characters") - - -def byte_decoder(length: int): - return partial(decode_bytes, length=length) - - -# Takes a positive integer value required to be between irange[0] and irange[1], inclusive. The -# integer may not be 0-prefixed or whitespace padded. -def parse_int_field(k, v, irange): - if ( - len(v) == 0 - or not all(c in "0123456789" for c in v) - or (len(v) > 1 and v[0] == "0") - ): - raise ParseError(k, "an integer value is required") - v = int(v) - imin, imax = irange - if imin <= v <= imax: - return v - raise ParseError(k, f"expected an integer between {imin} and {imax}") - - -def raw_eth_addr(k, v): - if re.fullmatch(eth_regex, v): - if not eth_utils.is_address(v): - raise ParseError(k, "ETH address checksum failed") - return bytes.fromhex(v[2:]) - raise ParseError(k, "not an ETH address") - - -def eth_format(addr: Union[bytes, str]) -> ChecksumAddress: - try: - return eth_utils.to_checksum_address(addr) - except ValueError: - raise ParseError(addr, "Invalid ETH address") - - -class SNSignatureValidationError(ValueError): - pass - - -def check_reg_keys_sigs(params): - if len( - params["pubkey_ed25519"] - ) != 32 or not sodium.crypto_core_ed25519_is_valid_point(params["pubkey_ed25519"]): - raise SNSignatureValidationError("Ed25519 pubkey is invalid") - if len(params["pubkey_bls"]) != 64: # FIXME: bls pubkey validation? - raise SNSignatureValidationError("BLS pubkey is invalid") - if len(params["operator"]) != 20: - raise SNSignatureValidationError("operator address is invalid") - contract = params.get("contract") - if contract is not None and len(contract) != 20: - raise SNSignatureValidationError("contract address is invalid") - - signed = ( - params["pubkey_ed25519"] - + params["pubkey_bls"] - ) - - try: - VerifyKey(params["pubkey_ed25519"]).verify(signed, params["sig_ed25519"]) - except nacl.exceptions.BadSignatureError: - raise SNSignatureValidationError("Ed25519 signature is invalid") - - # FIXME: BLS verification of pubkey_bls on signed - if False: - raise SNSignatureValidationError("BLS signature is invalid") - - -class ParseError(ValueError): - def __init__(self, field, reason): - self.field = field - super().__init__(f"{field}: {reason}") - - -class ParseMissingError(ParseError): - def __init__(self, field): - super().__init__(field, f"required parameter is missing") - - -class ParseUnknownError(ParseError): - def __init__(self, field): - super().__init__(field, f"unknown parameter") - - -class ParseMultipleError(ParseError): - def __init__(self, field): - super().__init__(field, f"cannot be specified multiple times") - - -def parse_query_params(params: dict[str, Callable[[str, str], Any]]): - """ - Takes a dict of fields and callables such as: - - { - "field": ("out", callable), - ... - } - - where: - - `"field"` is the expected query string name - - `callable` will be invoked as `callable("field", value)` to determined the returned value. - - On error, throws a ParseError with `.field` set to the "field" name that triggered the error. - - Notes: - - callable should throw a ParseError for an unaccept input value. - - if "-field" starts with "-" then the field is optional; otherwise it is an error if not - provided. The "-" is not included in the returned key. - - if "field" ends with "[]" then the value will be an array of values returned by the callable, - and the parameter can be specified multiple times. Otherwise a value can be specified only - once. The "[]" is not included in the returned key. - - you can do both of the above: "-field[]" will allow the value to be provided zero or more - times; the value will be omitted if not present in the input, and an array (under the "field") - key if provided at least once. - """ - - parsed = {} - - param_map = { - k.removeprefix("-").removesuffix("[]"): ( - k.startswith("-"), - k.endswith("[]"), - cb, - ) - for k, cb in params.items() - } - - for k, v in flask.request.values.items(multi=True): - found = param_map.get(k) - if found is None: - raise ParseUnknownError(k) - - _, multi, callback = found - - if multi: - parsed.setdefault(k, []).append(callback(k, v) if callback else v) - elif k not in parsed: - parsed[k] = callback(k, v) if callback else v - else: - raise ParseMultipleError(k) - - for k, p in param_map.items(): - optional = p[0] - if not optional and k not in flask.request.values: - raise ParseMissingError(k) - - return parsed - - -@app.route("/store/", methods=["GET", "POST"]) -def store_registration(sn_pubkey: bytes): - """ - Stores (or replaces) the pubkeys/signatures associated with a service node that are needed to - call the smart contract to create a SN registration. These pubkeys/signatures are stored - indefinitely, allowing the operator to call them up whenever they like to re-submit a - registration for the same node. There is nothing confidential here: the values will be publicly - broadcast as part of the registration process already, and are constructed in such a way that - only the operator wallet can submit a registration using them. - - This works for both solo registrations and multi-registrations: for the latter, a contract - address is passed in the "c" parameter. If omitted, the details are stored for a solo - registration. (One of each may be stored at a time for each pubkey). - - The distinction at the SN layer is that contract registrations sign the contract address while - solo registrations sign the operator address. For submission to the blockchain, a contract - stake requires an additional interaction through a multi-contributor contract while solo - registrations can call the staking contract directly. - """ - - try: - params = parse_query_params( - { - "pubkey_bls": byte_decoder(64), - "sig_ed25519": byte_decoder(64), - "sig_bls": byte_decoder(128), - "-contract": raw_eth_addr, - "operator": raw_eth_addr, - } - ) - - params["pubkey_ed25519"] = sn_pubkey - - check_reg_keys_sigs(params) - except ValueError as e: - raise e - return json_response({"error": f"Invalid registration: {e}"}) - - with get_sql() as sql: - cur = sql.cursor() - cur.execute( - """ - INSERT OR REPLACE INTO registrations (pubkey_ed25519, pubkey_bls, sig_ed25519, sig_bls, operator, contract) - VALUES (?, ?, ?, ?, ?, ?) - """, - ( - sn_pubkey, - params["pubkey_bls"], - params["sig_ed25519"], - params["sig_bls"], - params["operator"], - params.get("contract"), - ), - ) - - params["operator"] = eth_utils.to_checksum_address(params["operator"]) - if "contract" in params: - params["contract"] = eth_utils.to_checksum_address(params["contract"]) - params["type"] = "contract" - else: - params["type"] = "solo" - - return json_response({"success": True, "registration": params}) - - -@app.route("/registrations/") -def sn_pubkey_registrations(sn_pubkey: bytes) -> flask.Response: - """ - Retrieves stored registration(s) for the given service node pubkey. - - This returns an array in the "registrations" field containing either one or two registration - info dicts: a solo registration (if known) and a multi-contributor contract registration (if - known). These are sorted by timestamp of when the registration was last received/updated. - - Fields in each dict: - - "type": either "solo" or "contract" - - "operator": the operator address; for "type": "contract" this is merely informative, for - "type": "solo" this is a signed part of the registration. - - "contract": the contract address, for "type": "contract" and omitted for "type": "solo". - - "pubkey_ed25519": the primary SN pubkey, in hex. - - "pubkey_bls": the SN BLS pubkey, in hex. - - "sig_ed25519": the SN pubkey signed registration signature. - - "sig_bls": the SN BLS pubkey signed registration signature. - - "timestamp": the unix timestamp when this registration was received (or last updated) - - Returns the JSON response with the 'registrations' for the given 'sn_pubkey'. - """ - - reg_array = [] - with get_sql() as sql: - cur = sql.cursor() - cur.execute( - """ - SELECT pubkey_bls, sig_ed25519, sig_bls, operator, contract, timestamp - FROM registrations - WHERE pubkey_ed25519 = ? - ORDER BY timestamp DESC - """, - (sn_pubkey,), - ) - - for pubkey_bls, sig_ed25519, sig_bls, operator, contract, timestamp in cur: - reg_array.append({ - "type": "solo" if contract is None else "contract", - "pubkey_ed25519": sn_pubkey, - "pubkey_bls": pubkey_bls, - "sig_ed25519": sig_ed25519, - "sig_bls": sig_bls, - "operator": operator, - "timestamp": timestamp, - "contract": "" if contract is None else contract, - }) - - result = json_response({"registrations": reg_array}) - return result - -@app.route("/registrations/") -def operator_registrations(operator: str): - """ - Retrieves stored registration(s) for the given 'operator'. - - This returns an array in the "registrations" field containing as many registrations as are - current stored for the given operator wallet, sorted from most to least recently submitted. - - Fields are the same as the version of this endpoint that takes a SN pubkey. - - Returns the JSON response with the 'registrations' for the given 'operator'. - """ - - reg_array = [] - operator_bytes = bytes.fromhex(operator[2:]) - - with get_sql() as sql: - cur = sql.cursor() - cur.execute( - """ - SELECT pubkey_ed25519, pubkey_bls, sig_ed25519, sig_bls, contract, timestamp - FROM registrations - WHERE operator = ? - ORDER BY timestamp DESC - """, - (operator_bytes,), - ) - for pubkey_ed25519, pubkey_bls, sig_ed25519, sig_bls, contract, timestamp in cur: - reg_array.append({ - "type": "solo" if contract is None else "contract", - "pubkey_ed25519": pubkey_ed25519, - "pubkey_bls": pubkey_bls, - "sig_ed25519": sig_ed25519, - "sig_bls": sig_bls, - "operator": operator, - "timestamp": timestamp, - "contract": "" if contract is None else contract, - }) - - result = json_response({'registrations': reg_array}) - return result - - -def check_stakes(stakes, total, stakers, max_stakers): - if len(stakers) != len(stakes): - raise ValueError(f"s and S have different lengths") - if len(stakers) < 1: - raise ValueError(f"at least one s/S value pair is required") - if len(stakers) > max_stakers: - raise ValueError(f"too many stakers ({len(stakers)} > {max_stakers})") - if sum(stakes) > total: - raise ValueError(f"total stake is too large ({sum(stakes)} > total)") - if len(set(stakers)) != len(stakers): - raise ValueError(f"duplicate staking addresses in staker list") - - remaining_stake = total - remaining_spots = max_stakers - - for i in range(len(stakes)): - reqd = remaining_stake // (4 if i == 0 else remaining_spots) - if stakes[i] < reqd: - raise ValueError( - "reserved stake [i] ({stakers[i]}) is too low ({stakes[i]} < {reqd})" - ) - remaining_stake -= stakes[i] - remaining_spots -= 1 - -def format_currency(units: int, decimal: int = 9): - """ - Formats an atomic currency unit to `decimal` decimal places. The conversion is lossless (i.e. - it does not use floating point math or involve any truncation or rounding - """ - base = 10**decimal - app.logger.debug(f"units: {units}, base: {base}, decimal: {decimal}, {units//base}") - frac = units % base - frac = "" if frac == 0 else f".{frac:0{decimal}d}".rstrip("0") - return f"{units // base}{frac}" - - -def parse_currency(k, val: str, decimal: int = 9): - """ - Losslessly parses a currency value such as 1.23 into an atomic integer value such as 1000000023. - """ - pieces = val.split(".") - if len(pieces) > 2 or not all(re.fullmatch(r"\d+", p) for p in pieces): - raise ParseError(k, "Invalid currency amount") - whole = int(pieces[0]) - if len(pieces) > 1: - frac = pieces[1] - if len(frac) > decimal: - frac = frac[0:decimal] - elif len(frac) < decimal: - frac = frac.ljust(decimal, "0") - frac = int(frac) - else: - frac = 0 - - return whole * 10**decimal + frac - - -def error_response(code, **err): - """ - Error codes that can be returned to a client when validating registration details. The `code` - is a short string that uniquely defines the error; some errors have extra parameters (passed - into the `err` kwargs). This method formats the error, then returns a dict such as: - - { "code": "short_code", "error": "English string", **err } - - This is returned, typically as an "error" key, by various endpoints. - - As a special value, if a `detail` key is present in err then the usual error will have ": - {detail}" appended to it (the detail will also be passed along separately). - """ - - err["code"] = code - match code: - case "bad_request": - msg = "Invalid request parameters" - case "invalid_op_addr": - msg = "Invalid operator address" - case "invalid_op_stake": - msg = "Invalid/unparseable operator stake" - case "wrong_op_stake": - # For a solo node that doesn't contribute the exact requirement - msg = f"Invalid operator stake: exactly {format_currency(err['required'])} {TOKEN_NAME} is required for a solo node" - case "insufficient_op_stake": - network_info = get_info() - msg = f"Insufficient operator stake: at least {format_currency(err['minimum'])} ({err['minimum'] / network_info['staking_requirement'] * 100}%) is required" - case "invalid_contract_addr": - msg = "Invalid contract address" - case "invalid_res_addr": - msg = f"Invalid reserved contributor address {err['index']}: {err['address']}" - case "invalid_res_stake": - msg = f"Invalid/unparseable reserved contributor amount for contributor {err['index']} ({err['address']})" - case "insufficient_res_stake": - msg = f"Insufficient reserved contributor stake: contributor {err['index']} ({err['address']}) must contribute at least {format_currency(err['minimum'])}" - case "too_much": - # for multi-contributor (solo node would get wrong_op_stake instead) - msg = f"Total node reserved contributions are too large: {format_currency(err['total'])} exceeds the maximum stake {format_currency(err['maximum'])}" - case "too_many": - msg = f"Too many reserved contributors: only {err['max_contributors']} contributor spots are possible" - case "invalid_fee": - msg = "Invalid fee" - case "signature": - msg = "Invalid service node registration pubkeys/signatures" - case _: - msg = None - - err["error"] = f"{msg}: {err['detail']}" if "detail" in err else msg - - return json_response({"error": err}) - - -@app.route("/validate") -def validate_registration(): - """ - Validates a registration including fee, stakes, and reserved spot requirements. This does not - use stored registration info at all; all information has to be submitted as part of the request. - The data is not stored. - - Parameters for both types of stakes: - - "pubkey_ed25519" - - "pubkey_bls" - - "sig_ed25519" - - "sig_bls" - The above are as provided by oxend for the registration. Can be hex or base64. - - - "operator" -- the operator wallet address - - "stake" -- the amount the operator will stake. For a solo stake, this must be exactly equal - to the staking requirement, but for a multi-contribution node it can be less. - - For a multi-contribution node the following must additionally be passed: - - "contract" -- the ETH address of the multi-contribution staking contract for this node. - - "reserved" -- optional list of reserved contributor wallets. - - "res_stake" -- list of reserved contributor stakes. This must be the same length and order as - `"reserved"`. - - Various checks are performed to look for registration errors; if no errors are found then the - result contains the key "success": true. Otherwise the key "error" will be set to an error dict - indicating the error that was detected. See `error_response` for details. - """ - - network_info = get_info() - max_stake = network_info['staking_requirement'] - min_operator_stake = network_info['min_operator_stake'] - max_stakers = network_info['max_stakers'] - - try: - params = parse_query_params( - { - "pubkey_ed25519": byte_decoder(32), - "pubkey_bls": byte_decoder(64), - "sig_ed25519": byte_decoder(64), - "sig_bls": byte_decoder(128), - "-contract": raw_eth_addr, - "operator": raw_eth_addr, - "stake": parse_currency, - "-res_addr[]": None, - "-res_stake[]": None, - "-fee": None, - } - ) - except (ParseMissingError, ParseUnknownError, ParseMultipleError) as e: - return error_response("bad_request", field=e.field, detail=str(e)) - except ParseError as e: - code = None - match e.field: - case f if f.startswith("pubkey_") or f.startswith("sig_"): - return error_response("signature", field=f, detail=str(e)) - case "operator": - return error_response("invalid_op_addr", detail=str(e)) - case "stake": - return error_response("invalid_op_stake") - case "contract": - return error_response("invalid_contract_addr") - case f: - return error_response("bad_request", field=f, detail=str(e)) - - try: - check_reg_keys_sigs(params) - except SNSignatureValidationError as e: - return error_response("signature", detail=str(e)) - - solo = "contract" not in params - - for k in ("addr", "stake"): - params.setdefault(f"res_{k}", []) - - if solo and params["res_addr"]: - return error_response( - "invalid_contract_addr", - detail="the contract address is required for multi-contributor registrations", - ) - - if solo and "fee" in params: - return error_response( - "invalid_fee", detail="fee is not applicable to a solo node registration" - ) - elif "fee" not in params: - return error_response( - "invalid_fee", - detail="fee is required for a multi-contribution registration", - ) - else: - fee = params["fee"] - fee = int(fee) if re.fullmatch(r"\d+", fee) else -1 - if not 0 <= fee <= 10000: - return error_response( - "invalid_fee", - detail="fee must be an integer between 0 and 10000 (= 100.00%)", - ) - - if len(params["res_addr"]) != len(params["res_stake"]): - return error_response( - "bad_request", - field="res_addr", - detail="mismatched reserved address/stake lists", - ) - - reserved = [] - for i, (addr, stake) in enumerate(zip(params["res_addr"], params["res_stake"])): - try: - eth = raw_eth_addr("res_addr", addr) - except ValueError: - return error_response("invalid_res_addr", address=eth_format(addr), index=i+1) - try: - amt = parse_currency("res_stake", stake) - except ValueError: - return error_response( - "invalid_res_stake", address=eth_format(addr), index=i+1 - ) - - reserved.append((eth, amt)) - - total_reserved = params["stake"] + sum(stake for _, stake in reserved) - if solo: - if total_reserved != max_stake: - return error_response( - "wrong_op_stake", stake=total_reserved, required=max_stake - ) - else: - if params["stake"] < min_operator_stake: - return error_response( - "insufficient_op_stake", stake=params["stake"], minimum=min_operator_stake - ) - if total_reserved > max_stake: - return error_response("too_much", total=total_reserved, maximum=max_stake) - if 1 + len(reserved) > max_stakers: - return error_response("too_many", max_contributors=max_stakers - 1) - - remaining_stake = max_stake - params["stake"] - remaining_spots = max_stakers - 1 - - for i, (addr, amt) in enumerate(reserved): - # integer math ceiling: - min_contr = (remaining_stake + remaining_spots - 1) // remaining_spots - if amt < min_contr: - return error_response( - "insufficient_res_stake", - index=i+1, - address=eth_format(addr), - minimum=min_contr, - ) - remaining_stake -= amt - remaining_spots -= 1 - - res = {"success": True} - - if not solo: - res["remaining_contribution"] = remaining_stake - res["remaining_spots"] = remaining_spots - res["remaining_min_contribution"] = ( - remaining_stake + remaining_spots - 1 - ) // remaining_spots - - return json_response(res) - -def bootstrap(): - update_service_node_contract_ids(None) - -bootstrap() diff --git a/testnet.py b/testnet.py index c5d116b..ede9943 100644 --- a/testnet.py +++ b/testnet.py @@ -1,6 +1,4 @@ -from sent import app, config -import oxenmq +from api import app, config config.testnet = True - config.oxend_rpc = 'ipc://oxend/testnet.sock' From 513c93a4a8d63e301e4d3fb160e82e525cc4b9d0 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 27 Jan 2025 19:13:16 +1100 Subject: [PATCH 069/138] feat: convert flask scripts into contained apps with parent dir runners --- Makefile | 5 - api.py | 609 ------------------ app_registration.py | 6 + app_snapshot.py | 6 + app_staking.py | 6 + db/write.py | 2 +- devnet.py | 4 - fetcher.py | 5 +- mainnet.py | 3 - registration/app.py | 127 ++++ .../app_tests.py | 5 +- registrations.py | 161 ----- snapshot.py | 49 -- __init__.py => snapshot/__init__.py | 0 snapshot/app.py | 41 ++ staking/__init__.py | 0 staking/app.py | 606 +++++++++++++++++ arbitrum.py => staking/arbitrum.py | 0 testnet.py | 4 - util/config_import.py | 9 + util/flask.py | 43 -- util/flask_utils.py | 85 +++ 22 files changed, 893 insertions(+), 883 deletions(-) delete mode 100644 Makefile delete mode 100644 api.py create mode 100644 app_registration.py create mode 100644 app_snapshot.py create mode 100644 app_staking.py delete mode 100644 devnet.py delete mode 100644 mainnet.py create mode 100644 registration/app.py rename registrations_tests.py => registration/app_tests.py (92%) delete mode 100644 registrations.py delete mode 100644 snapshot.py rename __init__.py => snapshot/__init__.py (100%) create mode 100644 snapshot/app.py create mode 100644 staking/__init__.py create mode 100644 staking/app.py rename arbitrum.py => staking/arbitrum.py (100%) delete mode 100644 testnet.py create mode 100644 util/config_import.py delete mode 100644 util/flask.py create mode 100644 util/flask_utils.py diff --git a/Makefile b/Makefile deleted file mode 100644 index e0ef739..0000000 --- a/Makefile +++ /dev/null @@ -1,5 +0,0 @@ -uwsgi: - uwsgi --http 127.0.0.1:5000 --master -p 4 -w sent --callable app - -database: - sqlite3 sent-backend.db < schema.sqlite diff --git a/api.py b/api.py deleted file mode 100644 index 30012d7..0000000 --- a/api.py +++ /dev/null @@ -1,609 +0,0 @@ -#!/usr/bin/env python3 -import dataclasses -import statistics -import flask -import time -import eth_utils -import subprocess -from eth_typing import ChecksumAddress -from uwsgidecorators import timer -from werkzeug.exceptions import GatewayTimeout - -import config -from db.dataclasses import ArbitrumInfo, DBNetworkInfo -from db.read import DBReader -from log import Log -from oxen.rpc import OxenRPC -from registration.read import DBReaderRegistrations -from util.cache import Cache -from util.parse import Hex64Converter, hexify, EthConverter, eth_format - - -class App(flask.Flask): - def __init__(self, name): - super().__init__(__name__) - log = Log(name, enable_perf=config.backend.performance_logging) - log.set_level(config.backend.log_level) - git_rev = subprocess.run( - ["git", "rev-parse", "--short=9", "HEAD"], stdout=subprocess.PIPE, text=True - ) - self.git_rev = git_rev.stdout.strip() if git_rev.returncode == 0 else "(unknown)" - - # Creates a generic logger to pipe other packages logs into the main app logger - generic_logger = Log(None) - generic_logger.set_level( - config.backend.log_level_generic - if config.backend.log_level_generic is not None - else config.backend.log_level - ) - self.log = log.logger - - self.db_reader = DBReader( - db_path=config.backend.sqlite_db, - log_level=config.backend.log_level, - perf=config.backend.performance_logging, - ) - self.db_reader_registrations = DBReaderRegistrations( - db_path=config.backend.registration_sqlite_db, - log_level=config.backend.log_level, - perf=config.backend.performance_logging, - ) - - rpc_url = config.backend.rpc_api if config.backend.rpc_api else config.backend.rpc_shared - rpc_cache = ( - config.backend.rpc_api_cache - if config.backend.rpc_api_cache - else config.backend.rpc_shared_cache - ) - - self.rpc = OxenRPC( - logger=self.log, - rpc_url=rpc_url, - cache_seconds=rpc_cache, - usage_tracking=config.backend.rpc_api_usage_logging, - ) - - self.loop_sleep_refresh_rate_seconds = rpc_cache if rpc_cache > 0 else 5 - - self.cache = Cache(stale_time_seconds=config.backend.stale_time_seconds) - - self.allowed_contract_names = set() - - -app = App(config.backend.api_name if config.backend.api_name else __name__) - - -def get_and_refresh_allowed_contract_names(): - allowed_contract_names = set() - for name in app.db_reader.get_smart_contract_names(): - allowed_contract_names.add(name) - app.allowed_contract_names = allowed_contract_names - return allowed_contract_names - - -app.url_map.converters["hex64"] = Hex64Converter -app.url_map.converters["eth_wallet"] = EthConverter - - -def get_median_operator_fee_uncached(): - # remove nodes that only have a single contributor - nodes = [n for n in get_nodes_cached() if len(n.contributors) > 1] - return statistics.median([n.operator_fee for n in nodes]) if len(nodes) > 0 else 0 - -def get_median_operator_fee_cached(): - return app.cache.get("median_operator_fee", getter=get_median_operator_fee_uncached, ttl=3600) - -def get_network_info_uncached() -> tuple[dict | None, ArbitrumInfo]: - network_info = app.db_reader.get_network_info() - arbitrum_info = app.db_reader.get_arbitrum_info() - if network_info is None: - return None, arbitrum_info - network_info = dataclasses.asdict(network_info) - network_info["median_operator_fee"] = get_median_operator_fee_cached() - return network_info, arbitrum_info - -def get_next_block_timestamp_est(): - network_info, arbitrum_info = get_network_info_cached() - return network_info["pulse_target_timestamp"] - -def get_network_info_cached(): - return app.cache.get("network_info", getter=get_network_info_uncached, ttl=1) - - -def json_response(vals, include_network_info=True): - """ - Takes a dict, adds some general info fields to it, and jsonifies it for a flask route function - return value. The dict gets passed through `hexify` first to convert any bytes values to hex. - - Note: because network_info is cached, it can be called earlier in the route and both network_info - dict will be the same in both places, and basically guaranteed cached at this stage. - """ - hexify(vals) - - data = {**vals, "t": time.time()} - - if include_network_info: - network_info, arbitrum_info = get_network_info_cached() - network_info["l2_height"] = arbitrum_info.block - network_info["l2_height_timestamp"] = arbitrum_info.timestamp - data["network"] = network_info - - return flask.jsonify(data) - - -@app.route("/info") -def get_network_info(): - return json_response({}) - - -def get_nodes_cached(): - return app.cache.get("nodes", getter=app.db_reader.get_nodes) - - -@app.route("/nodes") -def route_get_nodes(): - return json_response({"nodes": get_nodes_cached()}) - - -def get_nodes_bls_keys_cached(): - return app.cache.get("contract_node_bls_keys_added", getter=app.db_reader.get_service_node_rewards_contract_id_bls_key_map) - - -@app.route("/nodes/bls") -def route_get_nodes_bls_keys(): - return json_response({"bls_keys": get_nodes_bls_keys_cached()}) - -""" -////////////////////////////////////////////////////////////// -// // -// Stake Endpoints // -// // -////////////////////////////////////////////////////////////// -""" - -def get_related_stakes_for_eth_address_uncached(address: ChecksumAddress): - nodes = get_nodes_cached() - - related_nodes = [] - for node in nodes: - if eth_format(node.operator_address) == address: - related_nodes.append(node) - elif node.contributors is not None: - for contributor in node.contributors: - if eth_format(contributor.address) == address: - related_nodes.append(node) - - return related_nodes - -def get_related_stakes_for_eth_address_cached(address: ChecksumAddress): - return app.cache.get("related-stakes-{}".format(address), getter=get_related_stakes_for_eth_address_uncached, getter_args=address) - -# TODO: might make sense to investigate storing contributor and operator addresses in the db as blobs and compare with bytes -@app.route("/stakes/") -@app.route("/nodes/") -def route_get_stakes_for_eth_address(eth_wal: str): - try: - address = eth_format(eth_wal) - return json_response({"stakes": get_related_stakes_for_eth_address_cached(address), "contracts": get_related_contribution_contracts_for_eth_address_cached(address), "added_bls_keys": get_nodes_bls_keys_cached()}) - - except ValueError as e: - app.logger.error(f"Exception: {e}") - return flask.abort(400, e) - except Exception as e: - app.logger.error(f"Exception: {e}") - app.logger.exception(e) - return flask.abort(500, e) - - -@app.route("/stakes/") -@app.route("/nodes/") -def route_get_stakes_for_sn_pubkey(sn_pubkey: bytes): - try: - nodes = get_nodes_cached() - related_nodes = [node for node in nodes if node.pubkey_ed25519 == sn_pubkey] - return json_response({"stakes": related_nodes}) - - except Exception as e: - app.logger.error(f"Exception: {e}") - return flask.abort(500, e) - - -""" -////////////////////////////////////////////////////////////// -// // -// Contract Endpoints // -// // -////////////////////////////////////////////////////////////// -""" - - -def get_cached_allowed_contract_names(): - return app.cache.get( - "allowed_contract_names", - getter=get_and_refresh_allowed_contract_names, - ttl=config.backend.stale_time_seconds_contract_abis - ) - - -@app.route("/contract/names") -def route_get_abi_names(): - return json_response({"names": list(get_cached_allowed_contract_names())}) - - -@app.route("/contract/abis") -def route_get_abis(): - return json_response( - {"abis": app.cache.get("abis_all", getter=app.db_reader.get_smart_contract_abis, - ttl=config.backend.stale_time_seconds_contract_abis)} - ) - - -@app.route("/contract/addresses") -def get_contract_addresses(): - return json_response( - {"addresses": app.cache.get("addresses_all", getter=app.db_reader.get_smart_contract_addresses)} - ) - -@app.route("/contract/addresses/core") -def get_contract_addresses_core(): - return json_response( - {"addresses": app.cache.get("addresses_core", getter=app.db_reader.get_smart_contract_addresses_core, - ttl=config.backend.stale_time_seconds_contract_abis)} - ) - -def get_contribution_contracts_cached(): - return app.cache.get("contracts", getter=app.db_reader.get_contribution_contracts, ttl=2) - -@app.route("/contract/contribution") -def get_open_contract_details(): - return json_response( - {"contracts": get_contribution_contracts_cached(), "added_bls_keys": get_nodes_bls_keys_cached()} - ) - -def get_contribution_contracts_for_sn_pubkey_uncached(sn_pubkey: bytes): - cached_contracts = app.db_reader.get_contribution_contracts() - contracts = [contract for contract in cached_contracts if contract.service_node_pubkey == sn_pubkey] - return contracts - -@app.route("/contract/contribution/") -def get_contribution_contract_for_sn_pubkey_cached(sn_pubkey: bytes): - key = sn_pubkey.hex() - return json_response( - {"contracts": app.cache.get("contract-sn-{}".format(key), getter=get_contribution_contracts_for_sn_pubkey_uncached, getter_args=key, ttl=2)} - ) - -def get_related_contribution_contracts_for_eth_address_uncached(eth_wal: str): - contracts = get_contribution_contracts_cached() - - related_contracts = [] - for contract in contracts: - if contract.operator_address == eth_wal: - related_contracts.append(contract) - elif contract.contributors is not None: - for contributor in contract.contributors: - if contributor.address == eth_wal: - related_contracts.append(contract) - return related_contracts - -def get_related_contribution_contracts_for_eth_address_cached(eth_wal: str): - return app.cache.get("related-contracts-{}".format(eth_wal), getter=get_related_contribution_contracts_for_eth_address_uncached, getter_args=eth_wal) - -@app.route("/contract/contribution/") -def get_contribution_contracts_for_wallet(eth_wal: str): - try: - if not eth_wal or not eth_utils.is_address(eth_wal): - raise ValueError("Invalid wallet address") - - return json_response({"contracts": get_related_contribution_contracts_for_eth_address_cached(eth_wal)}) - - except ValueError as e: - app.logger.error(f"Exception: {e}") - return flask.abort(400, e) - except Exception as e: - app.logger.error(f"Exception: {e}") - return flask.abort(500, e) - - -@app.route("/contract/abi/") -def get_abi(contract_name: str): - if contract_name not in get_cached_allowed_contract_names(): - return flask.abort(404, f"Contract {contract_name} not found") - - return json_response( - { - "contract": app.cache.get( - "abi-{}".format(contract_name), getter=app.db_reader.get_smart_contract_abi, getter_args=contract_name, - ttl=config.backend.stale_time_seconds_contract_abis - ) - } - ) - - -@app.route("/contract/address/") -def get_contract_address(contract_name: str): - if contract_name not in get_cached_allowed_contract_names(): - return flask.abort(404, f"Contract {contract_name} not found") - - return json_response( - { - "address": app.cache.get( - "address-{}".format(contract_name), - getter=app.db_reader.get_smart_contract_address, - getter_args=contract_name, - ) - } - ) - - -""" -////////////////////////////////////////////////////////////// -// // -// Event Endpoints // -// // -////////////////////////////////////////////////////////////// -""" - -def get_events_handler(count_limit=500, skip=0): - limit = min(count_limit, 500) - events, limit, skip, total = app.cache.get("events-{}-{}".format(count_limit, skip), getter=app.db_reader.get_arbitrum_events_page, getter_args=[limit, skip], ttl=10) - pagination = {"limit": limit, "skip": skip, "total": total} - - return {"events": events, "pagination": pagination} - -@app.route("/events//") -def get_events(count: int, skip: int): - return json_response(get_events_handler(count, skip)) - -def get_arbitrum_info_uncached(): - return app.db_reader.get_arbitrum_info() - -@app.route("/arbitrum-info") -def get_arbitrum_info(): - return json_response({"info": app.cache.get("arbitrum-info", getter=get_arbitrum_info_uncached)}) - -@app.route("/stake-events/") -def get_stake_events(contract_id: int): - if contract_id < 0: - return flask.abort(400, "Invalid contract ID") - - return json_response({"events": app.cache.get("stake-events-{}".format(contract_id), getter=app.db_reader.get_arbitrum_events_for_stake_contrat_id, getter_args=contract_id)}) - -""" -////////////////////////////////////////////////////////////// -// // -// Exit Endpoints // -// // -////////////////////////////////////////////////////////////// -""" - - -def handle_get_exit_and_liquidation(params: [bytes, bool]): - ed25519_pubkey, liquidate = params[0], params[1] - if ed25519_pubkey not in get_exitable_ed25519_keys_cached(): - return flask.abort(404, f"No exit available for {ed25519_pubkey.hex()}") - - response = app.rpc.bls_exit_liquidation_request(ed25519_pubkey, liquidate).get() - if response is None: - raise GatewayTimeout("Failed to get exit signature") - if "status" in response: - response.pop("status") - - return json_response({"result": response}) - - -def handle_get_exit_and_liquidation_cached(params: [bytes, bool]): - try: - return app.cache.get(f"exit-{params[0]}-{params[1]}", getter=handle_get_exit_and_liquidation, getter_args=params, - invalidate_timestamp=get_next_block_timestamp_est()) - except GatewayTimeout as e: - app.logger.error(f"Exception: {e}") - return flask.abort(504) # Gateway timeout - except TimeoutError: - return flask.abort(408) # Request timeout - except Exception as e: - app.logger.error(f"Exception: {e}") - return flask.abort(500, e) - - -@app.route("/exit/") -def route_get_exit(ed25519_pubkey: bytes): - return handle_get_exit_and_liquidation_cached([ed25519_pubkey, False]) - - -@app.route("/liquidation/") -def route_get_liquidation(ed25519_pubkey: bytes): - return handle_get_exit_and_liquidation_cached([ed25519_pubkey, True]) - - -def get_exit_liquidation_list_uncached(): - return app.rpc.bls_exit_liquidation_list().get() - - -def get_exit_liquidation_list_cached(): - return app.cache.get("exit_liquidation_list", getter=get_exit_liquidation_list_uncached, - invalidate_timestamp=get_next_block_timestamp_est()) - - -@app.route("/exit_liquidation_list") -def route_get_exit_liquidation_list(): - return json_response( - {"result": get_exit_liquidation_list_cached()} - ) - - -def get_exitable_ed25519_keys_uncached(): - return set([bytes.fromhex(x.get("service_node_pubkey")) for x in get_exit_liquidation_list_cached()]) - - -def get_exitable_ed25519_keys_cached(): - return app.cache.get("exitable_ed25519_keys", getter=get_exitable_ed25519_keys_uncached, - invalidate_timestamp=get_next_block_timestamp_est()) - -""" -////////////////////////////////////////////////////////////// -// // -// Rewards Endpoints // -// // -////////////////////////////////////////////////////////////// -""" - - -def get_rewards_signature_uncached(eth_wal: str): - address = eth_format(eth_wal) - response = app.rpc.bls_rewards_request(address).get() - if response is None: - raise TimeoutError("Failed to get rewards signature") - - response.pop("status") if "status" in response else None - response.pop("address") if "address" in response else None - - return response - - -def get_rewards_info_cached(): - # We cache all rewards info for all wallets so we don't need to multiple reads in a short period of time - return app.cache.get(f"rewards_info", getter=app.db_reader.get_rewards_info, - invalidate_timestamp=get_next_block_timestamp_est()) - - -def get_rewards_info_for_address_cached(eth_wal: str): - address = eth_format(eth_wal) - rewards_info = get_rewards_info_cached() - return rewards_info.get(address, 0) - - -def get_rewards_info_response(eth_wal: str): - return json_response({"rewards": get_rewards_info_for_address_cached(eth_wal)}) - - -def get_rewards_signature_response(eth_wal: str): - try: - rewards = get_rewards_info_for_address_cached(eth_wal) - if rewards == 0: - return flask.abort(404, f"No rewards available for {eth_wal}") - - return json_response({"rewards": app.cache.get(f"rewards-sig-{eth_wal}", getter=get_rewards_signature_uncached, - getter_args=eth_wal, - invalidate_timestamp=get_next_block_timestamp_est())}) - except ValueError as e: - return flask.abort(400, str(e)) - - -@app.route("/rewards/", methods=["GET", "POST"]) -def get_rewards(eth_wal: str): - if flask.request.method == "GET": - return app.cache.get(f"rewards-info-response-{eth_wal}", getter=get_rewards_info_response, getter_args=eth_wal, - invalidate_timestamp=get_next_block_timestamp_est()) - - if flask.request.method == "POST": - try: - return app.cache.get(f"rewards-sig-response-{eth_wal}", getter=get_rewards_signature_response, - getter_args=eth_wal, invalidate_timestamp=get_next_block_timestamp_est()) - except TimeoutError: - # We don't want to cache a 408 response - return flask.abort(408) - - return flask.abort(405) # Method not allowed - - -""" -////////////////////////////////////////////////////////////// -// // -// Registration Endpoints // -// // -////////////////////////////////////////////////////////////// -""" - - -@app.route("/registrations/") -def operator_registrations(operator: str): - """ - Retrieves stored registration(s) for the given 'operator'. - - This returns an array in the "registrations" field containing as many registrations as are - currently stored for the given operator wallet, sorted from most to least recently submitted. - - Fields are the same as the version of this endpoint that takes a SN pubkey. - - Returns the JSON response with the 'registrations' for the given 'operator'. - """ - - operator_bytes = bytes.fromhex(operator[2:]) - - return json_response( - { - "registrations": app.cache.get( - f"registrations-op-{operator_bytes}", - getter=app.db_reader_registrations.get_registrations_for_operator, - getter_args=operator_bytes, - ) - } - ) - - -@app.route("/registrations/") -def sn_pubkey_registrations(sn_pubkey: bytes) -> flask.Response: - """ - Retrieves stored registration(s) for the given service node pubkey. - - This returns an array in the "registrations" field containing either one or two registration - info dicts: a solo registration (if known) and a multi-contributor contract registration (if - known). These are sorted by timestamp of when the registration was last received/updated. - - Fields in each dict: - - "operator": the operator address. - - "contract": the contract address, for "type": "contract" and omitted for "type": "solo". - - "pubkey_ed25519": the primary SN pubkey, in hex. - - "pubkey_bls": the SN BLS pubkey, in hex. - - "sig_ed25519": the SN pubkey signed registration signature. - - "sig_bls": the SN BLS pubkey signed registration signature. - - "timestamp": the unix timestamp when this registration was received (or last updated) - - Returns the JSON response with the 'registrations' for the given 'sn_pubkey'. - """ - result = json_response( - { - "registrations": app.cache.get( - f"registration-sn-{sn_pubkey}", - getter=app.db_reader_registrations.get_registrations_by_pubkey, - getter_args=sn_pubkey, - ) - } - ) - return result - -""" -////////////////////////////////////////////////////////////// -// // -// Utility // -// // -////////////////////////////////////////////////////////////// -""" - -def bootstrap(): - get_and_refresh_allowed_contract_names() - - -bootstrap() - - -if config.backend.rpc_api_usage_logging: - def log_rpc_usage(signum): - app.rpc.usage_tracker.log_usage(" For signum {}".format(signum)) - app.rpc.usage_tracker.write_failure_reasons_to_file(f"rpc-usage-failure-reasons-{signum}.txt") - - @timer(config.backend.rpc_api_usage_logging_interval, target="worker1") - def log_rpc_usage_w1(signum): - log_rpc_usage(signum) - - @timer(config.backend.rpc_api_usage_logging_interval, target="worker2") - def log_rpc_usage_w2(signum): - log_rpc_usage(signum) - - @timer(config.backend.rpc_api_usage_logging_interval, target="worker3") - def log_rpc_usage_w3(signum): - log_rpc_usage(signum) - - @timer(config.backend.rpc_api_usage_logging_interval, target="worker4") - def log_rpc_usage_w4(signum): - log_rpc_usage(signum) \ No newline at end of file diff --git a/app_registration.py b/app_registration.py new file mode 100644 index 0000000..0bd3a14 --- /dev/null +++ b/app_registration.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 + +import config +from registration.app import create_app + +app = create_app(config) \ No newline at end of file diff --git a/app_snapshot.py b/app_snapshot.py new file mode 100644 index 0000000..2d56646 --- /dev/null +++ b/app_snapshot.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 + +import config +from snapshot.app import create_app + +app = create_app(config) \ No newline at end of file diff --git a/app_staking.py b/app_staking.py new file mode 100644 index 0000000..4fd2d13 --- /dev/null +++ b/app_staking.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 + +import config +from staking.app import create_app + +app = create_app(config) \ No newline at end of file diff --git a/db/write.py b/db/write.py index 1b3dec5..09525a9 100644 --- a/db/write.py +++ b/db/write.py @@ -5,7 +5,7 @@ from web3 import Web3 -from arbitrum import ContributionContractDetails +from staking.arbitrum import ContributionContractDetails from db.dataclasses import RewardsInfo, DBNodeExit from log import Log from oxen.rpc import ServiceNode, NetworkInfo diff --git a/devnet.py b/devnet.py deleted file mode 100644 index f45a157..0000000 --- a/devnet.py +++ /dev/null @@ -1,4 +0,0 @@ -from api import app, config - -config.devnet = True -config.oxend_rpc = 'ipc://oxend/devnet.sock' diff --git a/fetcher.py b/fetcher.py index 6ad308a..9464521 100644 --- a/fetcher.py +++ b/fetcher.py @@ -4,7 +4,7 @@ import time import config -from arbitrum import ( +from staking.arbitrum import ( get_service_node_rewards_contract_id_map, get_new_contribution_contracts, update_contribution_contract_details, batch_populate_events_with_block_timestamps, populate_events_with_main_arg, @@ -20,7 +20,7 @@ from db.write import DBWriter from log import Log from oxen.rpc import ServiceNode, OxenRPC, NetworkInfo -from util import format_seconds, is_not_empty_string +from util import format_seconds from log.time_keeper import TimeKeeper from util.parse import parse_bls_pubkey from web3client.abi_manager import ABIManager @@ -34,7 +34,6 @@ ) from web3client.contracts.service_node_rewards import ServiceNodeRewardsInterface from web3client.contracts.token import TokenInterface -from oxen.omq import omq_connection class App: diff --git a/mainnet.py b/mainnet.py deleted file mode 100644 index db7787e..0000000 --- a/mainnet.py +++ /dev/null @@ -1,3 +0,0 @@ -from api import app, config - -config.oxend_rpc = 'ipc://oxend/mainnet.sock' diff --git a/registration/app.py b/registration/app.py new file mode 100644 index 0000000..be26e57 --- /dev/null +++ b/registration/app.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +import eth_utils +from util.flask_utils import FlaskApp, FlaskReqLimiter, json_response +from werkzeug.middleware.proxy_fix import ProxyFix +from db.util import is_db_initialized, init_db +from registration.read import DBReaderRegistrations +from registration.validation import check_reg_keys_sigs +from registration.write import DBWriterRegistrations +from util.parse import ( + parse_query_params, + byte_decoder, + EthConverter, + hexify, + Hex64Converter, + raw_eth_addr, +) + + +class App(FlaskApp): + def __init__(self, config): + name = config.backend.registration_api_name if config.backend.registration_api_name else __name__ + super().__init__(name, enable_perf=config.backend.performance_logging, + log_level=config.backend.log_level, + cache_stale_time_seconds=config.backend.stale_time_seconds) + + if not is_db_initialized(config.backend.registration_sqlite_db): + self.log.info( + "Initializing database {} with schema {}".format( + config.backend.registration_sqlite_db, config.backend.registration_sqlite_schema + ) + ) + init_db( + config.backend.registration_sqlite_db, config.backend.registration_sqlite_schema + ) + + self.db_reader = DBReaderRegistrations( + db_path=config.backend.registration_sqlite_db, + log_level=config.backend.log_level, + perf=config.backend.performance_logging, + ) + self.db_writer = DBWriterRegistrations( + db_path=config.backend.registration_sqlite_db, + log_level=config.backend.log_level, + perf=config.backend.performance_logging, + ) + + self.allowed_contract_names = set() + self.log.info( + f"IP Rate limit: {config.backend.registration_api_rate_limit} per {config.backend.registration_api_rate_limit_period} seconds") + + +def create_app(config) -> App: + app = App(config) + + # Enables more reliable proxy pass through for rate limiting + app.wsgi_app = ProxyFix(app.wsgi_app) + app.req_limiter = FlaskReqLimiter(max_reqs_per_sec=config.backend.registration_api_rate_limit, + rate_limit_period=config.backend.registration_api_rate_limit_period) + + @app.before_request + def rate_limit(): + return app.req_limiter.rate_limit() + + app.url_map.converters["hex64"] = Hex64Converter + app.url_map.converters["eth_wallet"] = EthConverter + + @app.route("/info") + def get_network_info(): + return json_response() + + """ + ////////////////////////////////////////////////////////////// + // // + // Registration Endpoints // + // // + ////////////////////////////////////////////////////////////// + """ + + # NOTE: the /api prefix route here is to allow for local testing + + @app.route("/api/store/", methods=["GET", "POST"]) + @app.route("/registrations/", methods=["POST"]) + @app.route("/store/", methods=["GET", "POST"]) + def store_registration(sn_pubkey: bytes): + """ + Stores (or replaces) the pubkeys/signatures associated with a service node that are needed to + call the smart contract to create a SN registration. These pubkeys/signatures are stored + indefinitely, allowing the operator to call them up whenever they like to re-submit a + registration for the same node. There is nothing confidential here: the values will be publicly + broadcast as part of the registration process already, and are constructed in such a way that + only the operator wallet can submit a registration using them. + + This works for both solo registrations and multi-registrations: for the latter, a contract + address is passed in the "c" parameter. + + The distinction at the SN layer is that contract registrations sign the contract address while + solo registrations sign the operator address. For submission to the blockchain, a contract + stake requires an additional interaction through a multi-contributor contract while solo + registrations can call the staking contract directly. + """ + + try: + params = parse_query_params( + { + "pubkey_bls": byte_decoder(64), + "sig_ed25519": byte_decoder(64), + "sig_bls": byte_decoder(128), + "operator": raw_eth_addr, + } + ) + + params["pubkey_ed25519"] = sn_pubkey + + check_reg_keys_sigs(params) + except ValueError as e: + return json_response({"error": f"Invalid registration: {e}"}) + + app.db_writer.write_registration_to_db(params) + + params["operator"] = eth_utils.to_checksum_address(params["operator"]) + params["contract"] = ( + eth_utils.to_checksum_address(params["contract"]) if "contract" in params else None + ) + + return json_response({"success": True, "registration": params}) + + return app diff --git a/registrations_tests.py b/registration/app_tests.py similarity index 92% rename from registrations_tests.py rename to registration/app_tests.py index 978fc58..a149cb4 100644 --- a/registrations_tests.py +++ b/registration/app_tests.py @@ -1,7 +1,10 @@ import time import pytest -from registrations import app +from registration.app import create_app +from util.config_import import import_config +config = import_config() +app = create_app(config) @pytest.fixture() def client(): diff --git a/registrations.py b/registrations.py deleted file mode 100644 index 56ebc1b..0000000 --- a/registrations.py +++ /dev/null @@ -1,161 +0,0 @@ -#!/usr/bin/env python3 -import flask -import time - -import eth_utils -import subprocess - -from werkzeug.middleware.proxy_fix import ProxyFix -from util.flask import FlaskReqLimiter - -import config - -from db.util import is_db_initialized, init_db -from log import Log -from registration.read import DBReaderRegistrations -from registration.validation import check_reg_keys_sigs -from registration.write import DBWriterRegistrations -from util.cache import Cache -from util.parse import ( - parse_query_params, - byte_decoder, - EthConverter, - hexify, - Hex64Converter, - raw_eth_addr, -) - - -class App(flask.Flask): - def __init__(self, name): - super().__init__(__name__) - log = Log(name, enable_perf=config.backend.performance_logging) - log.set_level(config.backend.log_level) - git_rev = subprocess.run( - ["git", "rev-parse", "--short=9", "HEAD"], stdout=subprocess.PIPE, text=True - ) - self.git_rev = git_rev.stdout.strip() if git_rev.returncode == 0 else "(unknown)" - - # Creates a generic logger to pipe other packages logs into the main app logger - generic_logger = Log(None) - generic_logger.set_level( - config.backend.log_level_generic - if config.backend.log_level_generic is not None - else config.backend.log_level - ) - self.log = log.logger - - if not is_db_initialized(config.backend.registration_sqlite_db): - self.log.info( - "Initializing database {} with schema {}".format( - config.backend.registration_sqlite_db, config.backend.registration_sqlite_schema - ) - ) - init_db( - config.backend.registration_sqlite_db, config.backend.registration_sqlite_schema - ) - - self.db_reader = DBReaderRegistrations( - db_path=config.backend.registration_sqlite_db, - log_level=config.backend.log_level, - perf=config.backend.performance_logging, - ) - self.db_writer = DBWriterRegistrations( - db_path=config.backend.registration_sqlite_db, - log_level=config.backend.log_level, - perf=config.backend.performance_logging, - ) - - self.cache = Cache(stale_time_seconds=config.backend.stale_time_seconds) - - self.allowed_contract_names = set() - - self.log.info(f"IP Rate limit: {config.backend.registration_api_rate_limit} per {config.backend.registration_api_rate_limit_period} seconds") - - -app = App( - config.backend.registration_api_name if config.backend.registration_api_name else __name__ -) - -# Enables more reliable proxy pass through for rate limiting -app.wsgi_app = ProxyFix(app.wsgi_app) -app.req_limiter = FlaskReqLimiter(max_reqs_per_sec=config.backend.registration_api_rate_limit, rate_limit_period=config.backend.registration_api_rate_limit_period) - -@app.before_request -def rate_limit(): - return app.req_limiter.rate_limit() - -app.url_map.converters["hex64"] = Hex64Converter -app.url_map.converters["eth_wallet"] = EthConverter - - -def json_response(vals): - """ - Takes a dict, adds some general info fields to it, and jsonifies it for a flask route function - return value. The dict gets passed through `hexify` first to convert any bytes values to hex. - """ - hexify(vals) - return flask.jsonify({**vals, "t": time.time()}) - - -@app.route("/info") -def get_network_info(): - return json_response({}) - - -""" -////////////////////////////////////////////////////////////// -// // -// Registration Endpoints // -// // -////////////////////////////////////////////////////////////// -""" - -# NOTE: the /api prefix route here is to allow for local testing - -@app.route("/api/store/", methods=["GET", "POST"]) -@app.route("/registrations/", methods=["POST"]) -@app.route("/store/", methods=["GET", "POST"]) -def store_registration(sn_pubkey: bytes): - """ - Stores (or replaces) the pubkeys/signatures associated with a service node that are needed to - call the smart contract to create a SN registration. These pubkeys/signatures are stored - indefinitely, allowing the operator to call them up whenever they like to re-submit a - registration for the same node. There is nothing confidential here: the values will be publicly - broadcast as part of the registration process already, and are constructed in such a way that - only the operator wallet can submit a registration using them. - - This works for both solo registrations and multi-registrations: for the latter, a contract - address is passed in the "c" parameter. - - The distinction at the SN layer is that contract registrations sign the contract address while - solo registrations sign the operator address. For submission to the blockchain, a contract - stake requires an additional interaction through a multi-contributor contract while solo - registrations can call the staking contract directly. - """ - - try: - params = parse_query_params( - { - "pubkey_bls": byte_decoder(64), - "sig_ed25519": byte_decoder(64), - "sig_bls": byte_decoder(128), - "operator": raw_eth_addr, - } - ) - - params["pubkey_ed25519"] = sn_pubkey - - check_reg_keys_sigs(params) - except ValueError as e: - return json_response({"error": f"Invalid registration: {e}"}) - - app.db_writer.write_registration_to_db(params) - - params["operator"] = eth_utils.to_checksum_address(params["operator"]) - params["contract"] = ( - eth_utils.to_checksum_address(params["contract"]) if "contract" in params else None - ) - - return json_response({"success": True, "registration": params}) - diff --git a/snapshot.py b/snapshot.py deleted file mode 100644 index 618834b..0000000 --- a/snapshot.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env python3 -import flask -import subprocess -from uwsgidecorators import timer -import config -from db.snapshot import DBSnapshot -from log import Log - - -class App(flask.Flask): - def __init__(self, name): - super().__init__(__name__) - log = Log(name, enable_perf=config.backend.performance_logging) - log.set_level(config.backend.log_level) - git_rev = subprocess.run( - ["git", "rev-parse", "--short=9", "HEAD"], stdout=subprocess.PIPE, text=True - ) - self.git_rev = git_rev.stdout.strip() if git_rev.returncode == 0 else "(unknown)" - - self.log = log.logger - - assert config.backend.sqlite_snapshot_time_interval_seconds > 60, "Snapshot interval must be greater than 60 seconds" - - self.db_snapshot = DBSnapshot( - source_db_path=config.backend.sqlite_db, - snapshot_db_path=config.backend.sqlite_db_snapshot, - excluded_tables={ - # Staging rows, these are committed to the main db once the immutable height is reached, this can be synced by the end user - "service_nodes_staging", - "service_nodes_contributions_staging", - }, - log_level=config.backend.log_level, - perf=config.backend.performance_logging, - ) - - self.log.info("Snapshot task initialized, will run every {} seconds.".format(config.backend.sqlite_snapshot_time_interval_seconds)) - - -app = App( - config.backend.snapshot_task_name if config.backend.snapshot_task_name else __name__ -) - - -@timer(config.backend.sqlite_snapshot_time_interval_seconds) -def snapshot_db(signum): - app.db_snapshot.snapshot() - -if config.backend.snapshot_on_startup: - snapshot_db(None) \ No newline at end of file diff --git a/__init__.py b/snapshot/__init__.py similarity index 100% rename from __init__.py rename to snapshot/__init__.py diff --git a/snapshot/app.py b/snapshot/app.py new file mode 100644 index 0000000..b72a439 --- /dev/null +++ b/snapshot/app.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +from uwsgidecorators import timer +from db.snapshot import DBSnapshot +from util.flask_utils import FlaskApp + + +class App(FlaskApp): + def __init__(self, config): + name = config.backend.snapshot_task_name if config.backend.snapshot_task_name else __name__ + super().__init__(name, enable_perf=config.backend.performance_logging, + log_level=config.backend.log_level) + + assert config.backend.sqlite_snapshot_time_interval_seconds > 29, "Snapshot interval must be greater than 29 seconds" + + self.db_snapshot = DBSnapshot( + source_db_path=config.backend.sqlite_db, + snapshot_db_path=config.backend.sqlite_db_snapshot, + excluded_tables={ + # Staging rows, these are committed to the main db once the immutable height is reached, this can be synced by the end user + "service_nodes_staging", + "service_nodes_contributions_staging", + }, + log_level=config.backend.log_level, + perf=config.backend.performance_logging, + ) + + self.log.info("Snapshot task initialized, will run every {} seconds.".format( + config.backend.sqlite_snapshot_time_interval_seconds)) + + +def create_app(config) -> App: + app = App(config) + + @timer(config.backend.sqlite_snapshot_time_interval_seconds) + def snapshot_db(signum): + app.db_snapshot.snapshot() + + if config.backend.snapshot_on_startup: + snapshot_db(None) + + return app diff --git a/staking/__init__.py b/staking/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/staking/app.py b/staking/app.py new file mode 100644 index 0000000..bd9e33e --- /dev/null +++ b/staking/app.py @@ -0,0 +1,606 @@ +#!/usr/bin/env python3 +import dataclasses +import statistics +import flask +import eth_utils +from eth_typing import ChecksumAddress +from uwsgidecorators import timer +from werkzeug.exceptions import GatewayTimeout +from db.dataclasses import ArbitrumInfo, DBNetworkInfo +from db.read import DBReader +from oxen.rpc import OxenRPC +from registration.read import DBReaderRegistrations +from util.flask_utils import FlaskApp, json_response +from util.parse import Hex64Converter, hexify, EthConverter, eth_format + + +class App(FlaskApp): + def __init__(self, config): + name = config.backend.registration_api_name if config.backend.registration_api_name else __name__ + super().__init__(name, enable_perf=config.backend.performance_logging, + log_level=config.backend.log_level, log_level_generic=config.backend.log_level_generic, + cache_stale_time_seconds=config.backend.stale_time_seconds) + + self.db_reader = DBReader( + db_path=config.backend.sqlite_db, + log_level=config.backend.log_level, + perf=config.backend.performance_logging, + ) + self.db_reader_registrations = DBReaderRegistrations( + db_path=config.backend.registration_sqlite_db, + log_level=config.backend.log_level, + perf=config.backend.performance_logging, + ) + + rpc_url = config.backend.rpc_api if config.backend.rpc_api else config.backend.rpc_shared + rpc_cache = ( + config.backend.rpc_api_cache + if config.backend.rpc_api_cache + else config.backend.rpc_shared_cache + ) + + self.rpc = OxenRPC( + logger=self.log, + rpc_url=rpc_url, + cache_seconds=rpc_cache, + usage_tracking=config.backend.rpc_api_usage_logging, + ) + + self.allowed_contract_names = set() + + +def create_app(config) -> App: + app = App(config) + + def get_and_refresh_allowed_contract_names(): + allowed_contract_names = set() + for name in app.db_reader.get_smart_contract_names(): + allowed_contract_names.add(name) + app.allowed_contract_names = allowed_contract_names + return allowed_contract_names + + app.url_map.converters["hex64"] = Hex64Converter + app.url_map.converters["eth_wallet"] = EthConverter + + def get_median_operator_fee_uncached(): + # remove nodes that only have a single contributor + nodes = [n for n in get_nodes_cached() if len(n.contributors) > 1] + return statistics.median([n.operator_fee for n in nodes]) if len(nodes) > 0 else 0 + + def get_median_operator_fee_cached(): + return app.cache.get("median_operator_fee", getter=get_median_operator_fee_uncached, ttl=3600) + + def get_network_info_uncached() -> tuple[dict | None, ArbitrumInfo]: + network_info = app.db_reader.get_network_info() + arbitrum_info = app.db_reader.get_arbitrum_info() + if network_info is None: + return None, arbitrum_info + network_info = dataclasses.asdict(network_info) + network_info["median_operator_fee"] = get_median_operator_fee_cached() + return network_info, arbitrum_info + + def get_next_block_timestamp_est(): + network_info, arbitrum_info = get_network_info_cached() + return network_info["pulse_target_timestamp"] + + def get_network_info_cached(): + return app.cache.get("network_info", getter=get_network_info_uncached, ttl=1) + + def json_res(vals, include_network_info=True): + if include_network_info: + network_info, arbitrum_info = get_network_info_cached() + network_info["l2_height"] = arbitrum_info.block + network_info["l2_height_timestamp"] = arbitrum_info.timestamp + vals["network"] = network_info + + return json_response(vals) + + @app.route("/info") + def get_network_info(): + return json_res({}) + + def get_nodes_cached(): + return app.cache.get("nodes", getter=app.db_reader.get_nodes) + + @app.route("/nodes") + def route_get_nodes(): + return json_res({"nodes": get_nodes_cached()}) + + def get_nodes_bls_keys_cached(): + return app.cache.get("contract_node_bls_keys_added", + getter=app.db_reader.get_service_node_rewards_contract_id_bls_key_map) + + @app.route("/nodes/bls") + def route_get_nodes_bls_keys(): + return json_res({"bls_keys": get_nodes_bls_keys_cached()}) + + """ + ////////////////////////////////////////////////////////////// + // // + // Stake Endpoints // + // // + ////////////////////////////////////////////////////////////// + """ + + def get_related_stakes_for_eth_address_uncached(address: ChecksumAddress): + nodes = get_nodes_cached() + + related_nodes = [] + for node in nodes: + if eth_format(node.operator_address) == address: + related_nodes.append(node) + elif node.contributors is not None: + for contributor in node.contributors: + if eth_format(contributor.address) == address: + related_nodes.append(node) + + return related_nodes + + def get_related_stakes_for_eth_address_cached(address: ChecksumAddress): + return app.cache.get("related-stakes-{}".format(address), getter=get_related_stakes_for_eth_address_uncached, + getter_args=address) + + # TODO: might make sense to investigate storing contributor and operator addresses in the db as blobs and compare with bytes + @app.route("/stakes/") + @app.route("/nodes/") + def route_get_stakes_for_eth_address(eth_wal: str): + try: + address = eth_format(eth_wal) + return json_res({"stakes": get_related_stakes_for_eth_address_cached(address), + "contracts": get_related_contribution_contracts_for_eth_address_cached(address), + "added_bls_keys": get_nodes_bls_keys_cached()}) + + except ValueError as e: + app.logger.error(f"Exception: {e}") + return flask.abort(400, e) + except Exception as e: + app.logger.error(f"Exception: {e}") + app.logger.exception(e) + return flask.abort(500, e) + + @app.route("/stakes/") + @app.route("/nodes/") + def route_get_stakes_for_sn_pubkey(sn_pubkey: bytes): + try: + nodes = get_nodes_cached() + related_nodes = [node for node in nodes if node.pubkey_ed25519 == sn_pubkey] + return json_res({"stakes": related_nodes}) + + except Exception as e: + app.logger.error(f"Exception: {e}") + return flask.abort(500, e) + + """ + ////////////////////////////////////////////////////////////// + // // + // Contract Endpoints // + // // + ////////////////////////////////////////////////////////////// + """ + + def get_cached_allowed_contract_names(): + return app.cache.get( + "allowed_contract_names", + getter=get_and_refresh_allowed_contract_names, + ttl=config.backend.stale_time_seconds_contract_abis + ) + + @app.route("/contract/names") + def route_get_abi_names(): + return json_res({"names": list(get_cached_allowed_contract_names())}) + + @app.route("/contract/abis") + def route_get_abis(): + return json_res( + {"abis": app.cache.get("abis_all", getter=app.db_reader.get_smart_contract_abis, + ttl=config.backend.stale_time_seconds_contract_abis)} + ) + + @app.route("/contract/addresses") + def get_contract_addresses(): + return json_res( + {"addresses": app.cache.get("addresses_all", getter=app.db_reader.get_smart_contract_addresses)} + ) + + @app.route("/contract/addresses/core") + def get_contract_addresses_core(): + return json_res( + {"addresses": app.cache.get("addresses_core", getter=app.db_reader.get_smart_contract_addresses_core, + ttl=config.backend.stale_time_seconds_contract_abis)} + ) + + def get_contribution_contracts_cached(): + return app.cache.get("contracts", getter=app.db_reader.get_contribution_contracts, ttl=2) + + @app.route("/contract/contribution") + def get_open_contract_details(): + return json_res( + {"contracts": get_contribution_contracts_cached(), "added_bls_keys": get_nodes_bls_keys_cached()} + ) + + def get_contribution_contracts_for_sn_pubkey_uncached(sn_pubkey: bytes): + cached_contracts = app.db_reader.get_contribution_contracts() + contracts = [contract for contract in cached_contracts if contract.service_node_pubkey == sn_pubkey] + return contracts + + @app.route("/contract/contribution/") + def get_contribution_contract_for_sn_pubkey_cached(sn_pubkey: bytes): + key = sn_pubkey.hex() + return json_res( + {"contracts": app.cache.get("contract-sn-{}".format(key), + getter=get_contribution_contracts_for_sn_pubkey_uncached, getter_args=key, + ttl=2)} + ) + + def get_related_contribution_contracts_for_eth_address_uncached(eth_wal: str): + contracts = get_contribution_contracts_cached() + + related_contracts = [] + for contract in contracts: + if contract.operator_address == eth_wal: + related_contracts.append(contract) + elif contract.contributors is not None: + for contributor in contract.contributors: + if contributor.address == eth_wal: + related_contracts.append(contract) + return related_contracts + + def get_related_contribution_contracts_for_eth_address_cached(eth_wal: str): + return app.cache.get("related-contracts-{}".format(eth_wal), + getter=get_related_contribution_contracts_for_eth_address_uncached, getter_args=eth_wal) + + @app.route("/contract/contribution/") + def get_contribution_contracts_for_wallet(eth_wal: str): + try: + if not eth_wal or not eth_utils.is_address(eth_wal): + raise ValueError("Invalid wallet address") + + return json_res({"contracts": get_related_contribution_contracts_for_eth_address_cached(eth_wal)}) + + except ValueError as e: + app.logger.error(f"Exception: {e}") + return flask.abort(400, e) + except Exception as e: + app.logger.error(f"Exception: {e}") + return flask.abort(500, e) + + @app.route("/contract/abi/") + def get_abi(contract_name: str): + if contract_name not in get_cached_allowed_contract_names(): + return flask.abort(404, f"Contract {contract_name} not found") + + return json_res( + { + "contract": app.cache.get( + "abi-{}".format(contract_name), getter=app.db_reader.get_smart_contract_abi, + getter_args=contract_name, + ttl=config.backend.stale_time_seconds_contract_abis + ) + } + ) + + @app.route("/contract/address/") + def get_contract_address(contract_name: str): + if contract_name not in get_cached_allowed_contract_names(): + return flask.abort(404, f"Contract {contract_name} not found") + + return json_res( + { + "address": app.cache.get( + "address-{}".format(contract_name), + getter=app.db_reader.get_smart_contract_address, + getter_args=contract_name, + ) + } + ) + + """ + ////////////////////////////////////////////////////////////// + // // + // Event Endpoints // + // // + ////////////////////////////////////////////////////////////// + """ + + def get_events_handler(count_limit=500, skip=0): + limit = min(count_limit, 500) + events, limit, skip, total = app.cache.get("events-{}-{}".format(count_limit, skip), + getter=app.db_reader.get_arbitrum_events_page, + getter_args=[limit, skip], + ttl=10) + pagination = {"limit": limit, "skip": skip, "total": total} + + return {"events": events, "pagination": pagination} + + @app.route("/events//") + def get_events(count: int, skip: int): + return json_res(get_events_handler(count, skip)) + + def get_arbitrum_info_uncached(): + return app.db_reader.get_arbitrum_info() + + @app.route("/arbitrum-info") + def get_arbitrum_info(): + return json_res({"info": app.cache.get("arbitrum-info", getter=get_arbitrum_info_uncached)}) + + @app.route("/stake-events/") + def get_stake_events(contract_id: int): + if contract_id < 0: + return flask.abort(400, "Invalid contract ID") + + return json_res({"events": app.cache.get("stake-events-{}".format(contract_id), + getter=app.db_reader.get_arbitrum_events_for_stake_contrat_id, + getter_args=contract_id)}) + + """ + ////////////////////////////////////////////////////////////// + // // + // Exit Endpoints // + // // + ////////////////////////////////////////////////////////////// + """ + + def handle_get_exit_and_liquidation(params: [bytes, bool]): + ed25519_pubkey, liquidate = params[0], params[1] + if ed25519_pubkey not in get_exitable_ed25519_keys_cached(): + return flask.abort(404, f"No exit available for {ed25519_pubkey.hex()}") + + response = app.rpc.bls_exit_liquidation_request(ed25519_pubkey, liquidate).get() + if response is None: + raise GatewayTimeout("Failed to get exit signature") + if "status" in response: + response.pop("status") + + return json_res({"result": response}) + + def handle_get_exit_and_liquidation_cached(params: [bytes, bool]): + try: + return app.cache.get(f"exit-{params[0]}-{params[1]}", getter=handle_get_exit_and_liquidation, + getter_args=params, + invalidate_timestamp=get_next_block_timestamp_est()) + except GatewayTimeout as e: + app.logger.error(f"Exception: {e}") + return flask.abort(504) # Gateway timeout + except TimeoutError: + return flask.abort(408) # Request timeout + except Exception as e: + app.logger.error(f"Exception: {e}") + return flask.abort(500, e) + + @app.route("/exit/") + def route_get_exit(ed25519_pubkey: bytes): + return handle_get_exit_and_liquidation_cached([ed25519_pubkey, False]) + + @app.route("/liquidation/") + def route_get_liquidation(ed25519_pubkey: bytes): + return handle_get_exit_and_liquidation_cached([ed25519_pubkey, True]) + + def get_exit_liquidation_list_uncached(): + return app.rpc.bls_exit_liquidation_list().get() + + def get_exit_liquidation_list_cached(): + return app.cache.get("exit_liquidation_list", getter=get_exit_liquidation_list_uncached, + invalidate_timestamp=get_next_block_timestamp_est()) + + @app.route("/exit_liquidation_list") + def route_get_exit_liquidation_list(): + return json_res( + {"result": get_exit_liquidation_list_cached()} + ) + + def get_exitable_ed25519_keys_uncached(): + return set([bytes.fromhex(x.get("service_node_pubkey")) for x in get_exit_liquidation_list_cached()]) + + def get_exitable_ed25519_keys_cached(): + return app.cache.get("exitable_ed25519_keys", getter=get_exitable_ed25519_keys_uncached, + invalidate_timestamp=get_next_block_timestamp_est()) + + """ + ////////////////////////////////////////////////////////////// + // // + // Rewards Endpoints // + // // + ////////////////////////////////////////////////////////////// + """ + + def get_rewards_signature_uncached(eth_wal: str): + address = eth_format(eth_wal) + response = app.rpc.bls_rewards_request(address).get() + if response is None: + raise TimeoutError("Failed to get rewards signature") + + response.pop("status") if "status" in response else None + response.pop("address") if "address" in response else None + + return response + + def get_rewards_info_cached(): + # We cache all rewards info for all wallets so we don't need to multiple reads in a short period of time + return app.cache.get(f"rewards_info", getter=app.db_reader.get_rewards_info, + invalidate_timestamp=get_next_block_timestamp_est()) + + def get_rewards_info_for_address_cached(eth_wal: str): + address = eth_format(eth_wal) + rewards_info = get_rewards_info_cached() + return rewards_info.get(address, 0) + + def get_rewards_info_response(eth_wal: str): + return json_res({"rewards": get_rewards_info_for_address_cached(eth_wal)}) + + def get_rewards_signature_response(eth_wal: str): + try: + rewards = get_rewards_info_for_address_cached(eth_wal) + if rewards == 0: + return flask.abort(404, f"No rewards available for {eth_wal}") + + return json_res( + {"rewards": app.cache.get(f"rewards-sig-{eth_wal}", getter=get_rewards_signature_uncached, + getter_args=eth_wal, + invalidate_timestamp=get_next_block_timestamp_est())}) + except ValueError as e: + return flask.abort(400, str(e)) + + @app.route("/rewards/", methods=["GET", "POST"]) + def get_rewards(eth_wal: str): + if flask.request.method == "GET": + return app.cache.get(f"rewards-info-response-{eth_wal}", getter=get_rewards_info_response, + getter_args=eth_wal, + invalidate_timestamp=get_next_block_timestamp_est()) + + if flask.request.method == "POST": + try: + return app.cache.get(f"rewards-sig-response-{eth_wal}", getter=get_rewards_signature_response, + getter_args=eth_wal, invalidate_timestamp=get_next_block_timestamp_est()) + except TimeoutError: + # We don't want to cache a 408 response + return flask.abort(408) + + return flask.abort(405) # Method not allowed + + """ + ////////////////////////////////////////////////////////////// + // // + // Registration Endpoints // + // // + ////////////////////////////////////////////////////////////// + """ + + @app.route("/registrations/") + def operator_registrations(operator: str): + """ + Retrieves stored registration(s) for the given 'operator'. + + This returns an array in the "registrations" field containing as many registrations as are + currently stored for the given operator wallet, sorted from most to least recently submitted. + + Fields are the same as the version of this endpoint that takes a SN pubkey. + + Returns the JSON response with the 'registrations' for the given 'operator'. + """ + + operator_bytes = bytes.fromhex(operator[2:]) + + return json_res( + { + "registrations": app.cache.get( + f"registrations-op-{operator_bytes}", + getter=app.db_reader_registrations.get_registrations_for_operator, + getter_args=operator_bytes, + ) + } + ) + + @app.route("/registrations/") + def sn_pubkey_registrations(sn_pubkey: bytes) -> flask.Response: + """ + Retrieves stored registration(s) for the given service node pubkey. + + This returns an array in the "registrations" field containing either one or two registration + info dicts: a solo registration (if known) and a multi-contributor contract registration (if + known). These are sorted by timestamp of when the registration was last received/updated. + + Fields in each dict: + - "operator": the operator address. + - "contract": the contract address, for "type": "contract" and omitted for "type": "solo". + - "pubkey_ed25519": the primary SN pubkey, in hex. + - "pubkey_bls": the SN BLS pubkey, in hex. + - "sig_ed25519": the SN pubkey signed registration signature. + - "sig_bls": the SN BLS pubkey signed registration signature. + - "timestamp": the unix timestamp when this registration was received (or last updated) + + Returns the JSON response with the 'registrations' for the given 'sn_pubkey'. + """ + result = json_res( + { + "registrations": app.cache.get( + f"registration-sn-{sn_pubkey}", + getter=app.db_reader_registrations.get_registrations_by_pubkey, + getter_args=sn_pubkey, + ) + } + ) + return result + + """ + ////////////////////////////////////////////////////////////// + // // + // Token Endpoints // + // // + ////////////////////////////////////////////////////////////// + """ + + def get_token_info_uncached(): + net_info, arb_info = get_network_info_uncached() + staking_requirement = net_info.get("staking_requirement") + contract_address = app.db_reader.get_smart_contract_address("Token") + contract_address = eth_utils.to_checksum_address(contract_address) if contract_address is not None else None + + return { + "staking_requirement": staking_requirement, + "staking_reward_pool": arb_info.balance_reward_rate_pool, + "contract_address": contract_address, + } + + @app.route("/token") + def route_get_token_info(): + return json_res({"token": app.cache.get("token_info", getter=get_token_info_uncached)}, + include_network_info=False) + + """ + ////////////////////////////////////////////////////////////// + // // + // Network Endpoints // + // // + ////////////////////////////////////////////////////////////// + """ + + def get_network_info_basic_uncached(): + network_info = app.db_reader.get_network_info() + return { + "network_size": network_info.network_size, + } + + @app.route("/network") + def route_get_network_info(): + return json_res({"network": app.cache.get("network_info_basic", getter=get_network_info_basic_uncached)}) + + """ + ////////////////////////////////////////////////////////////// + // // + // Bootstap // + // // + ////////////////////////////////////////////////////////////// + """ + + get_and_refresh_allowed_contract_names() + + """ + ////////////////////////////////////////////////////////////// + // // + // Timers // + // // + ////////////////////////////////////////////////////////////// + """ + + if config.backend.rpc_api_usage_logging: + def log_rpc_usage(signum): + app.rpc.usage_tracker.log_usage(" For signum {}".format(signum)) + app.rpc.usage_tracker.write_failure_reasons_to_file(f"rpc-usage-failure-reasons-{signum}.txt") + + @timer(config.backend.rpc_api_usage_logging_interval, target="worker1") + def log_rpc_usage_w1(signum): + log_rpc_usage(signum) + + @timer(config.backend.rpc_api_usage_logging_interval, target="worker2") + def log_rpc_usage_w2(signum): + log_rpc_usage(signum) + + @timer(config.backend.rpc_api_usage_logging_interval, target="worker3") + def log_rpc_usage_w3(signum): + log_rpc_usage(signum) + + @timer(config.backend.rpc_api_usage_logging_interval, target="worker4") + def log_rpc_usage_w4(signum): + log_rpc_usage(signum) + + return app diff --git a/arbitrum.py b/staking/arbitrum.py similarity index 100% rename from arbitrum.py rename to staking/arbitrum.py diff --git a/testnet.py b/testnet.py deleted file mode 100644 index ede9943..0000000 --- a/testnet.py +++ /dev/null @@ -1,4 +0,0 @@ -from api import app, config - -config.testnet = True -config.oxend_rpc = 'ipc://oxend/testnet.sock' diff --git a/util/config_import.py b/util/config_import.py new file mode 100644 index 0000000..1d694c2 --- /dev/null +++ b/util/config_import.py @@ -0,0 +1,9 @@ +import os +import sys + +def import_config(): + sys.path.append(os.path.join(os.path.dirname(__file__), "..")) + import config + # reset system path to original state + sys.path.pop() + return config \ No newline at end of file diff --git a/util/flask.py b/util/flask.py deleted file mode 100644 index 4eb3a40..0000000 --- a/util/flask.py +++ /dev/null @@ -1,43 +0,0 @@ -import time - -import flask -from flask import request - - -class FlaskReqLimiter: - """ - Flask request limiter - - This is a simple request limiter that can be used to rate limit requests to a Flask app. - It uses a simple in-memory store to keep track of the number of requests per IP address. - - Usage: - ``` - from util.flask import FlaskReqLimiter - - app.req_limiter = FlaskReqLimiter(max_reqs_per_sec=100) - - @app.before_request - def rate_limit(): - return app.req_limiter.rate_limit() - ``` - """ - def __init__(self, max_reqs_per_sec: int = 100, rate_limit_period: int = 60): - self.store = {} - self.max_reqs_per_sec = max_reqs_per_sec - self.rate_limit_period = rate_limit_period - - - def rate_limit(self): - now = time.time() - ip_address = request.remote_addr - requests, expire = self.store.get(ip_address, (0, now + self.rate_limit_period)) - - if now > expire: - self.store[ip_address] = (1, now + self.rate_limit_period) - return - - if requests >= self.max_reqs_per_sec: - return flask.abort(429) - - self.store[ip_address] = (requests + 1, expire) \ No newline at end of file diff --git a/util/flask_utils.py b/util/flask_utils.py new file mode 100644 index 0000000..84caf67 --- /dev/null +++ b/util/flask_utils.py @@ -0,0 +1,85 @@ +import subprocess +import time +import flask +from util.cache import Cache +from log import Log +from util.parse import hexify + + +def json_response(vals = None, vals_no_hexify=None): + """ + Takes a dict, adds some general info fields to it, and jsonifies it for a flask route function + return value. The dict gets passed through `hexify` first to convert any bytes values to hex. + + Note: because network_info is cached, it can be called earlier in the route and both network_info + dict will be the same in both places, and basically guaranteed cached at this stage. + """ + if vals is None: + vals = {} + else: + hexify(vals) + + if vals_no_hexify is None: + vals_no_hexify = {} + + return flask.jsonify({**vals, **vals_no_hexify, "t": time.time()}) + + +class FlaskApp(flask.Flask): + def __init__(self, name: str, log_level: str, log_level_generic: str | None = None, enable_perf: bool = False, + cache_stale_time_seconds: int = 0): + super().__init__(__name__) + log = Log(name, enable_perf=enable_perf) + log.set_level(log_level) + self.log = log.logger + + git_rev = subprocess.run( + ["git", "rev-parse", "--short=9", "HEAD"], stdout=subprocess.PIPE, text=True + ) + self.git_rev = git_rev.stdout.strip() if git_rev.returncode == 0 else "(unknown)" + + # Creates a generic logger to pipe other packages logs into the main app logger + if log_level_generic is not None: + generic_logger = Log(None) + generic_logger.set_level(log_level_generic) + + self.cache = Cache(stale_time_seconds=cache_stale_time_seconds) + + +class FlaskReqLimiter: + """ + Flask request limiter + + This is a simple request limiter that can be used to rate limit requests to a Flask app. + It uses a simple in-memory store to keep track of the number of requests per IP address. + + Usage: + ``` + from util.flask import FlaskReqLimiter + + app.req_limiter = FlaskReqLimiter(max_reqs_per_sec=100) + + @app.before_request + def rate_limit(): + return app.req_limiter.rate_limit() + ``` + """ + + def __init__(self, max_reqs_per_sec: int = 100, rate_limit_period: int = 60): + self.store = {} + self.max_reqs_per_sec = max_reqs_per_sec + self.rate_limit_period = rate_limit_period + + def rate_limit(self): + now = time.time() + ip_address = flask.request.remote_addr + requests, expire = self.store.get(ip_address, (0, now + self.rate_limit_period)) + + if now > expire: + self.store[ip_address] = (1, now + self.rate_limit_period) + return + + if requests >= self.max_reqs_per_sec: + return flask.abort(429) + + self.store[ip_address] = (requests + 1, expire) From 7b47db2361740d5c3930ff66ca61d998c05d70f5 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 27 Jan 2025 19:17:18 +1100 Subject: [PATCH 070/138] feat: create price fetcher --- app_price.py | 6 ++ config_defaults.py | 17 ++++++ price/__init__.py | 0 price/app.py | 139 +++++++++++++++++++++++++++++++++++++++++++++ price/coingecko.py | 116 +++++++++++++++++++++++++++++++++++++ price/read.py | 69 ++++++++++++++++++++++ price/schema.sql | 14 +++++ price/write.py | 82 ++++++++++++++++++++++++++ 8 files changed, 443 insertions(+) create mode 100644 app_price.py create mode 100644 price/__init__.py create mode 100644 price/app.py create mode 100644 price/coingecko.py create mode 100644 price/read.py create mode 100644 price/schema.sql create mode 100644 price/write.py diff --git a/app_price.py b/app_price.py new file mode 100644 index 0000000..0a3da1f --- /dev/null +++ b/app_price.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 + +import config +from price.app import create_app + +app = create_app(config) \ No newline at end of file diff --git a/config_defaults.py b/config_defaults.py index 2152727..960793b 100644 --- a/config_defaults.py +++ b/config_defaults.py @@ -82,6 +82,23 @@ class Backend: sqlite_snapshot_time_interval_seconds: int = 600 snapshot_on_startup: bool = False + """ + PRICE FETCHER CONFIG + """ + prices_api_name: str = "prices_api" + coingecko_api_key: str = "" + coingecko_api_url: str = "https://api.coingecko.com/api" + coingecko_api_token_ids: list[str] = ["ethereum", "chainflip"] + coingecko_api_currencies: list[str] = ["usd", "aud"] + + # Creates a request per period limit by IP address (default is 10 requests per 10 minutes) + prices_api_rate_limit: int = 10 + prices_api_rate_limit_period: int = 600 + prices_sqlite_db: str = "ssb-prices.db" + prices_sqlite_schema: str = "price/schema.sql" + prices_api_default_currency: str = "usd" + prices_api_default_token: str = "ethereum" + prices_api_refetch_interval_seconds:int = 300 # Session mainnet contracts mainnet_backend = Backend() diff --git a/price/__init__.py b/price/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/price/app.py b/price/app.py new file mode 100644 index 0000000..8d4b90e --- /dev/null +++ b/price/app.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +from uwsgidecorators import timer +from price.coingecko import CoinGeckoTokenPriceRequest +from price.read import DBReaderPrices, PriceDB +from price.write import DBWriterPrices +from util.flask_utils import FlaskApp, FlaskReqLimiter, json_response +from werkzeug.middleware.proxy_fix import ProxyFix +from db.util import is_db_initialized, init_db + + +class App(FlaskApp): + def __init__(self, config): + name = config.backend.prices_api_name if config.backend.prices_api_name else __name__ + super().__init__(name, enable_perf=config.backend.performance_logging, + log_level=config.backend.log_level, log_level_generic=config.backend.log_level_generic, + cache_stale_time_seconds=config.backend.stale_time_seconds) + + if not is_db_initialized(config.backend.prices_sqlite_db): + self.log.info( + "Initializing database {} with schema {}".format( + config.backend.prices_sqlite_db, config.backend.prices_sqlite_schema + ) + ) + init_db( + config.backend.prices_sqlite_db, config.backend.prices_sqlite_schema + ) + + self.db_reader = DBReaderPrices( + db_path=config.backend.prices_sqlite_db, + log_level=config.backend.log_level, + perf=config.backend.performance_logging, + ) + self.db_writer = DBWriterPrices( + db_path=config.backend.prices_sqlite_db, + log_level=config.backend.log_level, + perf=config.backend.performance_logging, + ) + self.token_price_request = CoinGeckoTokenPriceRequest( + logger=self.log, + key=config.backend.coingecko_api_key, + url=config.backend.coingecko_api_url, + token_ids=config.backend.coingecko_api_token_ids, + currencies=config.backend.coingecko_api_currencies, + include_market_cap=True, + include_last_updated_at=True, + ) + + self.log.info( + f"IP Rate limit: {config.backend.prices_api_rate_limit} per {config.backend.prices_api_rate_limit_period} seconds") + self.log.info( + "Polling for price info every {} seconds".format(config.backend.prices_api_refetch_interval_seconds)) + + +def create_app(config): + app = App(config) + + # Enables more reliable proxy pass through for rate limiting + app.wsgi_app = ProxyFix(app.wsgi_app) + app.req_limiter = FlaskReqLimiter(max_reqs_per_sec=config.backend.prices_api_rate_limit, + rate_limit_period=config.backend.prices_api_rate_limit_period) + + @app.before_request + def rate_limit(): + return app.req_limiter.rate_limit() + + """ + ////////////////////////////////////////////////////////////// + // // + // Price Endpoints // + // // + ////////////////////////////////////////////////////////////// + """ + + def get_token_price_cache_key(token: str): + return f"price-{token}-all" + + def get_token_info_cached(token: str): + key = get_token_price_cache_key(token) + + data: dict[str, PriceDB] | None = app.cache.get_cached_only(key) + + if data: + return data + + data = app.db_reader.get_latest_prices(token) + + updated_at = max(price.updated_at for price in data.values()) + + stale_time = updated_at + config.backend.prices_api_refetch_interval_seconds + app.cache.set_cache_value(key, data, invalidate_timestamp=stale_time) + return data + + def get_price_for_token_uncached(params: [str, str]): + return get_token_info_cached(params[0]).get(params[1]) + + def get_price_for_token_cached(token: str, currency: str) -> PriceDB | None: + return app.cache.get(f"price-{token}-{currency}", getter=get_price_for_token_uncached, + getter_args=[token, currency], ttl=1) + + def get_token_price_info(token: str = config.backend.prices_api_default_token): + data = get_price_for_token_cached(token, + config.backend.prices_api_default_currency) + + if data is None: + return json_response({"error": "Failed to fetch price"}) + + key = get_token_price_cache_key(token) + stale_time = app.cache.get_stale_timestamp(key) + + return { + "usd": data.price, + "usd_market_cap": data.market_cap, + "t_price": data.updated_at, + "t_stale": stale_time, + } + + @app.route("/price") + def route_get_token_price(): + return json_response({ + "price": get_token_price_info() + }) + + @app.route("/price/") + def route_get_token_price_for_token(token: str): + return json_response({ + "price": get_token_price_info(token) + }) + + @timer(config.backend.prices_api_refetch_interval_seconds) + def fetch_token_price_info(signum): + app.logger.info("Fetch token price info start") + data = app.token_price_request.get() + formatted_data = app.token_price_request.format_for_db(data) + app.db_writer.write_prices_to_db(formatted_data) + app.logger.info("Fetch token price info finish") + + fetch_token_price_info(None) + + return app diff --git a/price/coingecko.py b/price/coingecko.py new file mode 100644 index 0000000..37ec4d8 --- /dev/null +++ b/price/coingecko.py @@ -0,0 +1,116 @@ +import logging +import requests + +from price.read import PriceDB + + +class CoinGeckoTokenPriceRequest: + def __init__(self, logger: logging, key: str, url: str, token_ids: list[str], currencies: list[str], + include_market_cap: bool = True, include_last_updated_at: bool = True): + self.log = logger + self.token_ids = token_ids + self.currencies = currencies + self.headers = { + "accept": "application/json", + "x-cg-demo-api-key": key + } + + query_params = { + "ids": "%2C".join(token_ids), + "vs_currencies": "%2C".join(currencies), + } + + if include_market_cap: + query_params["include_market_cap"] = "true" + + if include_last_updated_at: + query_params["include_last_updated_at"] = "true" + + query_string = "&".join([f"{key}={value}" for key, value in query_params.items()]) + self.url = f"{url}/v3/simple/price?{query_string}" + + def get(self): + """ + Fetch the latest token price info. Uses the params set in the constructor. + + Response looks like: + { + "arbitrum": { + "usd": 0.704886, + "usd_market_cap": 3061960433.734907, + "aud": 1.12, + "aud_market_cap": 4859220977.76168, + "eur": 0.675465, + "eur_market_cap": 2934457339.3137517, + "last_updated_at": 1737685901 + }, + "ethereum": { + "usd": 3305.97, + "usd_market_cap": 398379929097.7145, + "aud": 5245.21, + "aud_market_cap": 632065213327.2137, + "eur": 3167.98, + "eur_market_cap": 381751949237.1051, + "last_updated_at": 1737685896 + } + } + """ + response = requests.get(self.url, headers=self.headers) + + if response.ok: + return response.json() + + self.log.warning("Fetch token price info error: {}".format(response)) + return None + + def format_for_db(self, response: dict, as_list: bool = True): + """ + Converts the response from the CoinGecko API to a format that can be stored in the database. + + Example response: + { + "arbitrum": { + "usd": 0.704886, + "usd_market_cap": 3061960433.734907, + "aud": 1.12, + "aud_market_cap": 4859220977.76168, + "eur": 0.675465, + "eur_market_cap": 2934457339.3137517, + "last_updated_at": 1737685901 + }, + "ethereum": { + "usd": 3305.97, + "usd_market_cap": 398379929097.7145, + "aud": 5245.21, + "aud_market_cap": 632065213327.2137, + "eur": 3167.98, + "eur_market_cap": 381751949237.1051, + "last_updated_at": 1737685896 + } + } + """ + result = [] if as_list else {} + + for token in self.token_ids: + if token not in response: + self.log.warning(f"Token {token} not found in CoinGecko API response") + continue + + updated_at = response[token].get("last_updated_at", None) + for currency in self.currencies: + market_cap = response[token].get(f"{currency}_market_cap", None) + price = response[token].get(currency, None) + if price is None: + self.log.warning(f"Currency {currency} not found in CoinGecko API response for token {token}") + continue + + data = PriceDB(token=token, currency=currency, price=price, market_cap=market_cap, + updated_at=updated_at) + + if as_list: + result.append(data) + else: + result[token] = {} if result[token] is None else result[token] + result[token][currency] = data + + return result diff --git a/price/read.py b/price/read.py new file mode 100644 index 0000000..87a970c --- /dev/null +++ b/price/read.py @@ -0,0 +1,69 @@ +import sqlite3 +from contextlib import closing +from attr import dataclass +from log import Log + + +@dataclass +class PriceDB: + token: str + currency: str + price: float + market_cap: float + updated_at: int + + +class DBReaderPrices: + def __init__(self, db_path: str, log_level: int, perf: bool = False): + self.db_path = db_path + self.log = Log("db_reader", log_level, enable_perf=perf).logger + + def get_latest_price(self, token: str, currency: str): + self.log.perf.start("get_latest_price") + with closing(sqlite3.connect(self.db_path)) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute( + """ + SELECT * FROM prices WHERE token = ? AND currency = ? ORDER BY updated_at DESC LIMIT 1 + """, + (token, currency), + ) + price = PriceDB(*cursor.fetchone()) + self.log.debug("Price: {}".format(price)) + self.log.perf.end("get_latest_price") + return price + + def get_latest_prices(self, token: str): + self.log.perf.start("get_latest_prices") + with closing(sqlite3.connect(self.db_path)) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute( + """ + SELECT * FROM prices WHERE token = ? ORDER BY updated_at DESC + """, + (token,), + ) + prices_lst = [PriceDB(*price) for price in cursor.fetchall()] + + prices = { + price.currency: price for price in prices_lst + } + + self.log.debug("Prices: {}".format(len(prices))) + self.log.perf.end("get_latest_prices") + return prices + + def get_unique_currencies(self, token: str): + self.log.perf.start("get_unique_currencies") + with closing(sqlite3.connect(self.db_path)) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute( + """ + SELECT DISTINCT currency FROM prices WHERE token = ? + """, + (token,), + ) + currencies = [currency[0] for currency in cursor.fetchall()] + self.log.debug("Currencies: {}".format(len(currencies))) + self.log.perf.end("get_unique_currencies") + return currencies diff --git a/price/schema.sql b/price/schema.sql new file mode 100644 index 0000000..15a20a7 --- /dev/null +++ b/price/schema.sql @@ -0,0 +1,14 @@ +PRAGMA journal_mode=WAL; + +CREATE TABLE prices ( + token TEXT NOT NULL, + currency TEXT NOT NULL, + price FLOAT NOT NULL, + market_cap FLOAT NOT NULL, + updated_at INTEGER NOT NULL +); + +CREATE INDEX prices_updated_at_idx ON prices(updated_at DESC); +CREATE INDEX prices_token_at_idx ON prices(token, updated_at DESC); +CREATE INDEX prices_token_currency_idx ON prices(token, currency, updated_at DESC); +CREATE INDEX prices_currency_idx ON prices(currency, updated_at DESC); diff --git a/price/write.py b/price/write.py new file mode 100644 index 0000000..f658032 --- /dev/null +++ b/price/write.py @@ -0,0 +1,82 @@ +import sqlite3 +from contextlib import closing +from log import Log +from price.read import PriceDB + + +class DBWriterPrices: + def __init__(self, db_path: str, log_level: int, perf: bool = False): + self.db_path = db_path + self.log = Log("db_writer", log_level, enable_perf=perf).logger + + def write_prices_to_db(self, prices: list[PriceDB]): + self.log.perf.start("write_prices_to_db") + + with closing(sqlite3.connect(self.db_path)) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + self.log.debug("Inserting {} prices".format(len(prices))) + self.log.perf.start("write_prices_to_db -> insert prices") + + cursor.executemany( + """ + INSERT INTO prices (token, currency, price, market_cap, updated_at) + VALUES (?, ?, ?, ?, ?) + """, + ( + ( + price.token, + price.currency, + price.price, + price.market_cap, + price.updated_at, + ) + for price in prices + ), + ) + + inserted_rows = cursor.rowcount + + self.log.perf.end("write_prices_to_db -> insert prices") + self.log.debug("Inserted {} rows into prices".format(inserted_rows)) + + connection.commit() + self.log.perf.end("write_prices_to_db") + + def write_registration_to_db(self, registration): + self.log.perf.start("write_registration_to_db") + with closing(sqlite3.connect(self.db_path)) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + self.log.debug("Inserting {} registration".format(len(registration))) + self.log.perf.start("write_registration_to_db -> insert registration") + + cursor.execute( + """ + INSERT OR REPLACE INTO registrations ( + contract, + operator, + pubkey_bls, + pubkey_ed25519, + sig_bls, + sig_ed25519 + ) + VALUES (?, ?, ?, ?, ?, ?) + """, + ( + registration.get("contract"), + registration.get("operator"), + registration.get("pubkey_bls"), + registration.get("pubkey_ed25519"), + registration.get("sig_bls"), + registration.get("sig_ed25519"), + ), + ) + + inserted_rows = cursor.rowcount + + self.log.perf.end("write_registration_to_db -> insert registration") + self.log.debug("Inserted {} rows into registration".format(inserted_rows)) + + connection.commit() + self.log.perf.end("write_registration_to_db") From 8e2e2016b46bd63e588ebb811678a13839b03c9c Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 27 Jan 2025 20:18:15 +1100 Subject: [PATCH 071/138] feat: create new config system for prices and base flask --- app_price.py | 19 +++++++- price/app.py | 104 ++++++++++++++++++++++++-------------------- util/flask_utils.py | 58 +++++++++++++++--------- 3 files changed, 111 insertions(+), 70 deletions(-) diff --git a/app_price.py b/app_price.py index 0a3da1f..b7f3b42 100644 --- a/app_price.py +++ b/app_price.py @@ -1,6 +1,21 @@ #!/usr/bin/env python3 import config -from price.app import create_app +from price.app import create_app, PriceAppConfig -app = create_app(config) \ No newline at end of file +price_config = PriceAppConfig( + name="price_api", + log_level=config.backend.log_level, + enable_perf=config.backend.performance_logging, + sqlite_db=config.backend.prices_sqlite_db, + sqlite_schema=config.backend.prices_sqlite_schema, + coingecko_api_key=config.backend.coingecko_api_key, + coingecko_api_url=config.backend.coingecko_api_url, + coingecko_api_token_ids=config.backend.coingecko_api_token_ids, + coingecko_api_currencies=config.backend.coingecko_api_currencies, + coingecko_api_rate_poll_rate_seconds=config.backend.prices_api_refetch_interval_seconds, + default_token=config.backend.prices_api_default_token, + default_currency=config.backend.prices_api_default_currency, +) + +app = create_app(price_config) diff --git a/price/app.py b/price/app.py index 8d4b90e..2b6a5d2 100644 --- a/price/app.py +++ b/price/app.py @@ -1,67 +1,73 @@ #!/usr/bin/env python3 +from dataclasses import dataclass from uwsgidecorators import timer from price.coingecko import CoinGeckoTokenPriceRequest from price.read import DBReaderPrices, PriceDB from price.write import DBWriterPrices -from util.flask_utils import FlaskApp, FlaskReqLimiter, json_response -from werkzeug.middleware.proxy_fix import ProxyFix +from util.flask_utils import FlaskApp, FlaskReqLimiter, json_response, FlaskAppConfig from db.util import is_db_initialized, init_db +@dataclass +class PriceAppConfig(FlaskAppConfig): + # Flask App Config + sqlite_db: str = None + sqlite_schema: str = None + coingecko_api_key: str = None + coingecko_api_url: str = None + coingecko_api_token_ids: list[str] = None + coingecko_api_currencies: list[str] = None + + # Route Config + coingecko_api_rate_poll_rate_seconds: int = None + default_token: str = None + default_currency: str = None + + class App(FlaskApp): - def __init__(self, config): - name = config.backend.prices_api_name if config.backend.prices_api_name else __name__ - super().__init__(name, enable_perf=config.backend.performance_logging, - log_level=config.backend.log_level, log_level_generic=config.backend.log_level_generic, - cache_stale_time_seconds=config.backend.stale_time_seconds) + def __init__(self, config: PriceAppConfig, name=__name__): + super().__init__(config) - if not is_db_initialized(config.backend.prices_sqlite_db): + if not is_db_initialized(config.sqlite_db): self.log.info( "Initializing database {} with schema {}".format( - config.backend.prices_sqlite_db, config.backend.prices_sqlite_schema + config.sqlite_db, config.sqlite_schema ) ) init_db( - config.backend.prices_sqlite_db, config.backend.prices_sqlite_schema + config.sqlite_db, config.sqlite_schema ) - self.db_reader = DBReaderPrices( - db_path=config.backend.prices_sqlite_db, - log_level=config.backend.log_level, - perf=config.backend.performance_logging, + self.db_reader_prices = DBReaderPrices( + db_path=config.sqlite_db, + log_level=config.log_level, + perf=config.enable_perf, ) - self.db_writer = DBWriterPrices( - db_path=config.backend.prices_sqlite_db, - log_level=config.backend.log_level, - perf=config.backend.performance_logging, + self.db_writer_prices = DBWriterPrices( + db_path=config.sqlite_db, + log_level=config.log_level, + perf=config.enable_perf, ) self.token_price_request = CoinGeckoTokenPriceRequest( logger=self.log, - key=config.backend.coingecko_api_key, - url=config.backend.coingecko_api_url, - token_ids=config.backend.coingecko_api_token_ids, - currencies=config.backend.coingecko_api_currencies, + key=config.coingecko_api_key, + url=config.coingecko_api_url, + token_ids=config.coingecko_api_token_ids, + currencies=config.coingecko_api_currencies, include_market_cap=True, include_last_updated_at=True, ) - self.log.info( - f"IP Rate limit: {config.backend.prices_api_rate_limit} per {config.backend.prices_api_rate_limit_period} seconds") - self.log.info( - "Polling for price info every {} seconds".format(config.backend.prices_api_refetch_interval_seconds)) - -def create_app(config): +def create_app(config: PriceAppConfig): app = App(config) - # Enables more reliable proxy pass through for rate limiting - app.wsgi_app = ProxyFix(app.wsgi_app) - app.req_limiter = FlaskReqLimiter(max_reqs_per_sec=config.backend.prices_api_rate_limit, - rate_limit_period=config.backend.prices_api_rate_limit_period) + if config.api_rate_limit is not None and config.api_rate_limit_period is not None: + @app.before_request + def rate_limit(): + return app.req_limiter.rate_limit() - @app.before_request - def rate_limit(): - return app.req_limiter.rate_limit() + price_poll_rate_seconds = config.coingecko_api_rate_poll_rate_seconds if config.coingecko_api_rate_poll_rate_seconds is not None else 0 """ ////////////////////////////////////////////////////////////// @@ -82,11 +88,11 @@ def get_token_info_cached(token: str): if data: return data - data = app.db_reader.get_latest_prices(token) + data = app.db_reader_prices.get_latest_prices(token) updated_at = max(price.updated_at for price in data.values()) - stale_time = updated_at + config.backend.prices_api_refetch_interval_seconds + stale_time = updated_at + price_poll_rate_seconds app.cache.set_cache_value(key, data, invalidate_timestamp=stale_time) return data @@ -97,9 +103,8 @@ def get_price_for_token_cached(token: str, currency: str) -> PriceDB | None: return app.cache.get(f"price-{token}-{currency}", getter=get_price_for_token_uncached, getter_args=[token, currency], ttl=1) - def get_token_price_info(token: str = config.backend.prices_api_default_token): - data = get_price_for_token_cached(token, - config.backend.prices_api_default_currency) + def get_token_price_info(token: str = config.default_token): + data = get_price_for_token_cached(token, config.default_currency) if data is None: return json_response({"error": "Failed to fetch price"}) @@ -126,14 +131,17 @@ def route_get_token_price_for_token(token: str): "price": get_token_price_info(token) }) - @timer(config.backend.prices_api_refetch_interval_seconds) - def fetch_token_price_info(signum): - app.logger.info("Fetch token price info start") - data = app.token_price_request.get() - formatted_data = app.token_price_request.format_for_db(data) - app.db_writer.write_prices_to_db(formatted_data) - app.logger.info("Fetch token price info finish") + if price_poll_rate_seconds > 0: + app.log.info("Polling for price info every {} seconds".format(price_poll_rate_seconds)) + + @timer(price_poll_rate_seconds) + def fetch_token_price_info(signum): + app.logger.info("Fetch token price info start") + data = app.token_price_request.get() + formatted_data = app.token_price_request.format_for_db(data) + app.db_writer_prices.write_prices_to_db(formatted_data) + app.logger.info("Fetch token price info finish") - fetch_token_price_info(None) + fetch_token_price_info(None) return app diff --git a/util/flask_utils.py b/util/flask_utils.py index 84caf67..7ed180f 100644 --- a/util/flask_utils.py +++ b/util/flask_utils.py @@ -1,12 +1,14 @@ import subprocess import time +from dataclasses import dataclass import flask +from werkzeug.middleware.proxy_fix import ProxyFix from util.cache import Cache from log import Log from util.parse import hexify -def json_response(vals = None, vals_no_hexify=None): +def json_response(vals=None, vals_no_hexify=None): """ Takes a dict, adds some general info fields to it, and jsonifies it for a flask route function return value. The dict gets passed through `hexify` first to convert any bytes values to hex. @@ -25,25 +27,15 @@ def json_response(vals = None, vals_no_hexify=None): return flask.jsonify({**vals, **vals_no_hexify, "t": time.time()}) -class FlaskApp(flask.Flask): - def __init__(self, name: str, log_level: str, log_level_generic: str | None = None, enable_perf: bool = False, - cache_stale_time_seconds: int = 0): - super().__init__(__name__) - log = Log(name, enable_perf=enable_perf) - log.set_level(log_level) - self.log = log.logger - - git_rev = subprocess.run( - ["git", "rev-parse", "--short=9", "HEAD"], stdout=subprocess.PIPE, text=True - ) - self.git_rev = git_rev.stdout.strip() if git_rev.returncode == 0 else "(unknown)" - - # Creates a generic logger to pipe other packages logs into the main app logger - if log_level_generic is not None: - generic_logger = Log(None) - generic_logger.set_level(log_level_generic) - - self.cache = Cache(stale_time_seconds=cache_stale_time_seconds) +@dataclass +class FlaskAppConfig: + name: str + log_level: int + log_level_generic: str | None = None + enable_perf: bool | None = False + cache_stale_time_seconds: int = 0 + api_rate_limit: int | None = None + api_rate_limit_period: int | None = None class FlaskReqLimiter: @@ -83,3 +75,29 @@ def rate_limit(self): return flask.abort(429) self.store[ip_address] = (requests + 1, expire) + + +class FlaskApp(flask.Flask): + def __init__(self, config: FlaskAppConfig): + super().__init__(config.name) + log = Log(config.name, enable_perf=config.enable_perf) + log.set_level(config.log_level) + self.log = log.logger + + git_rev = subprocess.run( + ["git", "rev-parse", "--short=9", "HEAD"], stdout=subprocess.PIPE, text=True + ) + self.git_rev = git_rev.stdout.strip() if git_rev.returncode == 0 else "(unknown)" + + # Creates a generic logger to pipe other packages logs into the main app logger + if config.log_level_generic is not None: + generic_logger = Log(None) + generic_logger.set_level(config.log_level_generic) + + self.cache = Cache(stale_time_seconds=config.cache_stale_time_seconds) + + if config.api_rate_limit is not None and config.api_rate_limit_period is not None: + self.log.info(f"IP Rate limit: {config.api_rate_limit} per {config.api_rate_limit_period} seconds") + self.wsgi_app = ProxyFix(self.wsgi_app) + self.req_limiter = FlaskReqLimiter(max_reqs_per_sec=config.api_rate_limit, + rate_limit_period=config.api_rate_limit_period) From b2691626077b469ddf67a09d224c83162df75403 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Tue, 28 Jan 2025 14:47:30 +1100 Subject: [PATCH 072/138] chore: create token conversion methods and tests --- web3client/contracts/token.py | 19 +++++++++ web3client/contracts/token_tests.py | 65 +++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 web3client/contracts/token_tests.py diff --git a/web3client/contracts/token.py b/web3client/contracts/token.py index 5a8abc1..9cc682e 100644 --- a/web3client/contracts/token.py +++ b/web3client/contracts/token.py @@ -5,6 +5,8 @@ class TokenInterface(ContractInterface): abi_name = "Token" + decimals = 9 + def __init__(self, web3_client: Web3Client, contract_address: str): super().__init__(web3_client, contract_address, TokenInterface.abi_name) @@ -25,3 +27,20 @@ def balance_of(self, address: str): :return: balance """ return self.contract.functions.balanceOf(address).call() + + @staticmethod + def to_atomic(amount: float | int) -> int: + """ + Converts a float or int to an atomic amount + """ + print(TokenInterface.decimals) + print(amount) + print((amount * 10 ** TokenInterface.decimals)) + return int(amount * 10 ** TokenInterface.decimals) + + @staticmethod + def from_atomic(amount: int) -> float: + """ + Converts an atomic amount to a float + """ + return amount / 10 ** TokenInterface.decimals diff --git a/web3client/contracts/token_tests.py b/web3client/contracts/token_tests.py new file mode 100644 index 0000000..fba69a4 --- /dev/null +++ b/web3client/contracts/token_tests.py @@ -0,0 +1,65 @@ +from web3client.contracts.token import TokenInterface + +def test_token_decimals(): + assert TokenInterface.decimals == 9 + +def test_float_to_atomic(): + assert TokenInterface.to_atomic(0) == 0 + assert TokenInterface.to_atomic(0.0) == 0 + + assert TokenInterface.to_atomic(1) == 1_000000000 + assert TokenInterface.to_atomic(1.0) == 1_000000000 + + assert TokenInterface.to_atomic(10) == 10_000000000 + assert TokenInterface.to_atomic(10.0) == 10_000000000 + + assert TokenInterface.to_atomic(999) == 999_000000000 + assert TokenInterface.to_atomic(999.0) == 999_000000000 + + assert TokenInterface.to_atomic(0.1) == 100000000 + assert TokenInterface.to_atomic(0.000000001) == 1 + assert TokenInterface.to_atomic(0.9) == 900000000 + assert TokenInterface.to_atomic(0.99999) == 999990000 + assert TokenInterface.to_atomic(1.99999) == 1_999990000 + assert TokenInterface.to_atomic(0.999999999) == 999999999 + assert TokenInterface.to_atomic(1.999999999) == 1_999999999 + assert TokenInterface.to_atomic(0.333333333) == 333333333 + assert TokenInterface.to_atomic(1.333333333) == 1_333333333 + assert TokenInterface.to_atomic(1.000000001) == 1_000000001 + + assert TokenInterface.to_atomic(99.99) == 99_990000000 + assert TokenInterface.to_atomic(99.98) == 99_980000000 + assert TokenInterface.to_atomic(100) == 100_000000000 + assert TokenInterface.to_atomic(101) == 101_000000000 + assert TokenInterface.to_atomic(1000) == 1000_000000000 + assert TokenInterface.to_atomic(10.22) == 10_220000000 + assert TokenInterface.to_atomic(45.42) == 45_420000000 + + +def test_float_from_atomic(): + assert TokenInterface.from_atomic(0) == 0 + + assert TokenInterface.from_atomic(1_000000000) == 1 + assert TokenInterface.from_atomic(10_000000000) == 10 + assert TokenInterface.from_atomic(999_000000000) == 999 + + assert TokenInterface.from_atomic(100000000) == 0.1 + assert TokenInterface.from_atomic(1) == 0.000000001 + assert TokenInterface.from_atomic(900000000) == 0.9 + assert TokenInterface.from_atomic(999990000) == 0.99999 + assert TokenInterface.from_atomic(1_999990000) == 1.99999 + assert TokenInterface.from_atomic(999999999) == 0.999999999 + assert TokenInterface.from_atomic(1_999999999) == 1.999999999 + assert TokenInterface.from_atomic(333333333) == 0.333333333 + assert TokenInterface.from_atomic(1_333333333) == 1.333333333 + assert TokenInterface.from_atomic(1_000000001) == 1.000000001 + + assert TokenInterface.from_atomic(99_990000000) == 99.99 + assert TokenInterface.from_atomic(99_980000000) == 99.98 + assert TokenInterface.from_atomic(100_000000000) == 100 + assert TokenInterface.from_atomic(101_000000000) == 101 + assert TokenInterface.from_atomic(1000_000000000) == 1000 + assert TokenInterface.from_atomic(10_220000000) == 10.22 + assert TokenInterface.from_atomic(45_420000000) == 45.42 + + From 2c739dd46a18b50e9e2f8cd50524c7fceb5c5c71 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Tue, 28 Jan 2025 15:30:05 +1100 Subject: [PATCH 073/138] chore: move non route methods to app class in price api --- price/app.py | 83 ++++++++++++++++++++++++++++------------------------ 1 file changed, 44 insertions(+), 39 deletions(-) diff --git a/price/app.py b/price/app.py index 2b6a5d2..b389158 100644 --- a/price/app.py +++ b/price/app.py @@ -25,8 +25,9 @@ class PriceAppConfig(FlaskAppConfig): class App(FlaskApp): - def __init__(self, config: PriceAppConfig, name=__name__): + def __init__(self, config: PriceAppConfig): super().__init__(config) + self.app_config = config if not is_db_initialized(config.sqlite_db): self.log.info( @@ -58,59 +59,46 @@ def __init__(self, config: PriceAppConfig, name=__name__): include_last_updated_at=True, ) + self.price_poll_rate_seconds = config.coingecko_api_rate_poll_rate_seconds if config.coingecko_api_rate_poll_rate_seconds is not None else 0 -def create_app(config: PriceAppConfig): - app = App(config) - - if config.api_rate_limit is not None and config.api_rate_limit_period is not None: - @app.before_request - def rate_limit(): - return app.req_limiter.rate_limit() - - price_poll_rate_seconds = config.coingecko_api_rate_poll_rate_seconds if config.coingecko_api_rate_poll_rate_seconds is not None else 0 - - """ - ////////////////////////////////////////////////////////////// - // // - // Price Endpoints // - // // - ////////////////////////////////////////////////////////////// - """ - + @staticmethod def get_token_price_cache_key(token: str): return f"price-{token}-all" - def get_token_info_cached(token: str): - key = get_token_price_cache_key(token) + def get_token_info_cached(self, token: str): + key = App.get_token_price_cache_key(token) - data: dict[str, PriceDB] | None = app.cache.get_cached_only(key) + data: dict[str, PriceDB] | None = self.cache.get_cached_only(key) if data: return data - data = app.db_reader_prices.get_latest_prices(token) + data = self.db_reader_prices.get_latest_prices(token) updated_at = max(price.updated_at for price in data.values()) - stale_time = updated_at + price_poll_rate_seconds - app.cache.set_cache_value(key, data, invalidate_timestamp=stale_time) + stale_time = updated_at + self.price_poll_rate_seconds + self.cache.set_cache_value(key, data, invalidate_timestamp=stale_time) return data - def get_price_for_token_uncached(params: [str, str]): - return get_token_info_cached(params[0]).get(params[1]) + def get_price_for_token_uncached(self, params: [str, str]): + return self.get_token_info_cached(params[0]).get(params[1]) + + def get_price_for_token_cached(self, token: str, currency: str) -> PriceDB | None: + return self.cache.get(f"price-{token}-{currency}", getter=self.get_price_for_token_uncached, + getter_args=[token, currency], ttl=1) - def get_price_for_token_cached(token: str, currency: str) -> PriceDB | None: - return app.cache.get(f"price-{token}-{currency}", getter=get_price_for_token_uncached, - getter_args=[token, currency], ttl=1) + def get_token_price_info(self, token: str = None): + if token is None: + token = self.app_config.default_token - def get_token_price_info(token: str = config.default_token): - data = get_price_for_token_cached(token, config.default_currency) + data = self.get_price_for_token_cached(token, self.app_config.default_currency) if data is None: return json_response({"error": "Failed to fetch price"}) - key = get_token_price_cache_key(token) - stale_time = app.cache.get_stale_timestamp(key) + key = App.get_token_price_cache_key(token) + stale_time = self.cache.get_stale_timestamp(key) return { "usd": data.price, @@ -119,22 +107,39 @@ def get_token_price_info(token: str = config.default_token): "t_stale": stale_time, } + +def create_app(config: PriceAppConfig) -> App: + app = App(config) + + if config.api_rate_limit is not None and config.api_rate_limit_period is not None: + @app.before_request + def rate_limit(): + return app.req_limiter.rate_limit() + + """ + ////////////////////////////////////////////////////////////// + // // + // Price Endpoints // + // // + ////////////////////////////////////////////////////////////// + """ + @app.route("/price") def route_get_token_price(): return json_response({ - "price": get_token_price_info() + "price": app.get_token_price_info() }) @app.route("/price/") def route_get_token_price_for_token(token: str): return json_response({ - "price": get_token_price_info(token) + "price": app.get_token_price_info(token) }) - if price_poll_rate_seconds > 0: - app.log.info("Polling for price info every {} seconds".format(price_poll_rate_seconds)) + if app.price_poll_rate_seconds > 0: + app.log.info("Polling for price info every {} seconds".format(app.price_poll_rate_seconds)) - @timer(price_poll_rate_seconds) + @timer(app.price_poll_rate_seconds) def fetch_token_price_info(signum): app.logger.info("Fetch token price info start") data = app.token_price_request.get() From a1d943177c35e8839a0b1bad7abf8d8ccf98445f Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Wed, 29 Jan 2025 10:28:55 +1100 Subject: [PATCH 074/138] fix: abi manager default add to cache --- web3client/abi_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web3client/abi_manager.py b/web3client/abi_manager.py index 8a55fca..e555056 100644 --- a/web3client/abi_manager.py +++ b/web3client/abi_manager.py @@ -23,8 +23,8 @@ def __init__(self, db_writer=None, abi_dir="web3client/abis"): :param abi_dir: The directory where ABI files are stored. Default is 'abis'. """ self.abi_dir = abi_dir + abis = self.load_all_abis() if db_writer is not None: - abis = self.load_all_abis() db_writer.write_smart_contract_abis_to_db(abis) def get_abi(self, contract_name): From 663f42dc2905131a60d2e77bf05171693bc39974 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Wed, 29 Jan 2025 10:30:58 +1100 Subject: [PATCH 075/138] fix: change all inner module imports to relative imports --- db/dataclasses.py | 4 ++-- db/read.py | 8 ++++---- db/snapshot.py | 3 ++- db/write.py | 13 ++++++------- log/__init__.py | 4 ++-- log/time_keeper.py | 4 ++-- oxen/rpc.py | 3 ++- price/app.py | 11 ++++++----- price/coingecko.py | 2 +- price/read.py | 3 ++- price/write.py | 5 +++-- registration/app.py | 14 ++++++++------ registration/read.py | 5 +++-- registration/write.py | 2 +- snapshot/app.py | 5 +++-- staking/app.py | 13 +++++++------ staking/arbitrum.py | 10 +++++----- util/cache.py | 2 +- util/flask_utils.py | 7 ++++--- web3client/client.py | 2 +- web3client/contracts/contract.py | 5 +---- web3client/contracts/reward_rate_pool.py | 4 ++-- web3client/contracts/service_node_contribution.py | 4 ++-- .../contracts/service_node_contribution_factory.py | 6 +++--- web3client/contracts/service_node_rewards.py | 6 +++--- web3client/contracts/token.py | 4 ++-- web3client/contracts/token_tests.py | 2 +- 27 files changed, 79 insertions(+), 72 deletions(-) diff --git a/db/dataclasses.py b/db/dataclasses.py index 43972fc..930d3bf 100644 --- a/db/dataclasses.py +++ b/db/dataclasses.py @@ -2,8 +2,8 @@ from dataclasses import dataclass from typing import Optional -from util.parse import eth_format -from web3client.event_scanner import ProcessedEvent +from ..util.parse import eth_format +from ..web3client.event_scanner import ProcessedEvent @dataclass diff --git a/db/read.py b/db/read.py index 5610ed6..e95881e 100644 --- a/db/read.py +++ b/db/read.py @@ -1,11 +1,11 @@ import sqlite3 from contextlib import closing -from db.dataclasses import DBNode, DBContributionMain, DBNetworkInfo, DBContributionContract, \ +from ..db.dataclasses import DBNode, DBContributionMain, DBNetworkInfo, DBContributionContract, \ DBContributionContractContribution, SmartContractABI, ArbitrumInfo -from log import Log -from util.parse import eth_format -from web3client.event_scanner import ProcessedEvent +from ..log import Log +from ..util.parse import eth_format +from ..web3client.event_scanner import ProcessedEvent class DBReader: diff --git a/db/snapshot.py b/db/snapshot.py index e85439f..c41bb04 100644 --- a/db/snapshot.py +++ b/db/snapshot.py @@ -1,6 +1,7 @@ import os import sqlite3 -from log import Log + +from ..log import Log class DBSnapshot: diff --git a/db/write.py b/db/write.py index 09525a9..43dd82e 100644 --- a/db/write.py +++ b/db/write.py @@ -2,15 +2,14 @@ import sqlite3 import time from contextlib import closing - from web3 import Web3 -from staking.arbitrum import ContributionContractDetails -from db.dataclasses import RewardsInfo, DBNodeExit -from log import Log -from oxen.rpc import ServiceNode, NetworkInfo -from web3client.abi_manager import ABIData -from web3client.event_scanner import ProcessedEvent +from ..staking.arbitrum import ContributionContractDetails +from ..db.dataclasses import RewardsInfo, DBNodeExit +from ..log import Log +from ..oxen.rpc import ServiceNode, NetworkInfo +from ..web3client.abi_manager import ABIData +from ..web3client.event_scanner import ProcessedEvent class DBWriter: diff --git a/log/__init__.py b/log/__init__.py index dd13821..2d0faf0 100644 --- a/log/__init__.py +++ b/log/__init__.py @@ -1,7 +1,7 @@ import logging -from log.perf import PerformanceLogger -from log.util import add_logging_level +from ..log.perf import PerformanceLogger +from ..log.util import add_logging_level CUSTOM_LOG_LEVELS = {"SILLY": 1, "PERFORMANCE": 69} for label, lvl in CUSTOM_LOG_LEVELS.items(): diff --git a/log/time_keeper.py b/log/time_keeper.py index 3e7300c..226e361 100644 --- a/log/time_keeper.py +++ b/log/time_keeper.py @@ -2,8 +2,8 @@ import statistics import time -from log import PerformanceLogger -from util import format_seconds, format_ms +from ..log import PerformanceLogger +from ..util import format_seconds, format_ms class TimeKeeper: diff --git a/oxen/rpc.py b/oxen/rpc.py index 3e8cc23..53842ef 100644 --- a/oxen/rpc.py +++ b/oxen/rpc.py @@ -1,8 +1,9 @@ import logging from typing import TypedDict -from oxen.omq import FutureJSON, omq_connection, RPCUsageTracker from dataclasses import dataclass +from ..oxen.omq import FutureJSON, omq_connection, RPCUsageTracker + class ServiceNodeContributor(TypedDict): address: str diff --git a/price/app.py b/price/app.py index b389158..42b4e90 100644 --- a/price/app.py +++ b/price/app.py @@ -1,11 +1,12 @@ #!/usr/bin/env python3 from dataclasses import dataclass from uwsgidecorators import timer -from price.coingecko import CoinGeckoTokenPriceRequest -from price.read import DBReaderPrices, PriceDB -from price.write import DBWriterPrices -from util.flask_utils import FlaskApp, FlaskReqLimiter, json_response, FlaskAppConfig -from db.util import is_db_initialized, init_db + +from ..price.coingecko import CoinGeckoTokenPriceRequest +from ..price.read import DBReaderPrices, PriceDB +from ..price.write import DBWriterPrices +from ..util.flask_utils import FlaskApp, json_response, FlaskAppConfig +from ..db.util import is_db_initialized, init_db @dataclass diff --git a/price/coingecko.py b/price/coingecko.py index 37ec4d8..9fb4899 100644 --- a/price/coingecko.py +++ b/price/coingecko.py @@ -1,7 +1,7 @@ import logging import requests -from price.read import PriceDB +from ..price.read import PriceDB class CoinGeckoTokenPriceRequest: diff --git a/price/read.py b/price/read.py index 87a970c..fcdbc39 100644 --- a/price/read.py +++ b/price/read.py @@ -1,7 +1,8 @@ import sqlite3 from contextlib import closing from attr import dataclass -from log import Log + +from ..log import Log @dataclass diff --git a/price/write.py b/price/write.py index f658032..1d3cad4 100644 --- a/price/write.py +++ b/price/write.py @@ -1,7 +1,8 @@ import sqlite3 from contextlib import closing -from log import Log -from price.read import PriceDB + +from ..log import Log +from ..price.read import PriceDB class DBWriterPrices: diff --git a/registration/app.py b/registration/app.py index be26e57..4140ee9 100644 --- a/registration/app.py +++ b/registration/app.py @@ -1,12 +1,14 @@ #!/usr/bin/env python3 +from dataclasses import dataclass import eth_utils -from util.flask_utils import FlaskApp, FlaskReqLimiter, json_response from werkzeug.middleware.proxy_fix import ProxyFix -from db.util import is_db_initialized, init_db -from registration.read import DBReaderRegistrations -from registration.validation import check_reg_keys_sigs -from registration.write import DBWriterRegistrations -from util.parse import ( + +from ..util.flask_utils import FlaskApp, json_response, FlaskAppConfig +from ..db.util import is_db_initialized, init_db +from ..registration.read import DBReaderRegistrations +from ..registration.validation import check_reg_keys_sigs +from ..registration.write import DBWriterRegistrations +from ..util.parse import ( parse_query_params, byte_decoder, EthConverter, diff --git a/registration/read.py b/registration/read.py index dcc4a32..26ebec1 100644 --- a/registration/read.py +++ b/registration/read.py @@ -1,7 +1,8 @@ import sqlite3 from contextlib import closing -from db.dataclasses import Registration -from log import Log + +from ..db.dataclasses import Registration +from ..log import Log class DBReaderRegistrations: def __init__(self, db_path: str, log_level: int, perf: bool = False): diff --git a/registration/write.py b/registration/write.py index 1c48573..1f6e251 100644 --- a/registration/write.py +++ b/registration/write.py @@ -1,7 +1,7 @@ import sqlite3 from contextlib import closing -from log import Log +from ..log import Log class DBWriterRegistrations: diff --git a/snapshot/app.py b/snapshot/app.py index b72a439..9acca6e 100644 --- a/snapshot/app.py +++ b/snapshot/app.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 from uwsgidecorators import timer -from db.snapshot import DBSnapshot -from util.flask_utils import FlaskApp + +from ..db.snapshot import DBSnapshot +from ..util.flask_utils import FlaskApp class App(FlaskApp): diff --git a/staking/app.py b/staking/app.py index bd9e33e..dbc7e38 100644 --- a/staking/app.py +++ b/staking/app.py @@ -6,12 +6,13 @@ from eth_typing import ChecksumAddress from uwsgidecorators import timer from werkzeug.exceptions import GatewayTimeout -from db.dataclasses import ArbitrumInfo, DBNetworkInfo -from db.read import DBReader -from oxen.rpc import OxenRPC -from registration.read import DBReaderRegistrations -from util.flask_utils import FlaskApp, json_response -from util.parse import Hex64Converter, hexify, EthConverter, eth_format + +from ..db.dataclasses import ArbitrumInfo +from ..db.read import DBReader +from ..oxen.rpc import OxenRPC +from ..registration.read import DBReaderRegistrations +from ..util.flask_utils import FlaskApp, json_response +from ..util.parse import Hex64Converter, EthConverter, eth_format class App(FlaskApp): diff --git a/staking/arbitrum.py b/staking/arbitrum.py index 80b9e41..c1dd7d8 100644 --- a/staking/arbitrum.py +++ b/staking/arbitrum.py @@ -4,11 +4,11 @@ import eth_utils from web3.exceptions import BlockNotFound -from web3client.client import Web3Client -from web3client.contracts.service_node_contribution import ServiceNodeContributionInterface -from web3client.contracts.service_node_contribution_factory import ServiceNodeContributionFactory -from web3client.contracts.service_node_rewards import ServiceNodeRewardsInterface -from web3client.event_scanner import ProcessedEvent +from ..web3client.client import Web3Client +from ..web3client.contracts.service_node_contribution import ServiceNodeContributionInterface +from ..web3client.contracts.service_node_contribution_factory import ServiceNodeContributionFactory +from ..web3client.contracts.service_node_rewards import ServiceNodeRewardsInterface +from ..web3client.event_scanner import ProcessedEvent # TODO: we should be able to remove this once contract_id is always available via rpc.get_service_nodes diff --git a/util/cache.py b/util/cache.py index fce3195..c87b3c1 100644 --- a/util/cache.py +++ b/util/cache.py @@ -3,7 +3,7 @@ from copy import copy from typing import Optional, Callable -from log import Log +from ..log import Log class Cache: diff --git a/util/flask_utils.py b/util/flask_utils.py index 7ed180f..36e0ff6 100644 --- a/util/flask_utils.py +++ b/util/flask_utils.py @@ -3,9 +3,10 @@ from dataclasses import dataclass import flask from werkzeug.middleware.proxy_fix import ProxyFix -from util.cache import Cache -from log import Log -from util.parse import hexify + +from ..util.cache import Cache +from ..log import Log +from ..util.parse import hexify def json_response(vals=None, vals_no_hexify=None): diff --git a/web3client/client.py b/web3client/client.py index ac2e1ec..b231baf 100644 --- a/web3client/client.py +++ b/web3client/client.py @@ -3,7 +3,7 @@ import eth_utils from web3 import Web3, HTTPProvider from web3.contract.contract import ContractFunction -from web3client.abi_manager import ABIManager +from ..web3client.abi_manager import ABIManager class Web3Client: diff --git a/web3client/contracts/contract.py b/web3client/contracts/contract.py index af92944..f738218 100644 --- a/web3client/contracts/contract.py +++ b/web3client/contracts/contract.py @@ -1,8 +1,5 @@ from web3 import Web3 - -from log import Log -from web3client.abi_manager import ABIManager -from web3client.client import Web3Client +from ..client import Web3Client class ContractInterface: diff --git a/web3client/contracts/reward_rate_pool.py b/web3client/contracts/reward_rate_pool.py index 33244b6..ed66c02 100644 --- a/web3client/contracts/reward_rate_pool.py +++ b/web3client/contracts/reward_rate_pool.py @@ -1,5 +1,5 @@ -from web3client.client import Web3Client -from web3client.contracts.contract import ContractInterface +from ..client import Web3Client +from ..contracts.contract import ContractInterface class RewardRatePoolInterface(ContractInterface): diff --git a/web3client/contracts/service_node_contribution.py b/web3client/contracts/service_node_contribution.py index d5126f2..7fc2da5 100644 --- a/web3client/contracts/service_node_contribution.py +++ b/web3client/contracts/service_node_contribution.py @@ -2,8 +2,8 @@ from dataclasses import dataclass from web3 import Web3 -from web3client.client import Web3Client -from web3client.contracts.contract import ContractInterface +from ..client import Web3Client +from ..contracts.contract import ContractInterface class ServiceNodeContributionInterface(ContractInterface): diff --git a/web3client/contracts/service_node_contribution_factory.py b/web3client/contracts/service_node_contribution_factory.py index bcaf8e8..497b399 100644 --- a/web3client/contracts/service_node_contribution_factory.py +++ b/web3client/contracts/service_node_contribution_factory.py @@ -1,6 +1,6 @@ -from web3client.client import Web3Client -from web3client.contracts.contract import ContractInterface -from web3client.event_scanner import EventScanner +from ..client import Web3Client +from ..contracts.contract import ContractInterface +from ..event_scanner import EventScanner class ServiceNodeContributionFactory(ContractInterface): diff --git a/web3client/contracts/service_node_rewards.py b/web3client/contracts/service_node_rewards.py index ac43a2d..a747cfa 100644 --- a/web3client/contracts/service_node_rewards.py +++ b/web3client/contracts/service_node_rewards.py @@ -1,6 +1,6 @@ -from web3client.client import Web3Client -from web3client.contracts.contract import ContractInterface -from web3client.event_scanner import EventScanner +from ..client import Web3Client +from ..contracts.contract import ContractInterface +from ..event_scanner import EventScanner class ServiceNodeRewardsRecipient: diff --git a/web3client/contracts/token.py b/web3client/contracts/token.py index 9cc682e..75afc2c 100644 --- a/web3client/contracts/token.py +++ b/web3client/contracts/token.py @@ -1,5 +1,5 @@ -from web3client.client import Web3Client -from web3client.contracts.contract import ContractInterface +from ..client import Web3Client +from ..contracts.contract import ContractInterface class TokenInterface(ContractInterface): diff --git a/web3client/contracts/token_tests.py b/web3client/contracts/token_tests.py index fba69a4..c1e22a3 100644 --- a/web3client/contracts/token_tests.py +++ b/web3client/contracts/token_tests.py @@ -1,4 +1,4 @@ -from web3client.contracts.token import TokenInterface +from ..contracts.token import TokenInterface def test_token_decimals(): assert TokenInterface.decimals == 9 From b2acb63d252d5ee61fc0d34b1da291afde3ea78f Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Wed, 29 Jan 2025 11:38:51 +1100 Subject: [PATCH 076/138] fix: abi manager default and coingecko defaults --- price/app.py | 22 ++++++++++++---------- web3client/client.py | 5 ++++- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/price/app.py b/price/app.py index 42b4e90..1cc5ebb 100644 --- a/price/app.py +++ b/price/app.py @@ -50,15 +50,17 @@ def __init__(self, config: PriceAppConfig): log_level=config.log_level, perf=config.enable_perf, ) - self.token_price_request = CoinGeckoTokenPriceRequest( - logger=self.log, - key=config.coingecko_api_key, - url=config.coingecko_api_url, - token_ids=config.coingecko_api_token_ids, - currencies=config.coingecko_api_currencies, - include_market_cap=True, - include_last_updated_at=True, - ) + + if config.coingecko_api_url: + self.token_price_request = CoinGeckoTokenPriceRequest( + logger=self.log, + key=config.coingecko_api_key, + url=config.coingecko_api_url, + token_ids=config.coingecko_api_token_ids, + currencies=config.coingecko_api_currencies, + include_market_cap=True, + include_last_updated_at=True, + ) self.price_poll_rate_seconds = config.coingecko_api_rate_poll_rate_seconds if config.coingecko_api_rate_poll_rate_seconds is not None else 0 @@ -137,7 +139,7 @@ def route_get_token_price_for_token(token: str): "price": app.get_token_price_info(token) }) - if app.price_poll_rate_seconds > 0: + if app.price_poll_rate_seconds > 0 and config.coingecko_api_url: app.log.info("Polling for price info every {} seconds".format(app.price_poll_rate_seconds)) @timer(app.price_poll_rate_seconds) diff --git a/web3client/client.py b/web3client/client.py index b231baf..9ef4787 100644 --- a/web3client/client.py +++ b/web3client/client.py @@ -13,7 +13,7 @@ def __init__( caller_address: str | None, private_key: str | None, logger: logging, - abi_manager: ABIManager = ABIManager(), + abi_manager: ABIManager = None ): """ Initialize the web3 client. @@ -22,6 +22,9 @@ def __init__( :param caller_address: Address of the caller. :param private_key: Private key of the caller. """ + if abi_manager is None: + abi_manager = ABIManager() + self.web3 = Web3(HTTPProvider(endpoint_uri=provider_urls[0])) self.provider_url = provider_urls[0] self.abi_manager = abi_manager From 118f383534ca08506edfdfd8fc425d8d9be0e892 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Wed, 29 Jan 2025 14:40:09 +1100 Subject: [PATCH 077/138] fix: add root level package init --- __init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 __init__.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 From ad3fdc63ffe09980deb49239bb3a403ecf40bd91 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Wed, 29 Jan 2025 16:10:43 +1100 Subject: [PATCH 078/138] chore: move python code into src folder so it can be used as a package and run --- .gitignore | 2 +- app_registration.py | 6 --- pytest.ini | 2 + __init__.py => src/__init__.py | 0 app_price.py => src/app_price.py | 4 +- src/app_registration.py | 16 +++++++ app_snapshot.py => src/app_snapshot.py | 0 app_staking.py => src/app_staking.py | 0 config.py => src/config.py | 2 +- config_validate.py => src/config_validate.py | 1 - {db => src/db}/__init__.py | 0 {db => src/db}/dataclasses.py | 0 {db => src/db}/read.py | 0 {db => src/db}/schema.sql | 0 {db => src/db}/snapshot.py | 0 {db => src/db}/util.py | 0 {db => src/db}/write.py | 0 fetcher.py => src/fetcher.py | 0 {log => src/log}/__init__.py | 4 +- {log => src/log}/perf.py | 0 {log => src/log}/time_keeper.py | 0 {log => src/log}/util.py | 0 make-fake-reg.py => src/make-fake-reg.py | 0 {oxen => src/oxen}/__init__.py | 0 {oxen => src/oxen}/omq.py | 0 {oxen => src/oxen}/rpc.py | 0 {price => src/price}/__init__.py | 0 {price => src/price}/app.py | 9 ++-- {price => src/price}/coingecko.py | 2 +- src/price/dataclasses.py | 10 +++++ {price => src/price}/read.py | 11 +---- {price => src/price}/schema.sql | 0 {price => src/price}/write.py | 2 +- .../registration}/__init__.py | 0 {registration => src/registration}/app.py | 45 +++++++++---------- .../registration}/app_tests.py | 17 +++++-- {registration => src/registration}/read.py | 0 {registration => src/registration}/schema.sql | 0 .../registration}/validation.py | 0 {registration => src/registration}/write.py | 0 {snapshot => src/snapshot}/__init__.py | 0 {snapshot => src/snapshot}/app.py | 0 {staking => src/staking}/__init__.py | 0 {staking => src/staking}/app.py | 0 {staking => src/staking}/arbitrum.py | 0 {util => src/util}/__init__.py | 0 {util => src/util}/cache.py | 0 {util => src/util}/config_import.py | 2 +- {util => src/util}/flask_utils.py | 5 ++- {util => src/util}/parse.py | 0 {web3client => src/web3client}/__init__.py | 0 {web3client => src/web3client}/abi_manager.py | 0 .../web3client}/abis/RewardRatePool.json | 0 .../abis/ServiceNodeContribution.json | 0 .../abis/ServiceNodeContributionFactory.json | 0 .../web3client}/abis/ServiceNodeRewards.json | 0 .../web3client}/abis/Token.json | 0 {web3client => src/web3client}/client.py | 0 .../web3client}/contracts/__init__.py | 0 .../web3client}/contracts/contract.py | 0 .../web3client}/contracts/reward_rate_pool.py | 0 .../contracts/service_node_contribution.py | 0 .../service_node_contribution_factory.py | 0 .../contracts/service_node_rewards.py | 0 .../web3client}/contracts/token.py | 0 .../web3client}/contracts/token_tests.py | 0 .../web3client}/event_scanner.py | 0 67 files changed, 81 insertions(+), 59 deletions(-) delete mode 100644 app_registration.py create mode 100644 pytest.ini rename __init__.py => src/__init__.py (100%) rename app_price.py => src/app_price.py (90%) create mode 100644 src/app_registration.py rename app_snapshot.py => src/app_snapshot.py (100%) rename app_staking.py => src/app_staking.py (100%) rename config.py => src/config.py (92%) rename config_validate.py => src/config_validate.py (98%) rename {db => src/db}/__init__.py (100%) rename {db => src/db}/dataclasses.py (100%) rename {db => src/db}/read.py (100%) rename {db => src/db}/schema.sql (100%) rename {db => src/db}/snapshot.py (100%) rename {db => src/db}/util.py (100%) rename {db => src/db}/write.py (100%) rename fetcher.py => src/fetcher.py (100%) rename {log => src/log}/__init__.py (95%) rename {log => src/log}/perf.py (100%) rename {log => src/log}/time_keeper.py (100%) rename {log => src/log}/util.py (100%) rename make-fake-reg.py => src/make-fake-reg.py (100%) rename {oxen => src/oxen}/__init__.py (100%) rename {oxen => src/oxen}/omq.py (100%) rename {oxen => src/oxen}/rpc.py (100%) rename {price => src/price}/__init__.py (100%) rename {price => src/price}/app.py (95%) rename {price => src/price}/coingecko.py (99%) create mode 100644 src/price/dataclasses.py rename {price => src/price}/read.py (94%) rename {price => src/price}/schema.sql (100%) rename {price => src/price}/write.py (98%) rename {registration => src/registration}/__init__.py (100%) rename {registration => src/registration}/app.py (71%) rename {registration => src/registration}/app_tests.py (73%) rename {registration => src/registration}/read.py (100%) rename {registration => src/registration}/schema.sql (100%) rename {registration => src/registration}/validation.py (100%) rename {registration => src/registration}/write.py (100%) rename {snapshot => src/snapshot}/__init__.py (100%) rename {snapshot => src/snapshot}/app.py (100%) rename {staking => src/staking}/__init__.py (100%) rename {staking => src/staking}/app.py (100%) rename {staking => src/staking}/arbitrum.py (100%) rename {util => src/util}/__init__.py (100%) rename {util => src/util}/cache.py (100%) rename {util => src/util}/config_import.py (87%) rename {util => src/util}/flask_utils.py (95%) rename {util => src/util}/parse.py (100%) rename {web3client => src/web3client}/__init__.py (100%) rename {web3client => src/web3client}/abi_manager.py (100%) rename {web3client => src/web3client}/abis/RewardRatePool.json (100%) rename {web3client => src/web3client}/abis/ServiceNodeContribution.json (100%) rename {web3client => src/web3client}/abis/ServiceNodeContributionFactory.json (100%) rename {web3client => src/web3client}/abis/ServiceNodeRewards.json (100%) rename {web3client => src/web3client}/abis/Token.json (100%) rename {web3client => src/web3client}/client.py (100%) rename {web3client => src/web3client}/contracts/__init__.py (100%) rename {web3client => src/web3client}/contracts/contract.py (100%) rename {web3client => src/web3client}/contracts/reward_rate_pool.py (100%) rename {web3client => src/web3client}/contracts/service_node_contribution.py (100%) rename {web3client => src/web3client}/contracts/service_node_contribution_factory.py (100%) rename {web3client => src/web3client}/contracts/service_node_rewards.py (100%) rename {web3client => src/web3client}/contracts/token.py (100%) rename {web3client => src/web3client}/contracts/token_tests.py (100%) rename {web3client => src/web3client}/event_scanner.py (100%) diff --git a/.gitignore b/.gitignore index 4c15328..602479f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ /*.db -/config.py +/src/config.py /__pycache__ /oxend/*.sock */__pycache__ diff --git a/app_registration.py b/app_registration.py deleted file mode 100644 index 0bd3a14..0000000 --- a/app_registration.py +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env python3 - -import config -from registration.app import create_app - -app = create_app(config) \ No newline at end of file diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..eef2ade --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +python_files = *_tests.py diff --git a/__init__.py b/src/__init__.py similarity index 100% rename from __init__.py rename to src/__init__.py diff --git a/app_price.py b/src/app_price.py similarity index 90% rename from app_price.py rename to src/app_price.py index b7f3b42..2699eb9 100644 --- a/app_price.py +++ b/src/app_price.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 -import config -from price.app import create_app, PriceAppConfig +import src.config as config +from src.price.app import create_app, PriceAppConfig price_config = PriceAppConfig( name="price_api", diff --git a/src/app_registration.py b/src/app_registration.py new file mode 100644 index 0000000..d403164 --- /dev/null +++ b/src/app_registration.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 + +import config +from registration.app import create_app, RegistrationAppConfig + +registration_config = RegistrationAppConfig( + name="registration_api", + log_level=config.backend.log_level, + enable_perf=config.backend.performance_logging, + sqlite_db=config.backend.registration_sqlite_db, + sqlite_schema=config.backend.registration_sqlite_schema, + api_rate_limit=config.backend.registration_api_rate_limit, + api_rate_limit_period=config.backend.registration_api_rate_limit_period, +) + +app = create_app(registration_config) \ No newline at end of file diff --git a/app_snapshot.py b/src/app_snapshot.py similarity index 100% rename from app_snapshot.py rename to src/app_snapshot.py diff --git a/app_staking.py b/src/app_staking.py similarity index 100% rename from app_staking.py rename to src/app_staking.py diff --git a/config.py b/src/config.py similarity index 92% rename from config.py rename to src/config.py index f0510d4..51cac52 100644 --- a/config.py +++ b/src/config.py @@ -1,4 +1,4 @@ -from config_defaults import * +from src.config_defaults import * # Local settings. Changes to this file are meant for a local installation (and should not be # committed to git). diff --git a/config_validate.py b/src/config_validate.py similarity index 98% rename from config_validate.py rename to src/config_validate.py index 0b70248..f545654 100644 --- a/config_validate.py +++ b/src/config_validate.py @@ -2,7 +2,6 @@ import config from log import Log -from oxen.omq import omq_connection from oxen.rpc import OxenRPC from util import is_not_empty_string, valid_address_assertion from web3client.client import Web3Client diff --git a/db/__init__.py b/src/db/__init__.py similarity index 100% rename from db/__init__.py rename to src/db/__init__.py diff --git a/db/dataclasses.py b/src/db/dataclasses.py similarity index 100% rename from db/dataclasses.py rename to src/db/dataclasses.py diff --git a/db/read.py b/src/db/read.py similarity index 100% rename from db/read.py rename to src/db/read.py diff --git a/db/schema.sql b/src/db/schema.sql similarity index 100% rename from db/schema.sql rename to src/db/schema.sql diff --git a/db/snapshot.py b/src/db/snapshot.py similarity index 100% rename from db/snapshot.py rename to src/db/snapshot.py diff --git a/db/util.py b/src/db/util.py similarity index 100% rename from db/util.py rename to src/db/util.py diff --git a/db/write.py b/src/db/write.py similarity index 100% rename from db/write.py rename to src/db/write.py diff --git a/fetcher.py b/src/fetcher.py similarity index 100% rename from fetcher.py rename to src/fetcher.py diff --git a/log/__init__.py b/src/log/__init__.py similarity index 95% rename from log/__init__.py rename to src/log/__init__.py index 2d0faf0..f024c09 100644 --- a/log/__init__.py +++ b/src/log/__init__.py @@ -1,7 +1,7 @@ import logging -from ..log.perf import PerformanceLogger -from ..log.util import add_logging_level +from .perf import PerformanceLogger +from .util import add_logging_level CUSTOM_LOG_LEVELS = {"SILLY": 1, "PERFORMANCE": 69} for label, lvl in CUSTOM_LOG_LEVELS.items(): diff --git a/log/perf.py b/src/log/perf.py similarity index 100% rename from log/perf.py rename to src/log/perf.py diff --git a/log/time_keeper.py b/src/log/time_keeper.py similarity index 100% rename from log/time_keeper.py rename to src/log/time_keeper.py diff --git a/log/util.py b/src/log/util.py similarity index 100% rename from log/util.py rename to src/log/util.py diff --git a/make-fake-reg.py b/src/make-fake-reg.py similarity index 100% rename from make-fake-reg.py rename to src/make-fake-reg.py diff --git a/oxen/__init__.py b/src/oxen/__init__.py similarity index 100% rename from oxen/__init__.py rename to src/oxen/__init__.py diff --git a/oxen/omq.py b/src/oxen/omq.py similarity index 100% rename from oxen/omq.py rename to src/oxen/omq.py diff --git a/oxen/rpc.py b/src/oxen/rpc.py similarity index 100% rename from oxen/rpc.py rename to src/oxen/rpc.py diff --git a/price/__init__.py b/src/price/__init__.py similarity index 100% rename from price/__init__.py rename to src/price/__init__.py diff --git a/price/app.py b/src/price/app.py similarity index 95% rename from price/app.py rename to src/price/app.py index 1cc5ebb..8d3365a 100644 --- a/price/app.py +++ b/src/price/app.py @@ -2,10 +2,11 @@ from dataclasses import dataclass from uwsgidecorators import timer -from ..price.coingecko import CoinGeckoTokenPriceRequest -from ..price.read import DBReaderPrices, PriceDB -from ..price.write import DBWriterPrices -from ..util.flask_utils import FlaskApp, json_response, FlaskAppConfig +from src.util.flask_utils import FlaskApp, json_response, FlaskAppConfig +from .coingecko import CoinGeckoTokenPriceRequest +from .read import DBReaderPrices +from .dataclasses import PriceDB +from .write import DBWriterPrices from ..db.util import is_db_initialized, init_db diff --git a/price/coingecko.py b/src/price/coingecko.py similarity index 99% rename from price/coingecko.py rename to src/price/coingecko.py index 9fb4899..7bb4bb9 100644 --- a/price/coingecko.py +++ b/src/price/coingecko.py @@ -1,7 +1,7 @@ import logging import requests -from ..price.read import PriceDB +from .dataclasses import PriceDB class CoinGeckoTokenPriceRequest: diff --git a/src/price/dataclasses.py b/src/price/dataclasses.py new file mode 100644 index 0000000..9760d96 --- /dev/null +++ b/src/price/dataclasses.py @@ -0,0 +1,10 @@ +from attr import dataclass + + +@dataclass +class PriceDB: + token: str + currency: str + price: float + market_cap: float + updated_at: int diff --git a/price/read.py b/src/price/read.py similarity index 94% rename from price/read.py rename to src/price/read.py index fcdbc39..39e0bd1 100644 --- a/price/read.py +++ b/src/price/read.py @@ -1,19 +1,10 @@ import sqlite3 from contextlib import closing -from attr import dataclass +from .dataclasses import PriceDB from ..log import Log -@dataclass -class PriceDB: - token: str - currency: str - price: float - market_cap: float - updated_at: int - - class DBReaderPrices: def __init__(self, db_path: str, log_level: int, perf: bool = False): self.db_path = db_path diff --git a/price/schema.sql b/src/price/schema.sql similarity index 100% rename from price/schema.sql rename to src/price/schema.sql diff --git a/price/write.py b/src/price/write.py similarity index 98% rename from price/write.py rename to src/price/write.py index 1d3cad4..0bb238f 100644 --- a/price/write.py +++ b/src/price/write.py @@ -1,8 +1,8 @@ import sqlite3 from contextlib import closing +from .dataclasses import PriceDB from ..log import Log -from ..price.read import PriceDB class DBWriterPrices: diff --git a/registration/__init__.py b/src/registration/__init__.py similarity index 100% rename from registration/__init__.py rename to src/registration/__init__.py diff --git a/registration/app.py b/src/registration/app.py similarity index 71% rename from registration/app.py rename to src/registration/app.py index 4140ee9..0a07c2f 100644 --- a/registration/app.py +++ b/src/registration/app.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 from dataclasses import dataclass import eth_utils -from werkzeug.middleware.proxy_fix import ProxyFix from ..util.flask_utils import FlaskApp, json_response, FlaskAppConfig from ..db.util import is_db_initialized, init_db @@ -12,53 +11,51 @@ parse_query_params, byte_decoder, EthConverter, - hexify, Hex64Converter, raw_eth_addr, ) +@dataclass +class RegistrationAppConfig(FlaskAppConfig): + # Flask App Config + sqlite_db: str = None + sqlite_schema: str = None + # Route Config + coingecko_api_rate_poll_rate_seconds: int = None + default_token: str = None + default_currency: str = None class App(FlaskApp): - def __init__(self, config): - name = config.backend.registration_api_name if config.backend.registration_api_name else __name__ - super().__init__(name, enable_perf=config.backend.performance_logging, - log_level=config.backend.log_level, - cache_stale_time_seconds=config.backend.stale_time_seconds) + def __init__(self, config: RegistrationAppConfig): + super().__init__(config) - if not is_db_initialized(config.backend.registration_sqlite_db): + if not is_db_initialized(config.sqlite_db): self.log.info( "Initializing database {} with schema {}".format( - config.backend.registration_sqlite_db, config.backend.registration_sqlite_schema + config.sqlite_db, config.sqlite_schema ) ) init_db( - config.backend.registration_sqlite_db, config.backend.registration_sqlite_schema + config.sqlite_db, config.sqlite_schema ) self.db_reader = DBReaderRegistrations( - db_path=config.backend.registration_sqlite_db, - log_level=config.backend.log_level, - perf=config.backend.performance_logging, + db_path=config.sqlite_db, + log_level=config.log_level, + perf=config.enable_perf, ) self.db_writer = DBWriterRegistrations( - db_path=config.backend.registration_sqlite_db, - log_level=config.backend.log_level, - perf=config.backend.performance_logging, + db_path=config.sqlite_db, + log_level=config.log_level, + perf=config.enable_perf, ) self.allowed_contract_names = set() - self.log.info( - f"IP Rate limit: {config.backend.registration_api_rate_limit} per {config.backend.registration_api_rate_limit_period} seconds") -def create_app(config) -> App: +def create_app(config: RegistrationAppConfig) -> App: app = App(config) - # Enables more reliable proxy pass through for rate limiting - app.wsgi_app = ProxyFix(app.wsgi_app) - app.req_limiter = FlaskReqLimiter(max_reqs_per_sec=config.backend.registration_api_rate_limit, - rate_limit_period=config.backend.registration_api_rate_limit_period) - @app.before_request def rate_limit(): return app.req_limiter.rate_limit() diff --git a/registration/app_tests.py b/src/registration/app_tests.py similarity index 73% rename from registration/app_tests.py rename to src/registration/app_tests.py index a149cb4..557d48d 100644 --- a/registration/app_tests.py +++ b/src/registration/app_tests.py @@ -1,10 +1,21 @@ import time import pytest -from registration.app import create_app -from util.config_import import import_config +from ..registration.app import create_app, RegistrationAppConfig +from ..util.config_import import import_config config = import_config() -app = create_app(config) + +registration_config = RegistrationAppConfig( + name="registration_api", + log_level=config.backend.log_level, + enable_perf=config.backend.performance_logging, + sqlite_db=config.backend.registration_sqlite_db, + sqlite_schema=config.backend.registration_sqlite_schema, + api_rate_limit=config.backend.registration_api_rate_limit, + api_rate_limit_period=config.backend.registration_api_rate_limit_period, +) + +app = create_app(registration_config) @pytest.fixture() def client(): diff --git a/registration/read.py b/src/registration/read.py similarity index 100% rename from registration/read.py rename to src/registration/read.py diff --git a/registration/schema.sql b/src/registration/schema.sql similarity index 100% rename from registration/schema.sql rename to src/registration/schema.sql diff --git a/registration/validation.py b/src/registration/validation.py similarity index 100% rename from registration/validation.py rename to src/registration/validation.py diff --git a/registration/write.py b/src/registration/write.py similarity index 100% rename from registration/write.py rename to src/registration/write.py diff --git a/snapshot/__init__.py b/src/snapshot/__init__.py similarity index 100% rename from snapshot/__init__.py rename to src/snapshot/__init__.py diff --git a/snapshot/app.py b/src/snapshot/app.py similarity index 100% rename from snapshot/app.py rename to src/snapshot/app.py diff --git a/staking/__init__.py b/src/staking/__init__.py similarity index 100% rename from staking/__init__.py rename to src/staking/__init__.py diff --git a/staking/app.py b/src/staking/app.py similarity index 100% rename from staking/app.py rename to src/staking/app.py diff --git a/staking/arbitrum.py b/src/staking/arbitrum.py similarity index 100% rename from staking/arbitrum.py rename to src/staking/arbitrum.py diff --git a/util/__init__.py b/src/util/__init__.py similarity index 100% rename from util/__init__.py rename to src/util/__init__.py diff --git a/util/cache.py b/src/util/cache.py similarity index 100% rename from util/cache.py rename to src/util/cache.py diff --git a/util/config_import.py b/src/util/config_import.py similarity index 87% rename from util/config_import.py rename to src/util/config_import.py index 1d694c2..5936eeb 100644 --- a/util/config_import.py +++ b/src/util/config_import.py @@ -3,7 +3,7 @@ def import_config(): sys.path.append(os.path.join(os.path.dirname(__file__), "..")) - import config + from src import config # reset system path to original state sys.path.pop() return config \ No newline at end of file diff --git a/util/flask_utils.py b/src/util/flask_utils.py similarity index 95% rename from util/flask_utils.py rename to src/util/flask_utils.py index 36e0ff6..259991b 100644 --- a/util/flask_utils.py +++ b/src/util/flask_utils.py @@ -80,8 +80,9 @@ def rate_limit(self): class FlaskApp(flask.Flask): def __init__(self, config: FlaskAppConfig): - super().__init__(config.name) - log = Log(config.name, enable_perf=config.enable_perf) + name = config.name if config.name else __name__ + super().__init__(name) + log = Log(name, enable_perf=config.enable_perf) log.set_level(config.log_level) self.log = log.logger diff --git a/util/parse.py b/src/util/parse.py similarity index 100% rename from util/parse.py rename to src/util/parse.py diff --git a/web3client/__init__.py b/src/web3client/__init__.py similarity index 100% rename from web3client/__init__.py rename to src/web3client/__init__.py diff --git a/web3client/abi_manager.py b/src/web3client/abi_manager.py similarity index 100% rename from web3client/abi_manager.py rename to src/web3client/abi_manager.py diff --git a/web3client/abis/RewardRatePool.json b/src/web3client/abis/RewardRatePool.json similarity index 100% rename from web3client/abis/RewardRatePool.json rename to src/web3client/abis/RewardRatePool.json diff --git a/web3client/abis/ServiceNodeContribution.json b/src/web3client/abis/ServiceNodeContribution.json similarity index 100% rename from web3client/abis/ServiceNodeContribution.json rename to src/web3client/abis/ServiceNodeContribution.json diff --git a/web3client/abis/ServiceNodeContributionFactory.json b/src/web3client/abis/ServiceNodeContributionFactory.json similarity index 100% rename from web3client/abis/ServiceNodeContributionFactory.json rename to src/web3client/abis/ServiceNodeContributionFactory.json diff --git a/web3client/abis/ServiceNodeRewards.json b/src/web3client/abis/ServiceNodeRewards.json similarity index 100% rename from web3client/abis/ServiceNodeRewards.json rename to src/web3client/abis/ServiceNodeRewards.json diff --git a/web3client/abis/Token.json b/src/web3client/abis/Token.json similarity index 100% rename from web3client/abis/Token.json rename to src/web3client/abis/Token.json diff --git a/web3client/client.py b/src/web3client/client.py similarity index 100% rename from web3client/client.py rename to src/web3client/client.py diff --git a/web3client/contracts/__init__.py b/src/web3client/contracts/__init__.py similarity index 100% rename from web3client/contracts/__init__.py rename to src/web3client/contracts/__init__.py diff --git a/web3client/contracts/contract.py b/src/web3client/contracts/contract.py similarity index 100% rename from web3client/contracts/contract.py rename to src/web3client/contracts/contract.py diff --git a/web3client/contracts/reward_rate_pool.py b/src/web3client/contracts/reward_rate_pool.py similarity index 100% rename from web3client/contracts/reward_rate_pool.py rename to src/web3client/contracts/reward_rate_pool.py diff --git a/web3client/contracts/service_node_contribution.py b/src/web3client/contracts/service_node_contribution.py similarity index 100% rename from web3client/contracts/service_node_contribution.py rename to src/web3client/contracts/service_node_contribution.py diff --git a/web3client/contracts/service_node_contribution_factory.py b/src/web3client/contracts/service_node_contribution_factory.py similarity index 100% rename from web3client/contracts/service_node_contribution_factory.py rename to src/web3client/contracts/service_node_contribution_factory.py diff --git a/web3client/contracts/service_node_rewards.py b/src/web3client/contracts/service_node_rewards.py similarity index 100% rename from web3client/contracts/service_node_rewards.py rename to src/web3client/contracts/service_node_rewards.py diff --git a/web3client/contracts/token.py b/src/web3client/contracts/token.py similarity index 100% rename from web3client/contracts/token.py rename to src/web3client/contracts/token.py diff --git a/web3client/contracts/token_tests.py b/src/web3client/contracts/token_tests.py similarity index 100% rename from web3client/contracts/token_tests.py rename to src/web3client/contracts/token_tests.py diff --git a/web3client/event_scanner.py b/src/web3client/event_scanner.py similarity index 100% rename from web3client/event_scanner.py rename to src/web3client/event_scanner.py From 02fb28e8d38076d87632fdae6630fb3cde4e0f01 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Wed, 29 Jan 2025 16:25:55 +1100 Subject: [PATCH 079/138] chore: move configs --- src/config.py | 3 +++ config_defaults.py => src/config_defaults.py | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) rename config_defaults.py => src/config_defaults.py (99%) diff --git a/src/config.py b/src/config.py index 51cac52..705a6fd 100644 --- a/src/config.py +++ b/src/config.py @@ -12,3 +12,6 @@ # backend = stagenet_backend # backend.provider_url = 'tcp://127.0.0.1:6786' + +# backend.log_level = CUSTOM_LOG_LEVELS["SILLY"] +# backend.log_level = logging.INFO \ No newline at end of file diff --git a/config_defaults.py b/src/config_defaults.py similarity index 99% rename from config_defaults.py rename to src/config_defaults.py index 960793b..5ce437f 100644 --- a/config_defaults.py +++ b/src/config_defaults.py @@ -35,7 +35,8 @@ class Backend: rpc_api: str = "" rpc_api_cache: int = 2 rpc_api_usage_logging: bool = False - rpc_api_usage_logging_interval: int = 600 + rpc_api_usage_logging_interval: int = 300 + """ REGISTRATION CONFIG """ From 85879051dde385ce35039ca2bc72edf114a14499 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Thu, 30 Jan 2025 14:11:35 +1100 Subject: [PATCH 080/138] fix: make db readers read only --- src/db/read.py | 378 +-------------- src/db/write.py | 740 +--------------------------- src/fetcher.py | 10 +- src/price/app.py | 14 +- src/price/read.py | 14 +- src/price/write.py | 12 +- src/registration/read.py | 2 +- src/snapshot/app.py | 2 +- src/staking/app.py | 6 +- src/{db => staking}/dataclasses.py | 4 +- src/staking/read.py | 378 +++++++++++++++ src/{db => staking}/schema.sql | 0 src/{db => staking}/snapshot.py | 2 +- src/staking/write.py | 744 +++++++++++++++++++++++++++++ 14 files changed, 1167 insertions(+), 1139 deletions(-) rename src/{db => staking}/dataclasses.py (98%) create mode 100644 src/staking/read.py rename src/{db => staking}/schema.sql (100%) rename src/{db => staking}/snapshot.py (99%) create mode 100644 src/staking/write.py diff --git a/src/db/read.py b/src/db/read.py index e95881e..85cae32 100644 --- a/src/db/read.py +++ b/src/db/read.py @@ -1,379 +1,19 @@ import sqlite3 -from contextlib import closing - -from ..db.dataclasses import DBNode, DBContributionMain, DBNetworkInfo, DBContributionContract, \ - DBContributionContractContribution, SmartContractABI, ArbitrumInfo -from ..log import Log -from ..util.parse import eth_format -from ..web3client.event_scanner import ProcessedEvent +from src.log import Log class DBReader: def __init__(self, db_path: str, log_level: int, perf: bool = False): - self.db_path = db_path self.log = Log("db_reader", log_level, enable_perf=perf).logger - def get_last_fetched_network_block_height(self) -> int: - self.log.perf.start("get_last_fetched_network_block_height") - with closing(sqlite3.connect(self.db_path)) as connection: - with closing(connection.cursor()) as cursor: - cursor.execute("SELECT MAX(fetched_block_height) FROM service_nodes_staging") - (fetched_block_height,) = cursor.fetchone() - self.log.debug( - "get_last_fetched_network_block_height result: {}".format(fetched_block_height) - ) - self.log.perf.end("get_last_fetched_network_block_height") - return fetched_block_height if fetched_block_height is not None else 0 - - def get_last_commited_network_block_height(self) -> int: - self.log.perf.start("get_last_commited_network_block_height") - with closing(sqlite3.connect(self.db_path)) as connection: - with closing(connection.cursor()) as cursor: - cursor.execute("SELECT MAX(fetched_block_height) FROM service_nodes_main") - (commited_block_height,) = cursor.fetchone() - self.log.debug( - "get_last_commited_network_block_height result: {}".format( - commited_block_height - ) - ) - self.log.perf.end("get_last_commited_network_block_height") - return commited_block_height if commited_block_height is not None else 0 - - def get_network_info(self): - self.log.perf.start("get_network_info") - with closing(sqlite3.connect(self.db_path)) as connection: - with closing(connection.cursor()) as cursor: - cursor.execute("SELECT * FROM network_info LIMIT 1") - network_info = DBNetworkInfo(*cursor.fetchone()) - - self.log.debug("Network Info: {}".format(network_info)) - self.log.perf.end("get_network_info") - return network_info - - def get_last_fetched_arbitrum_event_block_height(self) -> int: - self.log.perf.start("get_last_fetched_arbitrum_event_block_height") - with closing(sqlite3.connect(self.db_path)) as connection: - with closing(connection.cursor()) as cursor: - cursor.execute("SELECT MAX(block) FROM arbitrum_events") - (fetched_block_height,) = cursor.fetchone() - self.log.debug( - "get_last_fetched_arbitrum_event_block_height result: {}".format( - fetched_block_height - ) - ) - self.log.perf.end("get_last_fetched_arbitrum_event_block_height") - return fetched_block_height if fetched_block_height is not None else 0 - - def get_contribution_contracts(self): - self.log.perf.start("get_contribution_contracts") - with closing(sqlite3.connect(self.db_path)) as connection: - with closing(connection.cursor()) as cursor: - cursor.execute("""SELECT * FROM contribution_contracts""") - contracts = cursor.fetchall() - - parsed_contracts = {} - for contract in contracts: - contract_dict = DBContributionContract(*contract, contributors=[]) - parsed_contracts[contract_dict.address] = contract_dict - - cursor.execute( - """ - SELECT * FROM contribution_contracts_contributions - """ - ) - contributions = cursor.fetchall() - for contribution in contributions: - contribution_dict = DBContributionContractContribution(*contribution) - parsed_contracts[contribution_dict.contract_address].contributors.append( - contribution_dict - ) - - self.log.debug("Parsed contribution contracts: {}".format(len(parsed_contracts))) - self.log.perf.end("get_contribution_contracts") - return list(parsed_contracts.values()) - - def get_contribution_contract_addresses(self): - self.log.perf.start("get_contribution_contracts") - with closing(sqlite3.connect(self.db_path)) as connection: - with closing(connection.cursor()) as cursor: - cursor.execute( - """ - SELECT address FROM contribution_contracts - """ - ) - addresses = cursor.fetchall() - self.log.debug("Contract addresses: {}".format(len(addresses))) - self.log.perf.end("get_contribution_contracts") - return [address[0] for address in addresses] - - def get_nodes(self): - self.log.perf.start("get_nodes") - with closing(sqlite3.connect(self.db_path)) as connection: - with closing(connection.cursor()) as cursor: - parsed_nodes = {} - - # TODO: investigate using a join or something less messy than two select * queries - cursor.execute("""SELECT * FROM service_nodes_main""") - - for node in cursor.fetchall(): - node_dict = DBNode(*node, contributors=[], events=[]) - parsed_nodes[node_dict.contract_id] = node_dict - - # We want to sort by fetched_block_height in ascending order so later updates overwrite earlier ones - cursor.execute( - """SELECT * FROM service_nodes_staging ORDER BY fetched_block_height ASC""" - ) - - for node in cursor.fetchall(): - node_dict = DBNode(*node, exit_type=None, deregistration_height=None, liquidation_height=None, contributors=[], events=[]) - existing_node = parsed_nodes.get(node_dict.contract_id) - if existing_node is not None: - existing_node.exit_type = node_dict.exit_type - existing_node.deregistration_height = node_dict.deregistration_height - existing_node.liquidation_height = node_dict.liquidation_height - parsed_nodes[node_dict.contract_id] = node_dict - - - cursor.execute("""SELECT * from service_nodes_contributions_main""") - - db_contributions_main = [DBContributionMain(*contribution) for contribution in cursor.fetchall()] - - # We want to sort by fetched_block_height in ascending order so later updates overwrite earlier ones - cursor.execute( - """SELECT * from service_nodes_contributions_staging ORDER BY fetched_block_height ASC""" - ) - - db_contributions_staging = [DBContributionMain(*contribution) for contribution in cursor.fetchall()] - - parsed_contributions = {} - for contribution_dict in db_contributions_main + db_contributions_staging: - # TODO: there has to be a better way to override the old data with new data - key = f"{contribution_dict.contract_id}{contribution_dict.address}" - parsed_contributions[key] = contribution_dict - - for contribution_dict in parsed_contributions.values(): - parsed_nodes[contribution_dict.contract_id].contributors.append( - contribution_dict - ) + if not db_path.startswith("file://"): + db_path = "file://" + db_path - contract_ids = list(parsed_nodes.keys()) + if not db_path.endswith("?mode=ro"): + db_path = db_path + "?mode=ro" - placeholder= '?' # For SQLite. See DBAPI paramstyle. - placeholders= ', '.join(placeholder for unused in contract_ids) - query= 'SELECT * FROM arbitrum_events WHERE main_arg IN (%s) ORDER BY block DESC' % placeholders - cursor.execute(query, contract_ids) - - for event in cursor.fetchall(): - processed_event = ProcessedEvent(*event) - try: - contract_id = int(processed_event.main_arg) - parsed_nodes[contract_id].events.append(processed_event) - except Exception as e: - self.log.error("Error processing event: {}".format(e)) - continue - - nodes_list = list(parsed_nodes.values()) - - self.log.debug("Parsed nodes: {}".format(len(nodes_list))) - self.log.perf.end("get_nodes") - return list(parsed_nodes.values()) - - def get_rewards_info(self): - self.log.perf.start("get_rewards_info") - with closing(sqlite3.connect(self.db_path)) as connection: - with closing(connection.cursor()) as cursor: - cursor.execute("SELECT * FROM rewards_info") - rewards_info = { - eth_format(address_hex): rewards - for address_hex, rewards in cursor.fetchall() - } - self.log.debug("Rewards info: {}".format(len(rewards_info))) - self.log.perf.end("get_rewards_info") - return rewards_info - - def get_smart_contract_abis(self): - self.log.perf.start("get_smart_contract_abis") - with closing(sqlite3.connect(self.db_path)) as connection: - with closing(connection.cursor()) as cursor: - cursor.execute( - """ - SELECT * FROM smart_contract_abis - """ - ) - abis = [SmartContractABI(*abi) for abi in cursor.fetchall()] - self.log.debug("Smart contract abis: {}".format(len(abis))) - self.log.perf.end("get_smart_contract_abis") - return abis - - def get_smart_contract_abi(self, name: str): - self.log.perf.start("get_smart_contract_abi") - with closing(sqlite3.connect(self.db_path)) as connection: - with closing(connection.cursor()) as cursor: - cursor.execute( - """ - SELECT * FROM smart_contract_abis WHERE name = ? - """, - (name,), - ) - abi = SmartContractABI(*cursor.fetchone()) - self.log.debug("Smart contract abi: {}".format(abi)) - self.log.perf.end("get_smart_contract_abi") - return abi - - def get_smart_contract_names(self) -> list[str]: - self.log.perf.start("get_smart_contract_names") - with closing(sqlite3.connect(self.db_path)) as connection: - with closing(connection.cursor()) as cursor: - cursor.execute( - """ - SELECT name FROM smart_contract_abis - """ - ) - names = [name[0] for name in cursor.fetchall()] - self.log.debug("Smart contract names: {}".format(len(names))) - self.log.perf.end("get_smart_contract_names") - return names - - def get_smart_contract_addresses(self): - self.log.perf.start("get_smart_contract_addresses") - with closing(sqlite3.connect(self.db_path)) as connection: - with closing(connection.cursor()) as cursor: - cursor.execute( - """ - SELECT address, name FROM smart_contracts - """ - ) - addresses = [ - {"address": address, "name": name} for address, name in cursor.fetchall() - ] - self.log.debug("Smart contract addresses: {}".format(len(addresses))) - self.log.perf.end("get_smart_contract_addresses") - return addresses - - def get_smart_contract_addresses_core(self): - self.log.perf.start("get_smart_contract_addresses_core") - with closing(sqlite3.connect(self.db_path)) as connection: - with closing(connection.cursor()) as cursor: - cursor.execute( - """ - SELECT address, name FROM smart_contracts WHERE name IN ('ServiceNodeRewards', 'ServiceNodeContributionFactory', 'ServiceNodeRewards') - """ - ) - addresses = [ - {"address": address, "name": name} for address, name in cursor.fetchall() - ] - self.log.debug("Smart contract addresses: {}".format(len(addresses))) - self.log.perf.end("get_smart_contract_addresses_core") - return addresses - - def get_smart_contract_address(self, name: str): - self.log.perf.start("get_smart_contract_address") - with closing(sqlite3.connect(self.db_path)) as connection: - with closing(connection.cursor()) as cursor: - cursor.execute( - """ - SELECT address FROM smart_contracts WHERE name = ? - """, - (name,), - ) - address = cursor.fetchone() - self.log.debug("Smart contract address: {}".format(address)) - self.log.perf.end("get_smart_contract_address") - return address[0] - - def get_arbitrum_events_page(self, args=None): - if args is None: - args = [1000, 0] - self.log.perf.start("get_arbitrum_events") - with closing(sqlite3.connect(self.db_path)) as connection: - with closing(connection.cursor()) as cursor: - limit = args[0] if len(args) > 0 else 1000 - skip = args[1] if len(args) > 1 else 0 - - cursor.execute( - """ - SELECT * FROM arbitrum_events ORDER BY block DESC LIMIT ? OFFSET ? - """, - (limit, skip), - ) - events = [ProcessedEvent(*event) for event in cursor.fetchall()] - self.log.debug("Arbitrum events: {}".format(len(events))) - self.log.perf.end("get_arbitrum_events") - - cursor.execute("SELECT COUNT(*) FROM arbitrum_events") - total = cursor.fetchone()[0] - - return events, limit, skip, total - - def get_arbitrum_events_since_timestamp(self, params: [int, list[str] | None]) -> list[ProcessedEvent]: - timestamp = params[0] if len(params) > 0 else None - events_types = params[1] if len(params) > 1 and len(params[1]) > 0 else None - - if timestamp is None or (not isinstance(timestamp, int) and not isinstance(timestamp, float)): - raise ValueError("Invalid timestamp, timestamp must be an integer or float") - - if events_types is not None: - if isinstance(events_types, str): - events_types = [events_types] - elif not isinstance(events_types, list): - raise ValueError("Invalid events_types, events_types must be a list of strings or a string") - - - self.log.perf.start("get_arbitrum_events_since_timestamp") - with closing(sqlite3.connect(self.db_path)) as connection: - with closing(connection.cursor()) as cursor: - if events_types is None: - cursor.execute("SELECT * FROM arbitrum_events WHERE timestamp > ? ORDER BY timestamp DESC", (timestamp,)) - else: - placeholder= '?' # For SQLite. See DBAPI paramstyle. - placeholders= ', '.join(placeholder for unused in events_types) - query= 'SELECT * FROM arbitrum_events WHERE timestamp > ? AND name IN (%s) ORDER BY timestamp DESC' % placeholders - cursor.execute(query, (timestamp, *events_types)) - # cursor.execute("SELECT * FROM arbitrum_events WHERE timestamp > ? AND name IN ({}) ORDER BY timestamp DESC".format(",".join(["?"]*len(events_types))), tuple(events_types)+(timestamp,)) - events = [ProcessedEvent(*event) for event in cursor.fetchall()] - self.log.debug("Arbitrum events: {}".format(len(events))) - self.log.perf.end("get_arbitrum_events_since_timestamp") - return events - - def get_arbitrum_info(self): - self.log.perf.start("get_arbitrum_info") - with closing(sqlite3.connect(self.db_path)) as connection: - with closing(connection.cursor()) as cursor: - cursor.execute("SELECT * FROM arbitrum_info ORDER BY block DESC LIMIT 1") - info = ArbitrumInfo(*cursor.fetchone()) - - self.log.debug("Arbitrum info: {}".format(info)) - self.log.perf.end("get_arbitrum_info") - return info - - def get_arbitrum_events_for_stake_contrat_id(self, contract_id: int): - self.log.perf.start("get_events_for_stake_contrat_id") - with closing(sqlite3.connect(self.db_path)) as connection: - with closing(connection.cursor()) as cursor: - cursor.execute( - """ - SELECT * FROM arbitrum_events WHERE main_arg = ? ORDER BY block DESC - """, - (contract_id,), - ) - events = [ProcessedEvent(*event) for event in cursor.fetchall()] - self.log.debug("Arbitrum events: {}".format(len(events))) - self.log.perf.end("get_events_for_stake_contrat_id") - return events + self.log.info(f"Connecting to db at {db_path}") + self.db_path = db_path - def get_service_node_rewards_contract_id_bls_key_map(self): - self.log.perf.start("get_service_node_rewards_contract_id_bls_key_map") - with closing(sqlite3.connect(self.db_path)) as connection: - with closing(connection.cursor()) as cursor: - cursor.execute( - """ - SELECT contract_id, pubkey_bls FROM service_node_rewards_contract_id_bls_key_map - """ - ) - contract_id_map = { - pubkey_bls: contract_id - for contract_id, pubkey_bls in cursor.fetchall() - } - self.log.debug("Service node rewards contract id bls key map: {}".format(len(contract_id_map))) - self.log.perf.end("get_service_node_rewards_contract_id_bls_key_map") - return contract_id_map \ No newline at end of file + def connect(self): + return sqlite3.connect(self.db_path, uri=True) diff --git a/src/db/write.py b/src/db/write.py index 43dd82e..1d7c4be 100644 --- a/src/db/write.py +++ b/src/db/write.py @@ -1,744 +1,12 @@ -import json import sqlite3 -import time -from contextlib import closing -from web3 import Web3 - -from ..staking.arbitrum import ContributionContractDetails -from ..db.dataclasses import RewardsInfo, DBNodeExit -from ..log import Log -from ..oxen.rpc import ServiceNode, NetworkInfo -from ..web3client.abi_manager import ABIData -from ..web3client.event_scanner import ProcessedEvent +from src.log import Log class DBWriter: def __init__(self, db_path: str, log_level: int, perf: bool = False): self.db_path = db_path self.log = Log("db_writer", log_level, enable_perf=perf).logger + self.log.info(f"Connecting to db at {db_path}") - def write_nodes_to_staging_db( - self, - height: int, - parsed_nodes: list[ServiceNode], - # TODO: type the contributor_stake_map properly - contributions: list[dict[str, int]], - ): - self.log.perf.start("write_to_db") - - with closing(sqlite3.connect(self.db_path)) as connection: - connection.execute("BEGIN") - with closing(connection.cursor()) as cursor: - - self.log.debug("Inserting {} service nodes".format(len(parsed_nodes))) - self.log.perf.start("write_nodes_to_staging_db -> insert nodes") - - cursor.executemany( - """ - INSERT INTO service_nodes_staging ( - active, - contract_id, - decommission_count, - earned_downtime_blocks, - fetched_block_height, - funded, - is_liquidatable, - is_removable, - last_reward_block_height, - last_uptime_proof, - lokinet_version, - operator_address, - operator_fee, - payable, - pubkey_bls, - pubkey_ed25519, - public_ip, - pulse_votes, - quorumnet_port, - registration_height, - registration_hf_version, - requested_unlock_height, - service_node_pubkey, - service_node_version, - staking_requirement, - state_height, - storage_lmq_port, - storage_port, - storage_server_version, - swarm, - swarm_id, - total_contributed - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - ( - node.get("active"), - node.get("contract_id"), - node.get("decommission_count"), - node.get("earned_downtime_blocks"), - height, - node.get("funded"), - node.get("is_liquidatable"), - node.get("is_removable"), - node.get("last_reward_block_height"), - node.get("last_uptime_proof"), - node.get("lokinet_version"), - node.get("operator_address"), - node.get("operator_fee"), - node.get("payable"), - node.get("pubkey_bls"), - node.get("pubkey_ed25519"), - node.get("public_ip"), - node.get("pulse_votes"), - node.get("quorumnet_port"), - node.get("registration_height"), - node.get("registration_hf_version"), - node.get("requested_unlock_height"), - node.get("service_node_pubkey"), - node.get("service_node_version"), - node.get("staking_requirement"), - node.get("state_height"), - node.get("storage_lmq_port"), - node.get("storage_port"), - node.get("storage_server_version"), - node.get("swarm"), - node.get("swarm_id"), - node.get("total_contributed"), - ) - for node in parsed_nodes - ), - ) - - inserted_nodes_rows = cursor.rowcount - - self.log.perf.end("write_nodes_to_staging_db -> insert nodes") - self.log.debug( - "Inserted {} rows into service_nodes_staging".format(inserted_nodes_rows) - ) - self.log.debug("Inserting {} service node contributions".format(len(contributions))) - self.log.perf.start("write_nodes_to_staging_db -> insert contributions") - - cursor.executemany( - """ - INSERT OR REPLACE INTO service_nodes_contributions_staging ( - address, - amount, - beneficiary, - contract_id, - fetched_block_height - ) - VALUES (?, ?, ?, ?, ?) - """, - ( - ( - contribution["address"], - contribution["amount"], - contribution["beneficiary"], - contribution["contract_id"], - height, - ) - for contribution in contributions - ), - ) - - inserted_contributions_rows = cursor.rowcount - - self.log.perf.end("write_nodes_to_staging_db -> insert contributions") - self.log.debug( - "Inserted {} rows into service_nodes_contributions_staging".format( - inserted_contributions_rows - ) - ) - - connection.commit() - self.log.perf.end("write_to_db") - - def write_nodes_to_main_db(self, immutable_height: int): - """ - Gets all nodes from the staging db at or below the immutable_height and writes them to the main db then remove - those nodes from the staging db. - """ - self.log.perf.start("write_nodes_to_main_db") - with closing(sqlite3.connect(self.db_path)) as connection: - connection.execute("BEGIN") - with closing(connection.cursor()) as cursor: - self.log.perf.start("write_nodes_to_main_db -> select nodes") - - cursor.execute( - """ - SELECT * FROM service_nodes_staging WHERE fetched_block_height = ? - """, - (immutable_height,), - ) - nodes = cursor.fetchall() - selected_nodes_count = len(nodes) - - self.log.perf.end("write_nodes_to_main_db -> select nodes") - self.log.info("Found {} nodes to write to main db".format(selected_nodes_count)) - - # We only want to continue here if there are any nodes ready to commit. - if selected_nodes_count == 0: - self.log.debug("No nodes ready to commit") - return - - self.log.perf.start("write_nodes_to_main_db -> insert nodes") - - cursor.executemany( - """ - INSERT OR REPLACE INTO service_nodes_main ( - active, - contract_id, - decommission_count, - earned_downtime_blocks, - fetched_block_height, - funded, - is_liquidatable, - is_removable, - last_reward_block_height, - last_uptime_proof, - lokinet_version, - operator_address, - operator_fee, - payable, - pubkey_bls, - pubkey_ed25519, - public_ip, - pulse_votes, - quorumnet_port, - registration_height, - registration_hf_version, - requested_unlock_height, - service_node_pubkey, - service_node_version, - staking_requirement, - state_height, - storage_lmq_port, - storage_port, - storage_server_version, - swarm, - swarm_id, - total_contributed - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - nodes, - ) - - inserted_or_updated_rows = cursor.rowcount - - self.log.perf.end("write_nodes_to_main_db -> insert nodes") - self.log.info("Wrote {} rows to main db".format(inserted_or_updated_rows)) - - if inserted_or_updated_rows != len(nodes): - self.log.error( - "Inserted or updated {} rows, but expected {}".format( - inserted_or_updated_rows, len(nodes) - ) - ) - connection.rollback() - self.log.perf.end("write_nodes_to_main_db") - return - - self.log.perf.start("write_nodes_to_main_db -> select contributions") - - cursor.execute( - """ - SELECT * FROM service_nodes_contributions_staging WHERE fetched_block_height = ? - """, - (immutable_height,), - ) - contributions = cursor.fetchall() - selected_contributions_count = len(contributions) - - self.log.perf.end("write_nodes_to_main_db -> select contributions") - self.log.debug( - "Found {} contributions to write to main db".format( - selected_contributions_count - ) - ) - self.log.perf.start("write_nodes_to_main_db -> insert contributions") - - cursor.executemany( - """ - INSERT OR REPLACE INTO service_nodes_contributions_main (address, amount, beneficiary, contract_id, fetched_block_height) - VALUES (?, ?, ?, ?, ?) - """, - contributions, - ) - - inserted_contributions_rows = cursor.rowcount - - self.log.perf.end("write_nodes_to_main_db -> insert contributions") - self.log.info("Wrote {} rows to main db".format(inserted_contributions_rows)) - - if inserted_contributions_rows != len(contributions): - self.log.error( - "Inserted {} rows, but expected {}".format( - inserted_contributions_rows, len(contributions) - ) - ) - connection.rollback() - return - - self.log.perf.start("write_nodes_to_main_db -> delete nodes") - - cursor.execute( - """ - DELETE FROM service_nodes_staging WHERE fetched_block_height <= ? - """, - (immutable_height,), - ) - - deleted_rows = cursor.rowcount - - self.log.perf.end("write_nodes_to_main_db -> delete nodes") - self.log.info("Deleted {} rows from staging db".format(deleted_rows)) - self.log.perf.start("write_nodes_to_main_db -> delete contributions") - - cursor.execute( - """ - DELETE FROM service_nodes_contributions_staging WHERE fetched_block_height <= ? - """, - (immutable_height,), - ) - - deleted_contributions_rows = cursor.rowcount - - self.log.perf.end("write_nodes_to_main_db -> delete contributions") - self.log.info( - "Deleted {} rows from staging contributions db".format( - deleted_contributions_rows - ) - ) - - connection.commit() - self.log.info("Transaction committed successfully") - - self.log.perf.end("write_nodes_to_main_db") - - def write_exit_list_to_db(self, exit_list: list[DBNodeExit]): - self.log.perf.start("write_exit_list_to_db") - with closing(sqlite3.connect(self.db_path)) as connection: - connection.execute("BEGIN") - with closing(connection.cursor()) as cursor: - self.log.debug("Updating nodes in main with {} exit events".format(len(exit_list))) - self.log.perf.start("write_exit_list_to_db -> insert exit events") - cursor.executemany( - """ - UPDATE service_nodes_main SET - deregistration_height = ?, - exit_type = ?, - liquidation_height = ? - WHERE pubkey_bls = ? - """, - ( - ( - e.deregistration_height, - e.exit_type, - e.liquidation_height, - e.pubkey_bls, - ) - for e in exit_list - ) - ) - inserted_exit_rows = cursor.rowcount - - self.log.perf.end("write_exit_list_to_db -> insert exit events") - self.log.debug( - "Inserted {} rows into exit events".format(inserted_exit_rows) - ) - - connection.commit() - self.log.perf.end("write_exit_list_to_db") - - def write_network_info_to_db( - self, - network: NetworkInfo, - node_count: int, - active_node_count: int, - ): - self.log.perf.start("write_network_info_to_db") - with closing(sqlite3.connect(self.db_path)) as connection: - with closing(connection.cursor()) as cursor: - cursor.execute( - """ - INSERT OR REPLACE INTO network_info ( - id, - active_node_count, - block_hash, - block_height, - block_timestamp, - hard_fork, - immutable_block_hash, - immutable_block_height, - max_stakers, - min_operator_contribution, - node_count, - nettype, - pulse_target_timestamp, - staking_requirement, - version - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - 1, - active_node_count, - network.block_hash, - network.block_height, - time.time().__floor__(), - network.hard_fork, - network.immutable_block_hash, - network.immutable_block_height, - network.max_stakers, - network.min_operator_contribution, - node_count, - network.nettype, - network.pulse_target_timestamp, - network.staking_requirement, - network.version, - ), - ) - connection.commit() - self.log.perf.end("write_network_info_to_db") - - def write_rewards_info_to_db(self, rewards_info: list[RewardsInfo]): - self.log.perf.start("write_rewards_info_to_db") - with closing(sqlite3.connect(self.db_path)) as connection: - connection.execute("BEGIN") - with closing(connection.cursor()) as cursor: - self.log.debug("Inserting {} rewards info".format(len(rewards_info))) - self.log.perf.start("write_rewards_info_to_db -> insert rewards info") - - cursor.executemany( - """ - INSERT OR REPLACE INTO rewards_info (address, rewards) - VALUES (?, ?) - """, - ( - ( - info.address, - info.rewards, - ) - for info in rewards_info - ), - ) - - inserted_rewards_rows = cursor.rowcount - - self.log.perf.end("write_rewards_info_to_db -> insert rewards info") - self.log.debug( - "Inserted {} rows into rewards_info".format(inserted_rewards_rows) - ) - - connection.commit() - self.log.perf.end("write_rewards_info_to_db") - - def write_arbitrum_events_to_db(self, events: list[ProcessedEvent]): - self.log.perf.start("write_arbitrum_events_to_db") - - with closing(sqlite3.connect(self.db_path)) as connection: - connection.execute("BEGIN") - with closing(connection.cursor()) as cursor: - - self.log.debug("Inserting {} events into arbitrum_events".format(len(events))) - self.log.perf.start("write_arbitrum_events_to_db -> insert events") - - cursor.executemany( - """ - INSERT OR REPLACE INTO arbitrum_events ( - block, - timestamp, - tx, - name, - main_arg, - args - ) - VALUES (?, ?, ?, ?, ?, ?) - """, - ( - ( - event.block, - event.timestamp, - "0x" + event.tx, - event.name, - event.main_arg, - Web3.to_json(dict(event.args)), - ) - for event in events - ), - ) - - inserted_or_updated_rows_count = cursor.rowcount - - self.log.perf.end("write_arbitrum_events_to_db -> insert events") - self.log.debug( - "Inserted or updated {} rows into arbitrum_events".format( - inserted_or_updated_rows_count - ) - ) - - connection.commit() - self.log.perf.end("write_arbitrum_events_to_db") - - def write_contribution_contracts_to_db( - self, contracts: list[ContributionContractDetails], contributions_list: list, add_event_timestamps: dict[str, int], node_last_added_timestamps: dict[str,int], create_contract_timestamps: dict[str, int] - ): - self.log.perf.start("write_contribution_contracts_to_db") - - with closing(sqlite3.connect(self.db_path)) as connection: - connection.execute("BEGIN") - with closing(connection.cursor()) as cursor: - - self.log.debug("Inserting {} contribution contracts".format(len(contracts))) - self.log.perf.start("write_contribution_contracts_to_db -> insert contracts") - - cursor.executemany( - """ - INSERT OR REPLACE INTO contribution_contracts ( - address, - created_timestamp, - fee, - last_added_timestamp, - manual_finalize, - node_add_timestamp, - operator_address, - pubkey_bls, - service_node_pubkey, - service_node_signature, - status - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - ( - contract.address, - create_contract_timestamps.get(contract.address), - contract.fee, - node_last_added_timestamps.get(contract.pubkey_bls), - contract.manual_finalize, - add_event_timestamps.get(contract.pubkey_bls), - contract.operator_address, - contract.pubkey_bls, - contract.service_node_pubkey, - contract.service_node_signature, - contract.status, - ) - for contract in contracts - ), - ) - - inserted_contract_rows = cursor.rowcount - - self.log.perf.end("write_contribution_contracts_to_db -> insert contracts") - self.log.debug( - "Inserted or Updated {} rows into contribution_contracts".format( - inserted_contract_rows - ) - ) - self.log.perf.start("write_contribution_contracts_to_db -> delete contributions") - - # The contributors for a contact need to be deleted before the contract can be inserted again to account - # for contract resets, or contributors leaving the contract. We could read from the db and only delete - # the missing ones but this should be more performant. - # TODO: investigate a better solution - cursor.executemany( - """DELETE FROM contribution_contracts_contributions WHERE contract_address = ?""", - (( - contract.address, - ) - for contract in contracts) - ) - - deleted_contributions_rows = cursor.rowcount - - self.log.perf.end("write_contribution_contracts_to_db -> delete contributions") - self.log.debug( - "Deleted {} rows from contribution_contracts_contributions".format( - deleted_contributions_rows - ) - ) - self.log.debug( - "Inserting {} contract contributions".format(len(contributions_list)) - ) - self.log.perf.start( - "write_contribution_contracts_to_db -> insert contribution contracts contributions" - ) - - cursor.executemany( - """ - INSERT INTO contribution_contracts_contributions ( - address, - amount, - beneficiary_address, - contract_address, - reserved - ) - VALUES (?, ?, ?, ?, ?) - """, - ( - ( - contribution["address"], - contribution["amount"], - contribution["beneficiary_address"], - contribution["contract_address"], - contribution["reserved"], - ) - for contribution in contributions_list - ), - ) - - inserted_contributions_rows = cursor.rowcount - - self.log.perf.end( - "write_contribution_contracts_to_db -> insert contribution contracts contributions" - ) - self.log.debug( - "Inserted {} rows into contribution_contracts_contributions".format( - inserted_contributions_rows - ) - ) - - connection.commit() - self.log.perf.end("write_contribution_contracts_to_db") - - def write_smart_contract_abis_to_db(self, abis: list[ABIData]): - self.log.perf.start("write_smart_contract_abis_to_db") - with closing(sqlite3.connect(self.db_path)) as connection: - connection.execute("BEGIN") - with closing(connection.cursor()) as cursor: - - self.log.debug("Inserting {} smart contract abis".format(len(abis))) - self.log.perf.start("write_smart_contract_abis_to_db -> insert abis") - - cursor.executemany( - """ - INSERT OR REPLACE INTO smart_contract_abis ( - name, - abi, - bytecode, - deployed_bytecode - ) - VALUES (?, ?, ?, ?) - """, - ( - ( - abi.name, - json.dumps(abi.abi), - abi.bytecode, - abi.deployed_bytecode, - ) - for abi in abis - ), - ) - - inserted_abi_rows = cursor.rowcount - - self.log.perf.end("write_smart_contract_abis_to_db -> insert abis") - self.log.debug( - "Inserted {} rows into smart_contract_abis".format(inserted_abi_rows) - ) - - connection.commit() - self.log.perf.end("write_smart_contract_abis_to_db") - - def write_smart_contract_details_to_db( - self, - contracts, - ): - self.log.perf.start("write_smart_contract_details_to_db") - with closing(sqlite3.connect(self.db_path)) as connection: - connection.execute("BEGIN") - with closing(connection.cursor()) as cursor: - - self.log.debug("Inserting {} smart contract details".format(len(contracts))) - self.log.perf.start("write_smart_contract_details_to_db -> insert contracts") - - cursor.executemany( - """ - INSERT OR REPLACE INTO smart_contracts ( - address, - name - ) - VALUES (?, ?) - """, - ( - ( - contract.get("address"), - contract.get("name"), - ) - for contract in contracts - ), - ) - - inserted_details_rows = cursor.rowcount - - self.log.perf.end("write_smart_contract_details_to_db -> insert details") - self.log.debug( - "Inserted {} rows into smart_contract_details".format(inserted_details_rows) - ) - - connection.commit() - self.log.perf.end("write_smart_contract_details_to_db") - - def write_arbitrum_info_to_db(self, current_block, service_node_rewards_balance, reward_rate_pool_balance): - self.log.perf.start("write_arbitrum_info_to_db") - with closing(sqlite3.connect(self.db_path)) as connection: - connection.execute("BEGIN") - with closing(connection.cursor()) as cursor: - self.log.debug( - "Inserting arbitrum info: current block {}, service node rewards balance {}, reward rate pool balance {}".format( - current_block, service_node_rewards_balance, reward_rate_pool_balance)) - self.log.perf.start("write_arbitrum_info_to_db -> insert info") - - cursor.execute("INSERT OR REPLACE INTO arbitrum_info (block, balance_service_node_rewards, balance_reward_rate_pool) VALUES (?, ?, ?)", (current_block, service_node_rewards_balance, reward_rate_pool_balance)) - - inserted_info_rows = cursor.rowcount - - self.log.perf.end("write_arbitrum_info_to_db -> insert info") - self.log.debug( - "Inserted {} rows into arbitrum_info".format(inserted_info_rows) - ) - - connection.commit() - self.log.perf.end("write_arbitrum_info_to_db") - - def write_service_node_rewards_contract_id_bls_key_map(self, contract_id_map: dict[str, str]): - self.log.perf.start("write_service_node_rewards_contract_id_bls_key_map") - with closing(sqlite3.connect(self.db_path)) as connection: - connection.execute("BEGIN") - with closing(connection.cursor()) as cursor: - self.log.debug("Inserting {} service node rewards contract ids".format(len(contract_id_map))) - self.log.perf.start("write_service_node_rewards_contract_id_bls_key_map -> insert contract ids") - - cursor.execute("DELETE FROM service_node_rewards_contract_id_bls_key_map") - - cursor.executemany( - """ - INSERT INTO service_node_rewards_contract_id_bls_key_map ( - contract_id, - pubkey_bls - ) - VALUES (?, ?) - """, - ( - ( - int(contract_id), - pubkey_bls, - ) - for pubkey_bls, contract_id in contract_id_map.items() - ), - ) - - inserted_contract_id_rows = cursor.rowcount - - self.log.perf.end("write_service_node_rewards_contract_id_bls_key_map -> insert contract ids") - self.log.debug( - "Inserted {} rows into service_node_rewards_contract_id_bls_key_map".format( - inserted_contract_id_rows - ) - ) - - connection.commit() - self.log.perf.end("write_service_node_rewards_contract_id_bls_key_map") \ No newline at end of file + def connect(self): + return sqlite3.connect(self.db_path) diff --git a/src/fetcher.py b/src/fetcher.py index 9464521..fa43086 100644 --- a/src/fetcher.py +++ b/src/fetcher.py @@ -10,14 +10,14 @@ update_contribution_contract_details, batch_populate_events_with_block_timestamps, populate_events_with_main_arg, ) from config_validate import validate_config -from db.dataclasses import RewardsInfo, DBNodeExit +from src.staking.dataclasses import RewardsInfo, DBNodeExit from db.util import ( assert_all_dict_values_are_within_sqlite_integer_range, is_db_initialized, init_db, ) -from db.read import DBReader -from db.write import DBWriter +from src.staking.read import DBReaderStaking +from src.staking.write import DBWriterStaking from log import Log from oxen.rpc import ServiceNode, OxenRPC, NetworkInfo from util import format_seconds @@ -65,12 +65,12 @@ def __init__(self, name): ) init_db(config.backend.sqlite_db, config.backend.sqlite_schema) - self.db_reader = DBReader( + self.db_reader = DBReaderStaking( db_path=config.backend.sqlite_db, log_level=config.backend.log_level, perf=config.backend.performance_logging, ) - self.db_writer = DBWriter( + self.db_writer = DBWriterStaking( db_path=config.backend.sqlite_db, log_level=config.backend.log_level, perf=config.backend.performance_logging, diff --git a/src/price/app.py b/src/price/app.py index 8d3365a..fdb38d8 100644 --- a/src/price/app.py +++ b/src/price/app.py @@ -31,7 +31,7 @@ def __init__(self, config: PriceAppConfig): super().__init__(config) self.app_config = config - if not is_db_initialized(config.sqlite_db): + if not is_db_initialized(config.sqlite_db) and config.coingecko_api_url: self.log.info( "Initializing database {} with schema {}".format( config.sqlite_db, config.sqlite_schema @@ -46,11 +46,13 @@ def __init__(self, config: PriceAppConfig): log_level=config.log_level, perf=config.enable_perf, ) - self.db_writer_prices = DBWriterPrices( - db_path=config.sqlite_db, - log_level=config.log_level, - perf=config.enable_perf, - ) + + if config.coingecko_api_url: + self.db_writer_prices = DBWriterPrices( + db_path=config.sqlite_db, + log_level=config.log_level, + perf=config.enable_perf, + ) if config.coingecko_api_url: self.token_price_request = CoinGeckoTokenPriceRequest( diff --git a/src/price/read.py b/src/price/read.py index 39e0bd1..8c48c0d 100644 --- a/src/price/read.py +++ b/src/price/read.py @@ -1,18 +1,16 @@ -import sqlite3 from contextlib import closing from .dataclasses import PriceDB -from ..log import Log +from ..db.read import DBReader -class DBReaderPrices: +class DBReaderPrices(DBReader): def __init__(self, db_path: str, log_level: int, perf: bool = False): - self.db_path = db_path - self.log = Log("db_reader", log_level, enable_perf=perf).logger + super().__init__(db_path, log_level, perf) def get_latest_price(self, token: str, currency: str): self.log.perf.start("get_latest_price") - with closing(sqlite3.connect(self.db_path)) as connection: + with closing(self.connect()) as connection: with closing(connection.cursor()) as cursor: cursor.execute( """ @@ -27,7 +25,7 @@ def get_latest_price(self, token: str, currency: str): def get_latest_prices(self, token: str): self.log.perf.start("get_latest_prices") - with closing(sqlite3.connect(self.db_path)) as connection: + with closing(self.connect()) as connection: with closing(connection.cursor()) as cursor: cursor.execute( """ @@ -47,7 +45,7 @@ def get_latest_prices(self, token: str): def get_unique_currencies(self, token: str): self.log.perf.start("get_unique_currencies") - with closing(sqlite3.connect(self.db_path)) as connection: + with closing(self.connect()) as connection: with closing(connection.cursor()) as cursor: cursor.execute( """ diff --git a/src/price/write.py b/src/price/write.py index 0bb238f..82ba12c 100644 --- a/src/price/write.py +++ b/src/price/write.py @@ -1,19 +1,17 @@ -import sqlite3 from contextlib import closing from .dataclasses import PriceDB -from ..log import Log +from ..db.write import DBWriter -class DBWriterPrices: +class DBWriterPrices(DBWriter): def __init__(self, db_path: str, log_level: int, perf: bool = False): - self.db_path = db_path - self.log = Log("db_writer", log_level, enable_perf=perf).logger + super().__init__(db_path, log_level, perf) def write_prices_to_db(self, prices: list[PriceDB]): self.log.perf.start("write_prices_to_db") - with closing(sqlite3.connect(self.db_path)) as connection: + with closing(self.connect()) as connection: connection.execute("BEGIN") with closing(connection.cursor()) as cursor: self.log.debug("Inserting {} prices".format(len(prices))) @@ -46,7 +44,7 @@ def write_prices_to_db(self, prices: list[PriceDB]): def write_registration_to_db(self, registration): self.log.perf.start("write_registration_to_db") - with closing(sqlite3.connect(self.db_path)) as connection: + with closing(self.connect()) as connection: connection.execute("BEGIN") with closing(connection.cursor()) as cursor: self.log.debug("Inserting {} registration".format(len(registration))) diff --git a/src/registration/read.py b/src/registration/read.py index 26ebec1..f3a629e 100644 --- a/src/registration/read.py +++ b/src/registration/read.py @@ -1,7 +1,7 @@ import sqlite3 from contextlib import closing -from ..db.dataclasses import Registration +from src.staking.dataclasses import Registration from ..log import Log class DBReaderRegistrations: diff --git a/src/snapshot/app.py b/src/snapshot/app.py index 9acca6e..3b0ccb5 100644 --- a/src/snapshot/app.py +++ b/src/snapshot/app.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 from uwsgidecorators import timer -from ..db.snapshot import DBSnapshot +from src.staking.snapshot import DBSnapshot from ..util.flask_utils import FlaskApp diff --git a/src/staking/app.py b/src/staking/app.py index dbc7e38..2a21db3 100644 --- a/src/staking/app.py +++ b/src/staking/app.py @@ -7,8 +7,8 @@ from uwsgidecorators import timer from werkzeug.exceptions import GatewayTimeout -from ..db.dataclasses import ArbitrumInfo -from ..db.read import DBReader +from src.staking.dataclasses import ArbitrumInfo +from src.staking.read import DBReaderStaking from ..oxen.rpc import OxenRPC from ..registration.read import DBReaderRegistrations from ..util.flask_utils import FlaskApp, json_response @@ -22,7 +22,7 @@ def __init__(self, config): log_level=config.backend.log_level, log_level_generic=config.backend.log_level_generic, cache_stale_time_seconds=config.backend.stale_time_seconds) - self.db_reader = DBReader( + self.db_reader = DBReaderStaking( db_path=config.backend.sqlite_db, log_level=config.backend.log_level, perf=config.backend.performance_logging, diff --git a/src/db/dataclasses.py b/src/staking/dataclasses.py similarity index 98% rename from src/db/dataclasses.py rename to src/staking/dataclasses.py index 930d3bf..a783df7 100644 --- a/src/db/dataclasses.py +++ b/src/staking/dataclasses.py @@ -2,8 +2,8 @@ from dataclasses import dataclass from typing import Optional -from ..util.parse import eth_format -from ..web3client.event_scanner import ProcessedEvent +from src.util.parse import eth_format +from src.web3client.event_scanner import ProcessedEvent @dataclass diff --git a/src/staking/read.py b/src/staking/read.py new file mode 100644 index 0000000..a851fbc --- /dev/null +++ b/src/staking/read.py @@ -0,0 +1,378 @@ +import sqlite3 +from contextlib import closing + +from src.db.read import DBReader +from src.staking.dataclasses import DBNode, DBContributionMain, DBNetworkInfo, DBContributionContract, \ + DBContributionContractContribution, SmartContractABI, ArbitrumInfo +from src.util.parse import eth_format +from src.web3client.event_scanner import ProcessedEvent + + +class DBReaderStaking(DBReader): + def __init__(self, db_path: str, log_level: int, perf: bool = False): + super().__init__(db_path, log_level, perf) + + def get_last_fetched_network_block_height(self) -> int: + self.log.perf.start("get_last_fetched_network_block_height") + with closing(self.connect()) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute("SELECT MAX(fetched_block_height) FROM service_nodes_staging") + (fetched_block_height,) = cursor.fetchone() + self.log.debug( + "get_last_fetched_network_block_height result: {}".format(fetched_block_height) + ) + self.log.perf.end("get_last_fetched_network_block_height") + return fetched_block_height if fetched_block_height is not None else 0 + + def get_last_commited_network_block_height(self) -> int: + self.log.perf.start("get_last_commited_network_block_height") + with closing(self.connect()) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute("SELECT MAX(fetched_block_height) FROM service_nodes_main") + (commited_block_height,) = cursor.fetchone() + self.log.debug( + "get_last_commited_network_block_height result: {}".format( + commited_block_height + ) + ) + self.log.perf.end("get_last_commited_network_block_height") + return commited_block_height if commited_block_height is not None else 0 + + def get_network_info(self): + self.log.perf.start("get_network_info") + with closing(sqlite3.connect(self.db_path, uri=True)) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute("SELECT * FROM network_info LIMIT 1") + network_info = DBNetworkInfo(*cursor.fetchone()) + + self.log.debug("Network Info: {}".format(network_info)) + self.log.perf.end("get_network_info") + return network_info + + def get_last_fetched_arbitrum_event_block_height(self) -> int: + self.log.perf.start("get_last_fetched_arbitrum_event_block_height") + with closing(self.connect()) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute("SELECT MAX(block) FROM arbitrum_events") + (fetched_block_height,) = cursor.fetchone() + self.log.debug( + "get_last_fetched_arbitrum_event_block_height result: {}".format( + fetched_block_height + ) + ) + self.log.perf.end("get_last_fetched_arbitrum_event_block_height") + return fetched_block_height if fetched_block_height is not None else 0 + + def get_contribution_contracts(self): + self.log.perf.start("get_contribution_contracts") + with closing(self.connect()) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute("""SELECT * FROM contribution_contracts""") + contracts = cursor.fetchall() + + parsed_contracts = {} + for contract in contracts: + contract_dict = DBContributionContract(*contract, contributors=[]) + parsed_contracts[contract_dict.address] = contract_dict + + cursor.execute( + """ + SELECT * FROM contribution_contracts_contributions + """ + ) + contributions = cursor.fetchall() + for contribution in contributions: + contribution_dict = DBContributionContractContribution(*contribution) + parsed_contracts[contribution_dict.contract_address].contributors.append( + contribution_dict + ) + + self.log.debug("Parsed contribution contracts: {}".format(len(parsed_contracts))) + self.log.perf.end("get_contribution_contracts") + return list(parsed_contracts.values()) + + def get_contribution_contract_addresses(self): + self.log.perf.start("get_contribution_contracts") + with closing(self.connect()) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute( + """ + SELECT address FROM contribution_contracts + """ + ) + addresses = cursor.fetchall() + self.log.debug("Contract addresses: {}".format(len(addresses))) + self.log.perf.end("get_contribution_contracts") + return [address[0] for address in addresses] + + def get_nodes(self): + self.log.perf.start("get_nodes") + with closing(self.connect()) as connection: + with closing(connection.cursor()) as cursor: + parsed_nodes = {} + + # TODO: investigate using a join or something less messy than two select * queries + cursor.execute("""SELECT * FROM service_nodes_main""") + + for node in cursor.fetchall(): + node_dict = DBNode(*node, contributors=[], events=[]) + parsed_nodes[node_dict.contract_id] = node_dict + + # We want to sort by fetched_block_height in ascending order so later updates overwrite earlier ones + cursor.execute( + """SELECT * FROM service_nodes_staging ORDER BY fetched_block_height ASC""" + ) + + for node in cursor.fetchall(): + node_dict = DBNode(*node, exit_type=None, deregistration_height=None, liquidation_height=None, contributors=[], events=[]) + existing_node = parsed_nodes.get(node_dict.contract_id) + if existing_node is not None: + existing_node.exit_type = node_dict.exit_type + existing_node.deregistration_height = node_dict.deregistration_height + existing_node.liquidation_height = node_dict.liquidation_height + parsed_nodes[node_dict.contract_id] = node_dict + + + cursor.execute("""SELECT * from service_nodes_contributions_main""") + + db_contributions_main = [DBContributionMain(*contribution) for contribution in cursor.fetchall()] + + # We want to sort by fetched_block_height in ascending order so later updates overwrite earlier ones + cursor.execute( + """SELECT * from service_nodes_contributions_staging ORDER BY fetched_block_height ASC""" + ) + + db_contributions_staging = [DBContributionMain(*contribution) for contribution in cursor.fetchall()] + + parsed_contributions = {} + for contribution_dict in db_contributions_main + db_contributions_staging: + # TODO: there has to be a better way to override the old data with new data + key = f"{contribution_dict.contract_id}{contribution_dict.address}" + parsed_contributions[key] = contribution_dict + + for contribution_dict in parsed_contributions.values(): + parsed_nodes[contribution_dict.contract_id].contributors.append( + contribution_dict + ) + + contract_ids = list(parsed_nodes.keys()) + + placeholder= '?' # For SQLite. See DBAPI paramstyle. + placeholders= ', '.join(placeholder for unused in contract_ids) + query= 'SELECT * FROM arbitrum_events WHERE main_arg IN (%s) ORDER BY block DESC' % placeholders + cursor.execute(query, contract_ids) + + for event in cursor.fetchall(): + processed_event = ProcessedEvent(*event) + try: + contract_id = int(processed_event.main_arg) + parsed_nodes[contract_id].events.append(processed_event) + except Exception as e: + self.log.error("Error processing event: {}".format(e)) + continue + + nodes_list = list(parsed_nodes.values()) + + self.log.debug("Parsed nodes: {}".format(len(nodes_list))) + self.log.perf.end("get_nodes") + return list(parsed_nodes.values()) + + def get_rewards_info(self): + self.log.perf.start("get_rewards_info") + with closing(self.connect()) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute("SELECT * FROM rewards_info") + rewards_info = { + eth_format(address_hex): rewards + for address_hex, rewards in cursor.fetchall() + } + self.log.debug("Rewards info: {}".format(len(rewards_info))) + self.log.perf.end("get_rewards_info") + return rewards_info + + def get_smart_contract_abis(self): + self.log.perf.start("get_smart_contract_abis") + with closing(self.connect()) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute( + """ + SELECT * FROM smart_contract_abis + """ + ) + abis = [SmartContractABI(*abi) for abi in cursor.fetchall()] + self.log.debug("Smart contract abis: {}".format(len(abis))) + self.log.perf.end("get_smart_contract_abis") + return abis + + def get_smart_contract_abi(self, name: str): + self.log.perf.start("get_smart_contract_abi") + with closing(self.connect()) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute( + """ + SELECT * FROM smart_contract_abis WHERE name = ? + """, + (name,), + ) + abi = SmartContractABI(*cursor.fetchone()) + self.log.debug("Smart contract abi: {}".format(abi)) + self.log.perf.end("get_smart_contract_abi") + return abi + + def get_smart_contract_names(self) -> list[str]: + self.log.perf.start("get_smart_contract_names") + with closing(self.connect()) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute( + """ + SELECT name FROM smart_contract_abis + """ + ) + names = [name[0] for name in cursor.fetchall()] + self.log.debug("Smart contract names: {}".format(len(names))) + self.log.perf.end("get_smart_contract_names") + return names + + def get_smart_contract_addresses(self): + self.log.perf.start("get_smart_contract_addresses") + with closing(self.connect()) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute( + """ + SELECT address, name FROM smart_contracts + """ + ) + addresses = [ + {"address": address, "name": name} for address, name in cursor.fetchall() + ] + self.log.debug("Smart contract addresses: {}".format(len(addresses))) + self.log.perf.end("get_smart_contract_addresses") + return addresses + + def get_smart_contract_addresses_core(self): + self.log.perf.start("get_smart_contract_addresses_core") + with closing(self.connect()) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute( + """ + SELECT address, name FROM smart_contracts WHERE name IN ('ServiceNodeRewards', 'ServiceNodeContributionFactory', 'ServiceNodeRewards') + """ + ) + addresses = [ + {"address": address, "name": name} for address, name in cursor.fetchall() + ] + self.log.debug("Smart contract addresses: {}".format(len(addresses))) + self.log.perf.end("get_smart_contract_addresses_core") + return addresses + + def get_smart_contract_address(self, name: str): + self.log.perf.start("get_smart_contract_address") + with closing(self.connect()) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute( + """ + SELECT address FROM smart_contracts WHERE name = ? + """, + (name,), + ) + address = cursor.fetchone() + self.log.debug("Smart contract address: {}".format(address)) + self.log.perf.end("get_smart_contract_address") + return address[0] + + def get_arbitrum_events_page(self, args=None): + if args is None: + args = [1000, 0] + self.log.perf.start("get_arbitrum_events") + with closing(self.connect()) as connection: + with closing(connection.cursor()) as cursor: + limit = args[0] if len(args) > 0 else 1000 + skip = args[1] if len(args) > 1 else 0 + + cursor.execute( + """ + SELECT * FROM arbitrum_events ORDER BY block DESC LIMIT ? OFFSET ? + """, + (limit, skip), + ) + events = [ProcessedEvent(*event) for event in cursor.fetchall()] + self.log.debug("Arbitrum events: {}".format(len(events))) + self.log.perf.end("get_arbitrum_events") + + cursor.execute("SELECT COUNT(*) FROM arbitrum_events") + total = cursor.fetchone()[0] + + return events, limit, skip, total + + def get_arbitrum_events_since_timestamp(self, params: [int, list[str] | None]) -> list[ProcessedEvent]: + timestamp = params[0] if len(params) > 0 else None + events_types = params[1] if len(params) > 1 and len(params[1]) > 0 else None + + if timestamp is None or (not isinstance(timestamp, int) and not isinstance(timestamp, float)): + raise ValueError("Invalid timestamp, timestamp must be an integer or float") + + if events_types is not None: + if isinstance(events_types, str): + events_types = [events_types] + elif not isinstance(events_types, list): + raise ValueError("Invalid events_types, events_types must be a list of strings or a string") + + + self.log.perf.start("get_arbitrum_events_since_timestamp") + with closing(self.connect()) as connection: + with closing(connection.cursor()) as cursor: + if events_types is None: + cursor.execute("SELECT * FROM arbitrum_events WHERE timestamp > ? ORDER BY timestamp DESC", (timestamp,)) + else: + placeholder= '?' # For SQLite. See DBAPI paramstyle. + placeholders= ', '.join(placeholder for unused in events_types) + query= 'SELECT * FROM arbitrum_events WHERE timestamp > ? AND name IN (%s) ORDER BY timestamp DESC' % placeholders + cursor.execute(query, (timestamp, *events_types)) + # cursor.execute("SELECT * FROM arbitrum_events WHERE timestamp > ? AND name IN ({}) ORDER BY timestamp DESC".format(",".join(["?"]*len(events_types))), tuple(events_types)+(timestamp,)) + events = [ProcessedEvent(*event) for event in cursor.fetchall()] + self.log.debug("Arbitrum events: {}".format(len(events))) + self.log.perf.end("get_arbitrum_events_since_timestamp") + return events + + def get_arbitrum_info(self): + self.log.perf.start("get_arbitrum_info") + with closing(self.connect()) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute("SELECT * FROM arbitrum_info ORDER BY block DESC LIMIT 1") + info = ArbitrumInfo(*cursor.fetchone()) + + self.log.debug("Arbitrum info: {}".format(info)) + self.log.perf.end("get_arbitrum_info") + return info + + def get_arbitrum_events_for_stake_contrat_id(self, contract_id: int): + self.log.perf.start("get_events_for_stake_contrat_id") + with closing(self.connect()) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute( + """ + SELECT * FROM arbitrum_events WHERE main_arg = ? ORDER BY block DESC + """, + (contract_id,), + ) + events = [ProcessedEvent(*event) for event in cursor.fetchall()] + self.log.debug("Arbitrum events: {}".format(len(events))) + self.log.perf.end("get_events_for_stake_contrat_id") + return events + + def get_service_node_rewards_contract_id_bls_key_map(self): + self.log.perf.start("get_service_node_rewards_contract_id_bls_key_map") + with closing(self.connect()) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute( + """ + SELECT contract_id, pubkey_bls FROM service_node_rewards_contract_id_bls_key_map + """ + ) + contract_id_map = { + pubkey_bls: contract_id + for contract_id, pubkey_bls in cursor.fetchall() + } + self.log.debug("Service node rewards contract id bls key map: {}".format(len(contract_id_map))) + self.log.perf.end("get_service_node_rewards_contract_id_bls_key_map") + return contract_id_map \ No newline at end of file diff --git a/src/db/schema.sql b/src/staking/schema.sql similarity index 100% rename from src/db/schema.sql rename to src/staking/schema.sql diff --git a/src/db/snapshot.py b/src/staking/snapshot.py similarity index 99% rename from src/db/snapshot.py rename to src/staking/snapshot.py index c41bb04..622f2f3 100644 --- a/src/db/snapshot.py +++ b/src/staking/snapshot.py @@ -1,7 +1,7 @@ import os import sqlite3 -from ..log import Log +from src.log import Log class DBSnapshot: diff --git a/src/staking/write.py b/src/staking/write.py new file mode 100644 index 0000000..773ecfa --- /dev/null +++ b/src/staking/write.py @@ -0,0 +1,744 @@ +import json +import sqlite3 +import time +from contextlib import closing +from web3 import Web3 + +from src.db.write import DBWriter +from src.staking.arbitrum import ContributionContractDetails +from src.staking.dataclasses import RewardsInfo, DBNodeExit +from src.log import Log +from src.oxen.rpc import ServiceNode, NetworkInfo +from src.web3client.abi_manager import ABIData +from src.web3client.event_scanner import ProcessedEvent + + +class DBWriterStaking(DBWriter): + def __init__(self, db_path: str, log_level: int, perf: bool = False): + super().__init__(db_path, log_level, perf) + + def write_nodes_to_staging_db( + self, + height: int, + parsed_nodes: list[ServiceNode], + # TODO: type the contributor_stake_map properly + contributions: list[dict[str, int]], + ): + self.log.perf.start("write_to_db") + + with closing(self.connect()) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + + self.log.debug("Inserting {} service nodes".format(len(parsed_nodes))) + self.log.perf.start("write_nodes_to_staging_db -> insert nodes") + + cursor.executemany( + """ + INSERT INTO service_nodes_staging ( + active, + contract_id, + decommission_count, + earned_downtime_blocks, + fetched_block_height, + funded, + is_liquidatable, + is_removable, + last_reward_block_height, + last_uptime_proof, + lokinet_version, + operator_address, + operator_fee, + payable, + pubkey_bls, + pubkey_ed25519, + public_ip, + pulse_votes, + quorumnet_port, + registration_height, + registration_hf_version, + requested_unlock_height, + service_node_pubkey, + service_node_version, + staking_requirement, + state_height, + storage_lmq_port, + storage_port, + storage_server_version, + swarm, + swarm_id, + total_contributed + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + ( + node.get("active"), + node.get("contract_id"), + node.get("decommission_count"), + node.get("earned_downtime_blocks"), + height, + node.get("funded"), + node.get("is_liquidatable"), + node.get("is_removable"), + node.get("last_reward_block_height"), + node.get("last_uptime_proof"), + node.get("lokinet_version"), + node.get("operator_address"), + node.get("operator_fee"), + node.get("payable"), + node.get("pubkey_bls"), + node.get("pubkey_ed25519"), + node.get("public_ip"), + node.get("pulse_votes"), + node.get("quorumnet_port"), + node.get("registration_height"), + node.get("registration_hf_version"), + node.get("requested_unlock_height"), + node.get("service_node_pubkey"), + node.get("service_node_version"), + node.get("staking_requirement"), + node.get("state_height"), + node.get("storage_lmq_port"), + node.get("storage_port"), + node.get("storage_server_version"), + node.get("swarm"), + node.get("swarm_id"), + node.get("total_contributed"), + ) + for node in parsed_nodes + ), + ) + + inserted_nodes_rows = cursor.rowcount + + self.log.perf.end("write_nodes_to_staging_db -> insert nodes") + self.log.debug( + "Inserted {} rows into service_nodes_staging".format(inserted_nodes_rows) + ) + self.log.debug("Inserting {} service node contributions".format(len(contributions))) + self.log.perf.start("write_nodes_to_staging_db -> insert contributions") + + cursor.executemany( + """ + INSERT OR REPLACE INTO service_nodes_contributions_staging ( + address, + amount, + beneficiary, + contract_id, + fetched_block_height + ) + VALUES (?, ?, ?, ?, ?) + """, + ( + ( + contribution["address"], + contribution["amount"], + contribution["beneficiary"], + contribution["contract_id"], + height, + ) + for contribution in contributions + ), + ) + + inserted_contributions_rows = cursor.rowcount + + self.log.perf.end("write_nodes_to_staging_db -> insert contributions") + self.log.debug( + "Inserted {} rows into service_nodes_contributions_staging".format( + inserted_contributions_rows + ) + ) + + connection.commit() + self.log.perf.end("write_to_db") + + def write_nodes_to_main_db(self, immutable_height: int): + """ + Gets all nodes from the staging db at or below the immutable_height and writes them to the main db then remove + those nodes from the staging db. + """ + self.log.perf.start("write_nodes_to_main_db") + with closing(self.connect()) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + self.log.perf.start("write_nodes_to_main_db -> select nodes") + + cursor.execute( + """ + SELECT * FROM service_nodes_staging WHERE fetched_block_height = ? + """, + (immutable_height,), + ) + nodes = cursor.fetchall() + selected_nodes_count = len(nodes) + + self.log.perf.end("write_nodes_to_main_db -> select nodes") + self.log.info("Found {} nodes to write to main db".format(selected_nodes_count)) + + # We only want to continue here if there are any nodes ready to commit. + if selected_nodes_count == 0: + self.log.debug("No nodes ready to commit") + return + + self.log.perf.start("write_nodes_to_main_db -> insert nodes") + + cursor.executemany( + """ + INSERT OR REPLACE INTO service_nodes_main ( + active, + contract_id, + decommission_count, + earned_downtime_blocks, + fetched_block_height, + funded, + is_liquidatable, + is_removable, + last_reward_block_height, + last_uptime_proof, + lokinet_version, + operator_address, + operator_fee, + payable, + pubkey_bls, + pubkey_ed25519, + public_ip, + pulse_votes, + quorumnet_port, + registration_height, + registration_hf_version, + requested_unlock_height, + service_node_pubkey, + service_node_version, + staking_requirement, + state_height, + storage_lmq_port, + storage_port, + storage_server_version, + swarm, + swarm_id, + total_contributed + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + nodes, + ) + + inserted_or_updated_rows = cursor.rowcount + + self.log.perf.end("write_nodes_to_main_db -> insert nodes") + self.log.info("Wrote {} rows to main db".format(inserted_or_updated_rows)) + + if inserted_or_updated_rows != len(nodes): + self.log.error( + "Inserted or updated {} rows, but expected {}".format( + inserted_or_updated_rows, len(nodes) + ) + ) + connection.rollback() + self.log.perf.end("write_nodes_to_main_db") + return + + self.log.perf.start("write_nodes_to_main_db -> select contributions") + + cursor.execute( + """ + SELECT * FROM service_nodes_contributions_staging WHERE fetched_block_height = ? + """, + (immutable_height,), + ) + contributions = cursor.fetchall() + selected_contributions_count = len(contributions) + + self.log.perf.end("write_nodes_to_main_db -> select contributions") + self.log.debug( + "Found {} contributions to write to main db".format( + selected_contributions_count + ) + ) + self.log.perf.start("write_nodes_to_main_db -> insert contributions") + + cursor.executemany( + """ + INSERT OR REPLACE INTO service_nodes_contributions_main (address, amount, beneficiary, contract_id, fetched_block_height) + VALUES (?, ?, ?, ?, ?) + """, + contributions, + ) + + inserted_contributions_rows = cursor.rowcount + + self.log.perf.end("write_nodes_to_main_db -> insert contributions") + self.log.info("Wrote {} rows to main db".format(inserted_contributions_rows)) + + if inserted_contributions_rows != len(contributions): + self.log.error( + "Inserted {} rows, but expected {}".format( + inserted_contributions_rows, len(contributions) + ) + ) + connection.rollback() + return + + self.log.perf.start("write_nodes_to_main_db -> delete nodes") + + cursor.execute( + """ + DELETE FROM service_nodes_staging WHERE fetched_block_height <= ? + """, + (immutable_height,), + ) + + deleted_rows = cursor.rowcount + + self.log.perf.end("write_nodes_to_main_db -> delete nodes") + self.log.info("Deleted {} rows from staging db".format(deleted_rows)) + self.log.perf.start("write_nodes_to_main_db -> delete contributions") + + cursor.execute( + """ + DELETE FROM service_nodes_contributions_staging WHERE fetched_block_height <= ? + """, + (immutable_height,), + ) + + deleted_contributions_rows = cursor.rowcount + + self.log.perf.end("write_nodes_to_main_db -> delete contributions") + self.log.info( + "Deleted {} rows from staging contributions db".format( + deleted_contributions_rows + ) + ) + + connection.commit() + self.log.info("Transaction committed successfully") + + self.log.perf.end("write_nodes_to_main_db") + + def write_exit_list_to_db(self, exit_list: list[DBNodeExit]): + self.log.perf.start("write_exit_list_to_db") + with closing(self.connect()) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + self.log.debug("Updating nodes in main with {} exit events".format(len(exit_list))) + self.log.perf.start("write_exit_list_to_db -> insert exit events") + cursor.executemany( + """ + UPDATE service_nodes_main SET + deregistration_height = ?, + exit_type = ?, + liquidation_height = ? + WHERE pubkey_bls = ? + """, + ( + ( + e.deregistration_height, + e.exit_type, + e.liquidation_height, + e.pubkey_bls, + ) + for e in exit_list + ) + ) + inserted_exit_rows = cursor.rowcount + + self.log.perf.end("write_exit_list_to_db -> insert exit events") + self.log.debug( + "Inserted {} rows into exit events".format(inserted_exit_rows) + ) + + connection.commit() + self.log.perf.end("write_exit_list_to_db") + + def write_network_info_to_db( + self, + network: NetworkInfo, + node_count: int, + active_node_count: int, + ): + self.log.perf.start("write_network_info_to_db") + with closing(self.connect()) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute( + """ + INSERT OR REPLACE INTO network_info ( + id, + active_node_count, + block_hash, + block_height, + block_timestamp, + hard_fork, + immutable_block_hash, + immutable_block_height, + max_stakers, + min_operator_contribution, + node_count, + nettype, + pulse_target_timestamp, + staking_requirement, + version + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + 1, + active_node_count, + network.block_hash, + network.block_height, + time.time().__floor__(), + network.hard_fork, + network.immutable_block_hash, + network.immutable_block_height, + network.max_stakers, + network.min_operator_contribution, + node_count, + network.nettype, + network.pulse_target_timestamp, + network.staking_requirement, + network.version, + ), + ) + connection.commit() + self.log.perf.end("write_network_info_to_db") + + def write_rewards_info_to_db(self, rewards_info: list[RewardsInfo]): + self.log.perf.start("write_rewards_info_to_db") + with closing(self.connect()) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + self.log.debug("Inserting {} rewards info".format(len(rewards_info))) + self.log.perf.start("write_rewards_info_to_db -> insert rewards info") + + cursor.executemany( + """ + INSERT OR REPLACE INTO rewards_info (address, rewards) + VALUES (?, ?) + """, + ( + ( + info.address, + info.rewards, + ) + for info in rewards_info + ), + ) + + inserted_rewards_rows = cursor.rowcount + + self.log.perf.end("write_rewards_info_to_db -> insert rewards info") + self.log.debug( + "Inserted {} rows into rewards_info".format(inserted_rewards_rows) + ) + + connection.commit() + self.log.perf.end("write_rewards_info_to_db") + + def write_arbitrum_events_to_db(self, events: list[ProcessedEvent]): + self.log.perf.start("write_arbitrum_events_to_db") + + with closing(self.connect()) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + + self.log.debug("Inserting {} events into arbitrum_events".format(len(events))) + self.log.perf.start("write_arbitrum_events_to_db -> insert events") + + cursor.executemany( + """ + INSERT OR REPLACE INTO arbitrum_events ( + block, + timestamp, + tx, + name, + main_arg, + args + ) + VALUES (?, ?, ?, ?, ?, ?) + """, + ( + ( + event.block, + event.timestamp, + "0x" + event.tx, + event.name, + event.main_arg, + Web3.to_json(dict(event.args)), + ) + for event in events + ), + ) + + inserted_or_updated_rows_count = cursor.rowcount + + self.log.perf.end("write_arbitrum_events_to_db -> insert events") + self.log.debug( + "Inserted or updated {} rows into arbitrum_events".format( + inserted_or_updated_rows_count + ) + ) + + connection.commit() + self.log.perf.end("write_arbitrum_events_to_db") + + def write_contribution_contracts_to_db( + self, contracts: list[ContributionContractDetails], contributions_list: list, add_event_timestamps: dict[str, int], node_last_added_timestamps: dict[str,int], create_contract_timestamps: dict[str, int] + ): + self.log.perf.start("write_contribution_contracts_to_db") + + with closing(self.connect()) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + + self.log.debug("Inserting {} contribution contracts".format(len(contracts))) + self.log.perf.start("write_contribution_contracts_to_db -> insert contracts") + + cursor.executemany( + """ + INSERT OR REPLACE INTO contribution_contracts ( + address, + created_timestamp, + fee, + last_added_timestamp, + manual_finalize, + node_add_timestamp, + operator_address, + pubkey_bls, + service_node_pubkey, + service_node_signature, + status + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + ( + contract.address, + create_contract_timestamps.get(contract.address), + contract.fee, + node_last_added_timestamps.get(contract.pubkey_bls), + contract.manual_finalize, + add_event_timestamps.get(contract.pubkey_bls), + contract.operator_address, + contract.pubkey_bls, + contract.service_node_pubkey, + contract.service_node_signature, + contract.status, + ) + for contract in contracts + ), + ) + + inserted_contract_rows = cursor.rowcount + + self.log.perf.end("write_contribution_contracts_to_db -> insert contracts") + self.log.debug( + "Inserted or Updated {} rows into contribution_contracts".format( + inserted_contract_rows + ) + ) + self.log.perf.start("write_contribution_contracts_to_db -> delete contributions") + + # The contributors for a contact need to be deleted before the contract can be inserted again to account + # for contract resets, or contributors leaving the contract. We could read from the db and only delete + # the missing ones but this should be more performant. + # TODO: investigate a better solution + cursor.executemany( + """DELETE FROM contribution_contracts_contributions WHERE contract_address = ?""", + (( + contract.address, + ) + for contract in contracts) + ) + + deleted_contributions_rows = cursor.rowcount + + self.log.perf.end("write_contribution_contracts_to_db -> delete contributions") + self.log.debug( + "Deleted {} rows from contribution_contracts_contributions".format( + deleted_contributions_rows + ) + ) + self.log.debug( + "Inserting {} contract contributions".format(len(contributions_list)) + ) + self.log.perf.start( + "write_contribution_contracts_to_db -> insert contribution contracts contributions" + ) + + cursor.executemany( + """ + INSERT INTO contribution_contracts_contributions ( + address, + amount, + beneficiary_address, + contract_address, + reserved + ) + VALUES (?, ?, ?, ?, ?) + """, + ( + ( + contribution["address"], + contribution["amount"], + contribution["beneficiary_address"], + contribution["contract_address"], + contribution["reserved"], + ) + for contribution in contributions_list + ), + ) + + inserted_contributions_rows = cursor.rowcount + + self.log.perf.end( + "write_contribution_contracts_to_db -> insert contribution contracts contributions" + ) + self.log.debug( + "Inserted {} rows into contribution_contracts_contributions".format( + inserted_contributions_rows + ) + ) + + connection.commit() + self.log.perf.end("write_contribution_contracts_to_db") + + def write_smart_contract_abis_to_db(self, abis: list[ABIData]): + self.log.perf.start("write_smart_contract_abis_to_db") + with closing(self.connect()) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + + self.log.debug("Inserting {} smart contract abis".format(len(abis))) + self.log.perf.start("write_smart_contract_abis_to_db -> insert abis") + + cursor.executemany( + """ + INSERT OR REPLACE INTO smart_contract_abis ( + name, + abi, + bytecode, + deployed_bytecode + ) + VALUES (?, ?, ?, ?) + """, + ( + ( + abi.name, + json.dumps(abi.abi), + abi.bytecode, + abi.deployed_bytecode, + ) + for abi in abis + ), + ) + + inserted_abi_rows = cursor.rowcount + + self.log.perf.end("write_smart_contract_abis_to_db -> insert abis") + self.log.debug( + "Inserted {} rows into smart_contract_abis".format(inserted_abi_rows) + ) + + connection.commit() + self.log.perf.end("write_smart_contract_abis_to_db") + + def write_smart_contract_details_to_db( + self, + contracts, + ): + self.log.perf.start("write_smart_contract_details_to_db") + with closing(self.connect()) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + + self.log.debug("Inserting {} smart contract details".format(len(contracts))) + self.log.perf.start("write_smart_contract_details_to_db -> insert contracts") + + cursor.executemany( + """ + INSERT OR REPLACE INTO smart_contracts ( + address, + name + ) + VALUES (?, ?) + """, + ( + ( + contract.get("address"), + contract.get("name"), + ) + for contract in contracts + ), + ) + + inserted_details_rows = cursor.rowcount + + self.log.perf.end("write_smart_contract_details_to_db -> insert details") + self.log.debug( + "Inserted {} rows into smart_contract_details".format(inserted_details_rows) + ) + + connection.commit() + self.log.perf.end("write_smart_contract_details_to_db") + + def write_arbitrum_info_to_db(self, current_block, service_node_rewards_balance, reward_rate_pool_balance): + self.log.perf.start("write_arbitrum_info_to_db") + with closing(self.connect()) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + self.log.debug( + "Inserting arbitrum info: current block {}, service node rewards balance {}, reward rate pool balance {}".format( + current_block, service_node_rewards_balance, reward_rate_pool_balance)) + self.log.perf.start("write_arbitrum_info_to_db -> insert info") + + cursor.execute("INSERT OR REPLACE INTO arbitrum_info (block, balance_service_node_rewards, balance_reward_rate_pool) VALUES (?, ?, ?)", (current_block, service_node_rewards_balance, reward_rate_pool_balance)) + + inserted_info_rows = cursor.rowcount + + self.log.perf.end("write_arbitrum_info_to_db -> insert info") + self.log.debug( + "Inserted {} rows into arbitrum_info".format(inserted_info_rows) + ) + + connection.commit() + self.log.perf.end("write_arbitrum_info_to_db") + + def write_service_node_rewards_contract_id_bls_key_map(self, contract_id_map: dict[str, str]): + self.log.perf.start("write_service_node_rewards_contract_id_bls_key_map") + with closing(self.connect()) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + self.log.debug("Inserting {} service node rewards contract ids".format(len(contract_id_map))) + self.log.perf.start("write_service_node_rewards_contract_id_bls_key_map -> insert contract ids") + + cursor.execute("DELETE FROM service_node_rewards_contract_id_bls_key_map") + + cursor.executemany( + """ + INSERT INTO service_node_rewards_contract_id_bls_key_map ( + contract_id, + pubkey_bls + ) + VALUES (?, ?) + """, + ( + ( + int(contract_id), + pubkey_bls, + ) + for pubkey_bls, contract_id in contract_id_map.items() + ), + ) + + inserted_contract_id_rows = cursor.rowcount + + self.log.perf.end("write_service_node_rewards_contract_id_bls_key_map -> insert contract ids") + self.log.debug( + "Inserted {} rows into service_node_rewards_contract_id_bls_key_map".format( + inserted_contract_id_rows + ) + ) + + connection.commit() + self.log.perf.end("write_service_node_rewards_contract_id_bls_key_map") \ No newline at end of file From ae65c662baee6ba15cae0b62478211d1eac6f5d0 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Thu, 30 Jan 2025 14:13:39 +1100 Subject: [PATCH 081/138] fix: use relative imports for src --- src/staking/app.py | 4 ++-- src/staking/dataclasses.py | 4 ++-- src/staking/read.py | 8 ++++---- src/staking/snapshot.py | 2 +- src/staking/write.py | 14 +++++++------- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/staking/app.py b/src/staking/app.py index 2a21db3..0b89d1b 100644 --- a/src/staking/app.py +++ b/src/staking/app.py @@ -7,8 +7,8 @@ from uwsgidecorators import timer from werkzeug.exceptions import GatewayTimeout -from src.staking.dataclasses import ArbitrumInfo -from src.staking.read import DBReaderStaking +from ..staking.dataclasses import ArbitrumInfo +from ..staking.read import DBReaderStaking from ..oxen.rpc import OxenRPC from ..registration.read import DBReaderRegistrations from ..util.flask_utils import FlaskApp, json_response diff --git a/src/staking/dataclasses.py b/src/staking/dataclasses.py index a783df7..930d3bf 100644 --- a/src/staking/dataclasses.py +++ b/src/staking/dataclasses.py @@ -2,8 +2,8 @@ from dataclasses import dataclass from typing import Optional -from src.util.parse import eth_format -from src.web3client.event_scanner import ProcessedEvent +from ..util.parse import eth_format +from ..web3client.event_scanner import ProcessedEvent @dataclass diff --git a/src/staking/read.py b/src/staking/read.py index a851fbc..0a30672 100644 --- a/src/staking/read.py +++ b/src/staking/read.py @@ -1,11 +1,11 @@ import sqlite3 from contextlib import closing -from src.db.read import DBReader -from src.staking.dataclasses import DBNode, DBContributionMain, DBNetworkInfo, DBContributionContract, \ +from ..db.read import DBReader +from ..staking.dataclasses import DBNode, DBContributionMain, DBNetworkInfo, DBContributionContract, \ DBContributionContractContribution, SmartContractABI, ArbitrumInfo -from src.util.parse import eth_format -from src.web3client.event_scanner import ProcessedEvent +from ..util.parse import eth_format +from ..web3client.event_scanner import ProcessedEvent class DBReaderStaking(DBReader): diff --git a/src/staking/snapshot.py b/src/staking/snapshot.py index 622f2f3..c41bb04 100644 --- a/src/staking/snapshot.py +++ b/src/staking/snapshot.py @@ -1,7 +1,7 @@ import os import sqlite3 -from src.log import Log +from ..log import Log class DBSnapshot: diff --git a/src/staking/write.py b/src/staking/write.py index 773ecfa..013a396 100644 --- a/src/staking/write.py +++ b/src/staking/write.py @@ -4,13 +4,13 @@ from contextlib import closing from web3 import Web3 -from src.db.write import DBWriter -from src.staking.arbitrum import ContributionContractDetails -from src.staking.dataclasses import RewardsInfo, DBNodeExit -from src.log import Log -from src.oxen.rpc import ServiceNode, NetworkInfo -from src.web3client.abi_manager import ABIData -from src.web3client.event_scanner import ProcessedEvent +from ..db.write import DBWriter +from ..staking.arbitrum import ContributionContractDetails +from ..staking.dataclasses import RewardsInfo, DBNodeExit +from ..log import Log +from ..oxen.rpc import ServiceNode, NetworkInfo +from ..web3client.abi_manager import ABIData +from ..web3client.event_scanner import ProcessedEvent class DBWriterStaking(DBWriter): From 97361cba7de21ead7ab618f10354c7d4f4308a21 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Thu, 30 Jan 2025 14:17:07 +1100 Subject: [PATCH 082/138] fix: change packages to use relative imports to themselves --- src/app_price.py | 4 ++-- src/config.py | 2 +- src/db/read.py | 2 +- src/db/write.py | 2 +- src/fetcher.py | 6 +++--- src/price/app.py | 2 +- src/registration/read.py | 2 +- src/snapshot/app.py | 2 +- src/staking/app.py | 4 ++-- src/staking/read.py | 2 +- 10 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/app_price.py b/src/app_price.py index 2699eb9..0a7f7e1 100644 --- a/src/app_price.py +++ b/src/app_price.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 -import src.config as config -from src.price.app import create_app, PriceAppConfig +import ..config as config +from ..price.app import create_app, PriceAppConfig price_config = PriceAppConfig( name="price_api", diff --git a/src/config.py b/src/config.py index 705a6fd..c2e75ed 100644 --- a/src/config.py +++ b/src/config.py @@ -1,4 +1,4 @@ -from src.config_defaults import * +from .config_defaults import * # Local settings. Changes to this file are meant for a local installation (and should not be # committed to git). diff --git a/src/db/read.py b/src/db/read.py index 85cae32..b5c4b57 100644 --- a/src/db/read.py +++ b/src/db/read.py @@ -1,6 +1,6 @@ import sqlite3 -from src.log import Log +from ..log import Log class DBReader: def __init__(self, db_path: str, log_level: int, perf: bool = False): diff --git a/src/db/write.py b/src/db/write.py index 1d7c4be..9606439 100644 --- a/src/db/write.py +++ b/src/db/write.py @@ -1,6 +1,6 @@ import sqlite3 -from src.log import Log +from ..log import Log class DBWriter: def __init__(self, db_path: str, log_level: int, perf: bool = False): diff --git a/src/fetcher.py b/src/fetcher.py index fa43086..7b67323 100644 --- a/src/fetcher.py +++ b/src/fetcher.py @@ -10,14 +10,14 @@ update_contribution_contract_details, batch_populate_events_with_block_timestamps, populate_events_with_main_arg, ) from config_validate import validate_config -from src.staking.dataclasses import RewardsInfo, DBNodeExit +from .staking.dataclasses import RewardsInfo, DBNodeExit from db.util import ( assert_all_dict_values_are_within_sqlite_integer_range, is_db_initialized, init_db, ) -from src.staking.read import DBReaderStaking -from src.staking.write import DBWriterStaking +from .staking.read import DBReaderStaking +from .staking.write import DBWriterStaking from log import Log from oxen.rpc import ServiceNode, OxenRPC, NetworkInfo from util import format_seconds diff --git a/src/price/app.py b/src/price/app.py index fdb38d8..6876d95 100644 --- a/src/price/app.py +++ b/src/price/app.py @@ -2,7 +2,7 @@ from dataclasses import dataclass from uwsgidecorators import timer -from src.util.flask_utils import FlaskApp, json_response, FlaskAppConfig +from ..util.flask_utils import FlaskApp, json_response, FlaskAppConfig from .coingecko import CoinGeckoTokenPriceRequest from .read import DBReaderPrices from .dataclasses import PriceDB diff --git a/src/registration/read.py b/src/registration/read.py index f3a629e..6740802 100644 --- a/src/registration/read.py +++ b/src/registration/read.py @@ -1,7 +1,7 @@ import sqlite3 from contextlib import closing -from src.staking.dataclasses import Registration +from ..staking.dataclasses import Registration from ..log import Log class DBReaderRegistrations: diff --git a/src/snapshot/app.py b/src/snapshot/app.py index 3b0ccb5..059e31c 100644 --- a/src/snapshot/app.py +++ b/src/snapshot/app.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 from uwsgidecorators import timer -from src.staking.snapshot import DBSnapshot +from ..staking.snapshot import DBSnapshot from ..util.flask_utils import FlaskApp diff --git a/src/staking/app.py b/src/staking/app.py index 0b89d1b..7267945 100644 --- a/src/staking/app.py +++ b/src/staking/app.py @@ -7,8 +7,8 @@ from uwsgidecorators import timer from werkzeug.exceptions import GatewayTimeout -from ..staking.dataclasses import ArbitrumInfo -from ..staking.read import DBReaderStaking +from .dataclasses import ArbitrumInfo +from .read import DBReaderStaking from ..oxen.rpc import OxenRPC from ..registration.read import DBReaderRegistrations from ..util.flask_utils import FlaskApp, json_response diff --git a/src/staking/read.py b/src/staking/read.py index 0a30672..48c1d72 100644 --- a/src/staking/read.py +++ b/src/staking/read.py @@ -2,7 +2,7 @@ from contextlib import closing from ..db.read import DBReader -from ..staking.dataclasses import DBNode, DBContributionMain, DBNetworkInfo, DBContributionContract, \ +from .dataclasses import DBNode, DBContributionMain, DBNetworkInfo, DBContributionContract, \ DBContributionContractContribution, SmartContractABI, ArbitrumInfo from ..util.parse import eth_format from ..web3client.event_scanner import ProcessedEvent From 022a210803c765e7c5ad47a219522f7d9a72ab02 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Thu, 30 Jan 2025 16:25:16 +1100 Subject: [PATCH 083/138] fix: only require uwsgi for price fetcher if timer is active --- src/config_defaults.py | 1 + src/price/app.py | 41 +++++++++++++++++++++++------------------ 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/config_defaults.py b/src/config_defaults.py index 5ce437f..9d5924e 100644 --- a/src/config_defaults.py +++ b/src/config_defaults.py @@ -87,6 +87,7 @@ class Backend: PRICE FETCHER CONFIG """ prices_api_name: str = "prices_api" + enable_price_fetcher: bool = False coingecko_api_key: str = "" coingecko_api_url: str = "https://api.coingecko.com/api" coingecko_api_token_ids: list[str] = ["ethereum", "chainflip"] diff --git a/src/price/app.py b/src/price/app.py index 6876d95..1049a90 100644 --- a/src/price/app.py +++ b/src/price/app.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 from dataclasses import dataclass -from uwsgidecorators import timer from ..util.flask_utils import FlaskApp, json_response, FlaskAppConfig from .coingecko import CoinGeckoTokenPriceRequest @@ -12,6 +11,9 @@ @dataclass class PriceAppConfig(FlaskAppConfig): + + enable_price_fetcher: bool = False + # Flask App Config sqlite_db: str = None sqlite_schema: str = None @@ -31,15 +33,19 @@ def __init__(self, config: PriceAppConfig): super().__init__(config) self.app_config = config - if not is_db_initialized(config.sqlite_db) and config.coingecko_api_url: - self.log.info( - "Initializing database {} with schema {}".format( - config.sqlite_db, config.sqlite_schema - ) - ) - init_db( - config.sqlite_db, config.sqlite_schema + if config.enable_price_fetcher: + self.log.info(f"Price fetcher enabled, fetching from {config.coingecko_api_url}") + if is_db_initialized(config.sqlite_db): + self.log.info(f"Initializing database {config.sqlite_db} with schema {config.sqlite_schema}") + init_db(config.sqlite_db, config.sqlite_schema) + self.db_writer_prices = DBWriterPrices( + db_path=config.sqlite_db, + log_level=config.log_level, + perf=config.enable_perf, ) + else: + self.log.info("Price fetcher disabled. No API url provided.") + self.db_reader_prices = DBReaderPrices( db_path=config.sqlite_db, @@ -47,14 +53,7 @@ def __init__(self, config: PriceAppConfig): perf=config.enable_perf, ) - if config.coingecko_api_url: - self.db_writer_prices = DBWriterPrices( - db_path=config.sqlite_db, - log_level=config.log_level, - perf=config.enable_perf, - ) - - if config.coingecko_api_url: + if config.enable_price_fetcher: self.token_price_request = CoinGeckoTokenPriceRequest( logger=self.log, key=config.coingecko_api_key, @@ -142,8 +141,14 @@ def route_get_token_price_for_token(token: str): "price": app.get_token_price_info(token) }) - if app.price_poll_rate_seconds > 0 and config.coingecko_api_url: + if config.enable_price_fetcher: app.log.info("Polling for price info every {} seconds".format(app.price_poll_rate_seconds)) + try: + from uwsgidecorators import timer + except ModuleNotFoundError as e: + if e.name == "uwsgi": + app.log.error("uwsgi is not installed, run with uwsgi or disable price polling") + raise e @timer(app.price_poll_rate_seconds) def fetch_token_price_info(signum): From 57bebdddcd7c700925661ec2b76df90f2a3c8ee7 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Thu, 30 Jan 2025 16:35:17 +1100 Subject: [PATCH 084/138] fix: leading slash on db file names --- src/db/read.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/db/read.py b/src/db/read.py index b5c4b57..08898f8 100644 --- a/src/db/read.py +++ b/src/db/read.py @@ -7,7 +7,10 @@ def __init__(self, db_path: str, log_level: int, perf: bool = False): self.log = Log("db_reader", log_level, enable_perf=perf).logger if not db_path.startswith("file://"): - db_path = "file://" + db_path + if db_path.startswith("/"): + db_path = "file:/" + db_path + else: + db_path = "file://" + db_path if not db_path.endswith("?mode=ro"): db_path = db_path + "?mode=ro" From 6f2b566a63f4d6f34c0265ce5d58837021419835 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Thu, 30 Jan 2025 16:44:46 +1100 Subject: [PATCH 085/138] fix: add disable db file rewrite option --- src/config_defaults.py | 2 ++ src/db/read.py | 17 +++++++++-------- src/price/app.py | 2 ++ src/price/read.py | 4 ++-- src/staking/app.py | 1 + src/staking/read.py | 4 ++-- 6 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/config_defaults.py b/src/config_defaults.py index 9d5924e..9bb70c3 100644 --- a/src/config_defaults.py +++ b/src/config_defaults.py @@ -27,6 +27,8 @@ class Backend: sqlite_schema: str = "db/schema.sql" rpc_shared: list[str] = "" rpc_shared_cache: int = 2 + disable_db_file_rewrite: bool = False + """ API CONFIG diff --git a/src/db/read.py b/src/db/read.py index 08898f8..dac083c 100644 --- a/src/db/read.py +++ b/src/db/read.py @@ -3,17 +3,18 @@ from ..log import Log class DBReader: - def __init__(self, db_path: str, log_level: int, perf: bool = False): + def __init__(self, db_path: str, log_level: int, perf: bool = False, disable_db_file_rewrite: bool = False): self.log = Log("db_reader", log_level, enable_perf=perf).logger - if not db_path.startswith("file://"): - if db_path.startswith("/"): - db_path = "file:/" + db_path - else: - db_path = "file://" + db_path + if not disable_db_file_rewrite: + if not db_path.startswith("file://"): + if db_path.startswith("/"): + db_path = "file:/" + db_path + else: + db_path = "file://" + db_path - if not db_path.endswith("?mode=ro"): - db_path = db_path + "?mode=ro" + if not db_path.endswith("?mode=ro"): + db_path = db_path + "?mode=ro" self.log.info(f"Connecting to db at {db_path}") self.db_path = db_path diff --git a/src/price/app.py b/src/price/app.py index 1049a90..cdb6622 100644 --- a/src/price/app.py +++ b/src/price/app.py @@ -15,6 +15,7 @@ class PriceAppConfig(FlaskAppConfig): enable_price_fetcher: bool = False # Flask App Config + disable_db_file_rewrite: bool = False sqlite_db: str = None sqlite_schema: str = None coingecko_api_key: str = None @@ -51,6 +52,7 @@ def __init__(self, config: PriceAppConfig): db_path=config.sqlite_db, log_level=config.log_level, perf=config.enable_perf, + disable_db_file_rewrite=config.disable_db_file_rewrite, ) if config.enable_price_fetcher: diff --git a/src/price/read.py b/src/price/read.py index 8c48c0d..a395534 100644 --- a/src/price/read.py +++ b/src/price/read.py @@ -5,8 +5,8 @@ class DBReaderPrices(DBReader): - def __init__(self, db_path: str, log_level: int, perf: bool = False): - super().__init__(db_path, log_level, perf) + def __init__(self, db_path: str, log_level: int, perf: bool = False, disable_db_file_rewrite: bool = False): + super().__init__(db_path, log_level, perf, disable_db_file_rewrite) def get_latest_price(self, token: str, currency: str): self.log.perf.start("get_latest_price") diff --git a/src/staking/app.py b/src/staking/app.py index 7267945..afa616b 100644 --- a/src/staking/app.py +++ b/src/staking/app.py @@ -31,6 +31,7 @@ def __init__(self, config): db_path=config.backend.registration_sqlite_db, log_level=config.backend.log_level, perf=config.backend.performance_logging, + disable_db_file_rewrite=config.backend.disable_db_file_rewrite, ) rpc_url = config.backend.rpc_api if config.backend.rpc_api else config.backend.rpc_shared diff --git a/src/staking/read.py b/src/staking/read.py index 48c1d72..afd349c 100644 --- a/src/staking/read.py +++ b/src/staking/read.py @@ -9,8 +9,8 @@ class DBReaderStaking(DBReader): - def __init__(self, db_path: str, log_level: int, perf: bool = False): - super().__init__(db_path, log_level, perf) + def __init__(self, db_path: str, log_level: int, perf: bool = False, disable_db_file_rewrite: bool = False): + super().__init__(db_path, log_level, perf, disable_db_file_rewrite) def get_last_fetched_network_block_height(self) -> int: self.log.perf.start("get_last_fetched_network_block_height") From 8a13b8d641d9007065054f940142d59e789c8b9d Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Thu, 30 Jan 2025 17:21:57 +1100 Subject: [PATCH 086/138] fix: update most recent price fetcher --- src/price/app.py | 10 +++++++--- src/price/read.py | 7 ++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/price/app.py b/src/price/app.py index cdb6622..57097bd 100644 --- a/src/price/app.py +++ b/src/price/app.py @@ -68,6 +68,7 @@ def __init__(self, config: PriceAppConfig): self.price_poll_rate_seconds = config.coingecko_api_rate_poll_rate_seconds if config.coingecko_api_rate_poll_rate_seconds is not None else 0 + @staticmethod def get_token_price_cache_key(token: str): return f"price-{token}-all" @@ -75,21 +76,24 @@ def get_token_price_cache_key(token: str): def get_token_info_cached(self, token: str): key = App.get_token_price_cache_key(token) - data: dict[str, PriceDB] | None = self.cache.get_cached_only(key) + data: list[PriceDB] | None = self.cache.get_cached_only(key) if data: return data data = self.db_reader_prices.get_latest_prices(token) - updated_at = max(price.updated_at for price in data.values()) + updated_at = data[0].updated_at stale_time = updated_at + self.price_poll_rate_seconds self.cache.set_cache_value(key, data, invalidate_timestamp=stale_time) return data def get_price_for_token_uncached(self, params: [str, str]): - return self.get_token_info_cached(params[0]).get(params[1]) + for price in self.get_token_info_cached(params[0]): + if price.currency == params[1]: + return price + return None def get_price_for_token_cached(self, token: str, currency: str) -> PriceDB | None: return self.cache.get(f"price-{token}-{currency}", getter=self.get_price_for_token_uncached, diff --git a/src/price/read.py b/src/price/read.py index a395534..53ff460 100644 --- a/src/price/read.py +++ b/src/price/read.py @@ -29,15 +29,12 @@ def get_latest_prices(self, token: str): with closing(connection.cursor()) as cursor: cursor.execute( """ - SELECT * FROM prices WHERE token = ? ORDER BY updated_at DESC + SELECT * FROM prices WHERE token = ? ORDER BY updated_at DESC LIMIT 100 """, (token,), ) - prices_lst = [PriceDB(*price) for price in cursor.fetchall()] - prices = { - price.currency: price for price in prices_lst - } + prices = [PriceDB(*price) for price in cursor.fetchall()] self.log.debug("Prices: {}".format(len(prices))) self.log.perf.end("get_latest_prices") From b2d3018b57ff1c3addd610329bcc6c3709aff0a7 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 31 Jan 2025 10:28:58 +1100 Subject: [PATCH 087/138] fix: cache getters on no item --- src/util/cache.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/util/cache.py b/src/util/cache.py index c87b3c1..72bd9e8 100644 --- a/src/util/cache.py +++ b/src/util/cache.py @@ -17,20 +17,20 @@ def get(self, key, getter=Optional[Callable], getter_args=None, ttl=None, invali if ttl is None or ttl < 0: ttl = self.default_stale_time_seconds now = time.time() - if key in self.store and self.cache_expiry[key] > now: - return self.store[key] + + if key in self.store and self.cache_expiry.get(key, 0) > now: + return self.store.get(key) self.clear_stale(now) data = getter(getter_args) if getter_args is not None else getter() - self.store[key] = data - self.cache_expiry[key] = min(now + ttl, invalidate_timestamp) if invalidate_timestamp is not None else now + ttl + self.set_cache_value(key, data, ttl, invalidate_timestamp) return data def get_cached_only(self, key: str): now = time.time() - if key in self.store and self.cache_expiry[key] > now: - return self.store[key] + if key in self.store and self.cache_expiry.get(key, 0) > now: + return self.store.get(key) return None def set_cache_value(self, key: str, data=None, ttl=None, invalidate_timestamp=None): @@ -39,7 +39,8 @@ def set_cache_value(self, key: str, data=None, ttl=None, invalidate_timestamp=No now = time.time() self.store[key] = data - self.cache_expiry[key] = min(now + ttl, invalidate_timestamp) if invalidate_timestamp is not None else now + ttl + expire = min(now + ttl, invalidate_timestamp) if invalidate_timestamp is not None else now + ttl + self.set_expiry_timestamp(key, expire) def set_expiry_ttl(self, key: str, ttl: int): self.cache_expiry[key] = time.time() + ttl @@ -48,7 +49,7 @@ def set_expiry_timestamp(self, key: str, timestamp: int): self.cache_expiry[key] = timestamp def get_stale_timestamp(self, key: str): - return self.cache_expiry[key] + return self.cache_expiry.get(key, 0) def clear_stale(self, now): # NOTE: must be a copy as the dictionary is modified during iteration From 20410e90f3e136f73a76cb625f78df88964907ef Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 31 Jan 2025 10:46:24 +1100 Subject: [PATCH 088/138] fix: cache ttl and invalidation defaults --- src/util/cache.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/util/cache.py b/src/util/cache.py index 72bd9e8..de149cf 100644 --- a/src/util/cache.py +++ b/src/util/cache.py @@ -34,12 +34,15 @@ def get_cached_only(self, key: str): return None def set_cache_value(self, key: str, data=None, ttl=None, invalidate_timestamp=None): - if ttl is None or ttl < 0: - ttl = self.default_stale_time_seconds - now = time.time() + if invalidate_timestamp is None: + if ttl is None or ttl < 0: + ttl = self.default_stale_time_seconds + expire = now + ttl + else: + expire = invalidate_timestamp + self.store[key] = data - expire = min(now + ttl, invalidate_timestamp) if invalidate_timestamp is not None else now + ttl self.set_expiry_timestamp(key, expire) def set_expiry_ttl(self, key: str, ttl: int): From 53ad6f02a07a26a0507b79873da72206531c8542 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 31 Jan 2025 11:12:29 +1100 Subject: [PATCH 089/138] fix: db read uri --- src/db/read.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/db/read.py b/src/db/read.py index dac083c..f9c5a24 100644 --- a/src/db/read.py +++ b/src/db/read.py @@ -7,11 +7,8 @@ def __init__(self, db_path: str, log_level: int, perf: bool = False, disable_db_ self.log = Log("db_reader", log_level, enable_perf=perf).logger if not disable_db_file_rewrite: - if not db_path.startswith("file://"): - if db_path.startswith("/"): - db_path = "file:/" + db_path - else: - db_path = "file://" + db_path + if not db_path.startswith("file:"): + db_path = "file:" + db_path if not db_path.endswith("?mode=ro"): db_path = db_path + "?mode=ro" From 06d7d877716a1fcc7b47d955eb0392244144c1e0 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 31 Jan 2025 11:57:10 +1100 Subject: [PATCH 090/138] fix: change server timestamp to int --- src/util/flask_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/flask_utils.py b/src/util/flask_utils.py index 259991b..be87177 100644 --- a/src/util/flask_utils.py +++ b/src/util/flask_utils.py @@ -25,7 +25,7 @@ def json_response(vals=None, vals_no_hexify=None): if vals_no_hexify is None: vals_no_hexify = {} - return flask.jsonify({**vals, **vals_no_hexify, "t": time.time()}) + return flask.jsonify({**vals, **vals_no_hexify, "t": int(time.time())}) @dataclass From a7b0e76e38547782a1a836348e8a315e3c46fa8e Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 3 Feb 2025 11:16:17 +1100 Subject: [PATCH 091/138] fix: raw transaction old version support --- src/web3client/client.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/web3client/client.py b/src/web3client/client.py index 9ef4787..d19b8d8 100644 --- a/src/web3client/client.py +++ b/src/web3client/client.py @@ -64,8 +64,18 @@ def contract_write(self, contract_function: ContractFunction, args: tuple): call_function, private_key=self.private_key ) + raw_transaction = None + try: + raw_transaction = signed_tx.raw_transaction + except Exception as e: + raw_transaction = signed_tx.rawTransaction + logging.warning("raw_transaction is deprecated, using rawTransaction instead") + + if raw_transaction is None: + raise ValueError("raw_transaction is None") + # Send transaction - send_tx = self.web3.eth.send_raw_transaction(signed_tx.rawTransaction) + send_tx = self.web3.eth.send_raw_transaction(raw_transaction) # Wait for transaction receipt tx_hash = self.web3.eth.wait_for_transaction_receipt(send_tx).get("transactionHash") From 0d9ffd082309026f39b591ca4e7060e2ffbe796a Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 3 Feb 2025 11:44:27 +1100 Subject: [PATCH 092/138] feat: create cache tests --- src/util/cache_tests.py | 56 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/util/cache_tests.py diff --git a/src/util/cache_tests.py b/src/util/cache_tests.py new file mode 100644 index 0000000..bca7c08 --- /dev/null +++ b/src/util/cache_tests.py @@ -0,0 +1,56 @@ +import time + +import pytest + +default_stale_time = 2 + + +@pytest.fixture() +def cache(): + from .cache import Cache + return Cache(stale_time_seconds=default_stale_time) + + +def test_cache_init(cache): + assert cache.default_stale_time_seconds == default_stale_time + assert cache.store == {} + assert cache.cache_expiry == {} + + +def test_cache_set(cache): + cache.set_cache_value("test", "potato") + assert cache.store == {"test": "potato"} + + cache_expiry = cache.cache_expiry + assert int(cache_expiry.get("test")) == int(time.time() + default_stale_time) + + +def test_cache_clear_stale(cache): + cache.set_cache_value("test", "potato") + cache.clear_stale(time.time() + 2) + assert cache.store == {} + assert cache.cache_expiry == {} + +def test_cache_normal(cache): + def getter(): + return "potato" + + val = cache.get("test", getter) + assert val == "potato" + +def test_cache_stale_timestamp_expired(cache): + cache.get("test", getter=lambda: "potato") + assert int(cache.cache_expiry.get("test")) == int(time.time() + default_stale_time) + assert int(cache.get_stale_timestamp("test")) == int(time.time() + default_stale_time) + +def test_cache_large_invalidate(cache): + def getter(): + return "potato" + + invalidate_timestamp = 99999999999999999999 + + cache.set_cache_value("test", "potato", invalidate_timestamp=invalidate_timestamp) + val = cache.get("test", getter) + assert val == "potato" + + assert cache.cache_expiry.get("test") == invalidate_timestamp From 19653f1cef425150219ebaf210f423ac087ee2de Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Tue, 4 Feb 2025 11:36:27 +1100 Subject: [PATCH 093/138] fix: staking api config --- src/app_price.py | 4 +-- src/app_staking.py | 25 +++++++++++++++--- src/staking/app.py | 65 ++++++++++++++++++++++++++++------------------ 3 files changed, 64 insertions(+), 30 deletions(-) diff --git a/src/app_price.py b/src/app_price.py index 0a7f7e1..0b885de 100644 --- a/src/app_price.py +++ b/src/app_price.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 -import ..config as config -from ..price.app import create_app, PriceAppConfig +from src import config +from src.price.app import create_app, PriceAppConfig price_config = PriceAppConfig( name="price_api", diff --git a/src/app_staking.py b/src/app_staking.py index 4fd2d13..13dac81 100644 --- a/src/app_staking.py +++ b/src/app_staking.py @@ -1,6 +1,25 @@ #!/usr/bin/env python3 +from src import config +from src.staking.app import create_app, StakingAppConfig -import config -from staking.app import create_app +staking_app_config = StakingAppConfig( + name="staking_api", + log_level=config.backend.log_level, + enable_perf=config.backend.performance_logging, + sqlite_db=config.backend.sqlite_db, + sqlite_schema=config.backend.sqlite_schema, + sqlite_db_registrations=config.backend.registration_sqlite_db, + sqlite_schema_registrations=config.backend.registration_sqlite_schema, + rpc_api=config.backend.rpc_api, + rpc_api_cache=config.backend.rpc_api_cache, + rpc_shared=config.backend.rpc_shared, + rpc_shared_cache=config.backend.rpc_shared_cache, + rpc_api_usage_logging=config.backend.rpc_api_usage_logging, + log_level_generic=config.backend.log_level_generic, + cache_stale_time_seconds=config.backend.stale_time_seconds, + disable_db_file_rewrite=config.backend.disable_db_file_rewrite, + stale_time_seconds_contract_abis=config.backend.stale_time_seconds_contract_abis, + rpc_api_usage_logging_interval=config.backend.rpc_api_usage_logging_interval, +) -app = create_app(config) \ No newline at end of file +app = create_app(staking_app_config) \ No newline at end of file diff --git a/src/staking/app.py b/src/staking/app.py index afa616b..2301315 100644 --- a/src/staking/app.py +++ b/src/staking/app.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import dataclasses +from dataclasses import dataclass import statistics import flask import eth_utils @@ -11,47 +12,61 @@ from .read import DBReaderStaking from ..oxen.rpc import OxenRPC from ..registration.read import DBReaderRegistrations -from ..util.flask_utils import FlaskApp, json_response +from ..util.flask_utils import FlaskApp, json_response, FlaskAppConfig from ..util.parse import Hex64Converter, EthConverter, eth_format +@dataclass +class StakingAppConfig(FlaskAppConfig): + # Flask App Config + disable_db_file_rewrite: bool = False + sqlite_db: str = None + sqlite_schema: str = None + + sqlite_db_registrations: str = None + sqlite_schema_registrations: str = None + + rpc_api: str = None + rpc_api_cache: int = None + rpc_shared: str = None + rpc_shared_cache: int = None + rpc_api_usage_logging: bool = None + rpc_api_usage_logging_interval: int = None + + stale_time_seconds_contract_abis: int = None class App(FlaskApp): - def __init__(self, config): - name = config.backend.registration_api_name if config.backend.registration_api_name else __name__ - super().__init__(name, enable_perf=config.backend.performance_logging, - log_level=config.backend.log_level, log_level_generic=config.backend.log_level_generic, - cache_stale_time_seconds=config.backend.stale_time_seconds) + def __init__(self, config: StakingAppConfig): + super().__init__(config) self.db_reader = DBReaderStaking( - db_path=config.backend.sqlite_db, - log_level=config.backend.log_level, - perf=config.backend.performance_logging, + db_path=config.sqlite_db, + log_level=config.log_level, + perf=config.enable_perf, ) self.db_reader_registrations = DBReaderRegistrations( - db_path=config.backend.registration_sqlite_db, - log_level=config.backend.log_level, - perf=config.backend.performance_logging, - disable_db_file_rewrite=config.backend.disable_db_file_rewrite, + db_path=config.sqlite_db_registrations, + log_level=config.log_level, + perf=config.enable_perf, ) - rpc_url = config.backend.rpc_api if config.backend.rpc_api else config.backend.rpc_shared + rpc_url = config.rpc_api if config.rpc_api else config.rpc_shared rpc_cache = ( - config.backend.rpc_api_cache - if config.backend.rpc_api_cache - else config.backend.rpc_shared_cache + config.rpc_api_cache + if config.rpc_api_cache + else config.rpc_shared_cache ) self.rpc = OxenRPC( logger=self.log, rpc_url=rpc_url, cache_seconds=rpc_cache, - usage_tracking=config.backend.rpc_api_usage_logging, + usage_tracking=config.rpc_api_usage_logging, ) self.allowed_contract_names = set() -def create_app(config) -> App: +def create_app(config: StakingAppConfig) -> App: app = App(config) def get_and_refresh_allowed_contract_names(): @@ -184,7 +199,7 @@ def get_cached_allowed_contract_names(): return app.cache.get( "allowed_contract_names", getter=get_and_refresh_allowed_contract_names, - ttl=config.backend.stale_time_seconds_contract_abis + ttl=config.stale_time_seconds_contract_abis ) @app.route("/contract/names") @@ -584,24 +599,24 @@ def route_get_network_info(): ////////////////////////////////////////////////////////////// """ - if config.backend.rpc_api_usage_logging: + if config.rpc_api_usage_logging: def log_rpc_usage(signum): app.rpc.usage_tracker.log_usage(" For signum {}".format(signum)) app.rpc.usage_tracker.write_failure_reasons_to_file(f"rpc-usage-failure-reasons-{signum}.txt") - @timer(config.backend.rpc_api_usage_logging_interval, target="worker1") + @timer(config.rpc_api_usage_logging_interval, target="worker1") def log_rpc_usage_w1(signum): log_rpc_usage(signum) - @timer(config.backend.rpc_api_usage_logging_interval, target="worker2") + @timer(config.rpc_api_usage_logging_interval, target="worker2") def log_rpc_usage_w2(signum): log_rpc_usage(signum) - @timer(config.backend.rpc_api_usage_logging_interval, target="worker3") + @timer(config.rpc_api_usage_logging_interval, target="worker3") def log_rpc_usage_w3(signum): log_rpc_usage(signum) - @timer(config.backend.rpc_api_usage_logging_interval, target="worker4") + @timer(config.rpc_api_usage_logging_interval, target="worker4") def log_rpc_usage_w4(signum): log_rpc_usage(signum) From fe6fd00a08b5714c5d1b88dc32f8186d5d1b7bc9 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Tue, 4 Feb 2025 11:56:43 +1100 Subject: [PATCH 094/138] fix: abi manager cache --- src/web3client/abi_manager.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/web3client/abi_manager.py b/src/web3client/abi_manager.py index e555056..a173ded 100644 --- a/src/web3client/abi_manager.py +++ b/src/web3client/abi_manager.py @@ -13,9 +13,9 @@ class ABIData: class ABIManager: - cache = {} + cache: dict[str, ABIData] = {} - def __init__(self, db_writer=None, abi_dir="web3client/abis"): + def __init__(self, db_writer=None, abi_dir="src/web3client/abis"): """ Initializes the ABIManager with the directory containing ABI JSON files. @@ -32,7 +32,7 @@ def get_abi(self, contract_name): Gets the ABI for a contract. """ if contract_name in self.cache: - return self.cache[contract_name] + return self.cache[contract_name].abi return self.load_abi(contract_name) @@ -60,8 +60,9 @@ def load_abi(self, file_name): name = data["contractName"] bytecode_bytes = data["bytecode"] deployed_bytecode_bytes = data["deployedBytecode"] - self.cache[file_name] = abi - return ABIData(name, abi, bytecode_bytes, deployed_bytecode_bytes) + data = ABIData(name, abi, bytecode_bytes, deployed_bytecode_bytes) + self.cache[file_name] = data + return data def load_all_abis(self): """ From 71a225a3f5c5690251a7f990a5dead38e7651a25 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Tue, 4 Feb 2025 11:57:05 +1100 Subject: [PATCH 095/138] fix: fetcher runner --- run_fetcher.py | 2 ++ src/app_fetcher.py | 6 ++++++ src/config_defaults.py | 2 +- src/config_validate.py | 11 +++++----- src/{ => staking}/fetcher.py | 41 +++++++++++++++++------------------- 5 files changed, 34 insertions(+), 28 deletions(-) create mode 100644 run_fetcher.py create mode 100644 src/app_fetcher.py rename src/{ => staking}/fetcher.py (96%) diff --git a/run_fetcher.py b/run_fetcher.py new file mode 100644 index 0000000..46d51cd --- /dev/null +++ b/run_fetcher.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +import src.app_fetcher \ No newline at end of file diff --git a/src/app_fetcher.py b/src/app_fetcher.py new file mode 100644 index 0000000..d9d4782 --- /dev/null +++ b/src/app_fetcher.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +from src import config +from src.staking.fetcher import App + +app = App(config.backend.fetcher_name if config.backend.fetcher_name else __name__) +app.run() \ No newline at end of file diff --git a/src/config_defaults.py b/src/config_defaults.py index 9bb70c3..8a23f5e 100644 --- a/src/config_defaults.py +++ b/src/config_defaults.py @@ -54,7 +54,7 @@ class Backend: """ FETCHER CONFIG """ - abi_dir = "web3client/abis" + abi_dir = "src/web3client/abis" # Arbitrum runs at ~4 blocks per second, and the rpc node has a limit of 30m, so scan for 120 blocks arbitrum_rescan_safety_blocks: int = 60 arbitrum_scan_start_chunk_size: int = 20 diff --git a/src/config_validate.py b/src/config_validate.py index f545654..7d6d79b 100644 --- a/src/config_validate.py +++ b/src/config_validate.py @@ -1,10 +1,11 @@ import logging -import config -from log import Log -from oxen.rpc import OxenRPC -from util import is_not_empty_string, valid_address_assertion -from web3client.client import Web3Client +from . import config +from .log import Log +from .oxen.rpc import OxenRPC +from .util import is_not_empty_string, valid_address_assertion +from .web3client.abi_manager import ABIManager +from .web3client.client import Web3Client def validate_config(conf: config): diff --git a/src/fetcher.py b/src/staking/fetcher.py similarity index 96% rename from src/fetcher.py rename to src/staking/fetcher.py index 7b67323..7d8ec49 100644 --- a/src/fetcher.py +++ b/src/staking/fetcher.py @@ -3,37 +3,37 @@ import subprocess import time -import config -from staking.arbitrum import ( +from ..staking.arbitrum import ( get_service_node_rewards_contract_id_map, get_new_contribution_contracts, update_contribution_contract_details, batch_populate_events_with_block_timestamps, populate_events_with_main_arg, ) -from config_validate import validate_config -from .staking.dataclasses import RewardsInfo, DBNodeExit -from db.util import ( +from ..config_validate import validate_config +from .. import config +from ..staking.dataclasses import RewardsInfo, DBNodeExit +from ..db.util import ( assert_all_dict_values_are_within_sqlite_integer_range, is_db_initialized, init_db, ) -from .staking.read import DBReaderStaking -from .staking.write import DBWriterStaking -from log import Log -from oxen.rpc import ServiceNode, OxenRPC, NetworkInfo -from util import format_seconds -from log.time_keeper import TimeKeeper -from util.parse import parse_bls_pubkey -from web3client.abi_manager import ABIManager -from web3client.client import Web3Client -from web3client.contracts.reward_rate_pool import RewardRatePoolInterface -from web3client.contracts.service_node_contribution import ( +from ..staking.read import DBReaderStaking +from ..staking.write import DBWriterStaking +from ..log import Log +from ..oxen.rpc import ServiceNode, OxenRPC, NetworkInfo +from ..util import format_seconds +from ..log.time_keeper import TimeKeeper +from ..util.parse import parse_bls_pubkey +from ..web3client.abi_manager import ABIManager +from ..web3client.client import Web3Client +from ..web3client.contracts.reward_rate_pool import RewardRatePoolInterface +from ..web3client.contracts.service_node_contribution import ( ServiceNodeContributionInterface, ) -from web3client.contracts.service_node_contribution_factory import ( +from ..web3client.contracts.service_node_contribution_factory import ( ServiceNodeContributionFactory, ) -from web3client.contracts.service_node_rewards import ServiceNodeRewardsInterface -from web3client.contracts.token import TokenInterface +from ..web3client.contracts.service_node_rewards import ServiceNodeRewardsInterface +from ..web3client.contracts.token import TokenInterface class App: @@ -589,6 +589,3 @@ def update_arbitrum_node_event_timestamps(self): return node_add_timestamps, node_last_added_timestamps - -app = App(config.backend.fetcher_name if config.backend.fetcher_name else __name__) -app.run() From 109b98fe388210f49c0c32a1aa0d4559d2345e7a Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Tue, 4 Feb 2025 15:48:14 +1100 Subject: [PATCH 096/138] fix: show reserved slots for non-contributed contracts --- src/staking/app.py | 6 +++--- src/staking/arbitrum.py | 28 +++++++++++++++------------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/staking/app.py b/src/staking/app.py index 2301315..8a858b6 100644 --- a/src/staking/app.py +++ b/src/staking/app.py @@ -210,7 +210,7 @@ def route_get_abi_names(): def route_get_abis(): return json_res( {"abis": app.cache.get("abis_all", getter=app.db_reader.get_smart_contract_abis, - ttl=config.backend.stale_time_seconds_contract_abis)} + ttl=config.stale_time_seconds_contract_abis)} ) @app.route("/contract/addresses") @@ -223,7 +223,7 @@ def get_contract_addresses(): def get_contract_addresses_core(): return json_res( {"addresses": app.cache.get("addresses_core", getter=app.db_reader.get_smart_contract_addresses_core, - ttl=config.backend.stale_time_seconds_contract_abis)} + ttl=config.stale_time_seconds_contract_abis)} ) def get_contribution_contracts_cached(): @@ -291,7 +291,7 @@ def get_abi(contract_name: str): "contract": app.cache.get( "abi-{}".format(contract_name), getter=app.db_reader.get_smart_contract_abi, getter_args=contract_name, - ttl=config.backend.stale_time_seconds_contract_abis + ttl=config.stale_time_seconds_contract_abis ) } ) diff --git a/src/staking/arbitrum.py b/src/staking/arbitrum.py index c1dd7d8..43c7035 100644 --- a/src/staking/arbitrum.py +++ b/src/staking/arbitrum.py @@ -122,27 +122,29 @@ def update_contribution_contract_details( reserved_addresses = reserved[0] reserved_amounts = reserved[1] - reserved_slots = {} - - for j in range(len(reserved_addresses)): - reserved_slots[reserved_addresses[j]] = reserved_amounts[j] + contributor_slots = {} for j in range(len(contributions_addresses)): address = contributions_addresses[j] - contributions_list.append( - { - "contract_address": contract_address, - "address": address, - "amount": contributions_amounts[j], - "beneficiary_address": contributions_beneficiaries[j], - "reserved": reserved_slots.get(address, 0), - } - ) + contributor_slots[address] = { + "contract_address": contract_address, + "address": address, + "amount": contributions_amounts[j], + "beneficiary_address": contributions_beneficiaries[j], + "reserved": 0, + } + + for j in range(len(reserved_addresses)): + address = reserved_addresses[j] + amount = reserved_amounts[j] + contributor_slots.setdefault(address, {"contract_address": contract_address, "address": address, "beneficiary_address":address, "amount": 0}).update({"reserved": amount}) status = responses[i + 4] manual_finalize = responses[i + 5] + contributions_list.extend(contributor_slots.values()) + contract_details.append( ContributionContractDetails( address=contract_address, From bee4a2b22a1d2591b054c7a217eaa6d9fa28f99b Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 7 Feb 2025 08:51:20 +1100 Subject: [PATCH 097/138] fix: module resolution for registration and snapshot apps --- src/app_registration.py | 4 ++-- src/app_snapshot.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app_registration.py b/src/app_registration.py index d403164..bf2c980 100644 --- a/src/app_registration.py +++ b/src/app_registration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 -import config -from registration.app import create_app, RegistrationAppConfig +from src import config +from src.registration.app import create_app, RegistrationAppConfig registration_config = RegistrationAppConfig( name="registration_api", diff --git a/src/app_snapshot.py b/src/app_snapshot.py index 2d56646..bbf2485 100644 --- a/src/app_snapshot.py +++ b/src/app_snapshot.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import config -from snapshot.app import create_app +from src import config +from src.snapshot.app import create_app app = create_app(config) \ No newline at end of file From df6361f2f3d9c987e756ce0e5ee93751f3f6736c Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 7 Feb 2025 10:33:28 +1100 Subject: [PATCH 098/138] fix: make default immutable block height 0 --- src/staking/dataclasses.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/staking/dataclasses.py b/src/staking/dataclasses.py index 930d3bf..5a642f5 100644 --- a/src/staking/dataclasses.py +++ b/src/staking/dataclasses.py @@ -105,6 +105,8 @@ class DBNetworkInfo: version: str def __post_init__(self): + if self.immutable_block_height is None: + self.immutable_block_height = 0 # We don't need the id field when its a dict self.__dataclass_fields__ = { k: v for k, v in self.__dataclass_fields__.items() if k != "id" From b52cfb25a357c78b185ad5bbf1bdca21c8425276 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 7 Feb 2025 10:37:02 +1100 Subject: [PATCH 099/138] fix: make immutable block height 0 if None --- src/staking/fetcher.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/staking/fetcher.py b/src/staking/fetcher.py index 7d8ec49..8f1ab5e 100644 --- a/src/staking/fetcher.py +++ b/src/staking/fetcher.py @@ -184,6 +184,8 @@ def run(self): self.log.perf.start("loop") network = self.rpc.get_network_info_from_network() + immutable_block_height = network.immutable_block_height if network.immutable_block_height is not None else 0 + network_last_fetched_height = ( self.db_reader.get_last_fetched_network_block_height() ) @@ -193,7 +195,7 @@ def run(self): self.log.debug( "Last fetched height: {}, Immutable height: {}, Commited height {}, Current height: {}, next block timestamp: {}, ".format( network_last_fetched_height, - network.immutable_block_height, + immutable_block_height, network_last_commited_height, network.block_height, network.pulse_target_timestamp, @@ -207,9 +209,9 @@ def run(self): self.update_arbitrum_details() self.time_keeper.end("arb_update") - if network.immutable_block_height > network_last_commited_height: + if immutable_block_height > network_last_commited_height: self.time_keeper.add("db_migrate_and_update_exit_list") - self.db_writer.write_nodes_to_main_db(network.immutable_block_height) + self.db_writer.write_nodes_to_main_db(immutable_block_height) self.update_exit_list() self.time_keeper.end("db_migrate_and_update_exit_list") From 419a637326f2b82284f143e7987f5c8b2a8bec6e Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 7 Feb 2025 10:42:03 +1100 Subject: [PATCH 100/138] fix: mutate immutable block height 0 if None --- src/staking/fetcher.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/staking/fetcher.py b/src/staking/fetcher.py index 8f1ab5e..f55c9bd 100644 --- a/src/staking/fetcher.py +++ b/src/staking/fetcher.py @@ -184,7 +184,8 @@ def run(self): self.log.perf.start("loop") network = self.rpc.get_network_info_from_network() - immutable_block_height = network.immutable_block_height if network.immutable_block_height is not None else 0 + if network.immutable_block_height is None: + network.immutable_block_height = 0 network_last_fetched_height = ( self.db_reader.get_last_fetched_network_block_height() @@ -195,7 +196,7 @@ def run(self): self.log.debug( "Last fetched height: {}, Immutable height: {}, Commited height {}, Current height: {}, next block timestamp: {}, ".format( network_last_fetched_height, - immutable_block_height, + network.immutable_block_height, network_last_commited_height, network.block_height, network.pulse_target_timestamp, @@ -209,9 +210,9 @@ def run(self): self.update_arbitrum_details() self.time_keeper.end("arb_update") - if immutable_block_height > network_last_commited_height: + if network.immutable_block_height > network_last_commited_height: self.time_keeper.add("db_migrate_and_update_exit_list") - self.db_writer.write_nodes_to_main_db(immutable_block_height) + self.db_writer.write_nodes_to_main_db(network.immutable_block_height) self.update_exit_list() self.time_keeper.end("db_migrate_and_update_exit_list") From afcf94191ed0012717a951a8e3396563cda0754c Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 7 Feb 2025 10:44:54 +1100 Subject: [PATCH 101/138] fix: use get for immutable block height 0 if None --- src/staking/fetcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/staking/fetcher.py b/src/staking/fetcher.py index f55c9bd..7060525 100644 --- a/src/staking/fetcher.py +++ b/src/staking/fetcher.py @@ -184,7 +184,7 @@ def run(self): self.log.perf.start("loop") network = self.rpc.get_network_info_from_network() - if network.immutable_block_height is None: + if network.get("immutable_block_height") is None: network.immutable_block_height = 0 network_last_fetched_height = ( From d6d3f7b4db2dfc682f6f8c4803387a97620d551f Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 7 Feb 2025 10:48:22 +1100 Subject: [PATCH 102/138] fix: recreate network class on immutable 0 --- src/staking/fetcher.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/staking/fetcher.py b/src/staking/fetcher.py index 7060525..cc202bf 100644 --- a/src/staking/fetcher.py +++ b/src/staking/fetcher.py @@ -184,8 +184,8 @@ def run(self): self.log.perf.start("loop") network = self.rpc.get_network_info_from_network() - if network.get("immutable_block_height") is None: - network.immutable_block_height = 0 + if network.immutable_block_height is None: + network = NetworkInfo(*network, immutable_block_height=0) network_last_fetched_height = ( self.db_reader.get_last_fetched_network_block_height() From 6b767cff7246152b4127075f479f05584a0df754 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 7 Feb 2025 10:51:21 +1100 Subject: [PATCH 103/138] fix: add defaults to oxen rpc network info --- src/oxen/rpc.py | 10 +++++----- src/staking/fetcher.py | 3 --- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/oxen/rpc.py b/src/oxen/rpc.py index 53842ef..ee75c18 100644 --- a/src/oxen/rpc.py +++ b/src/oxen/rpc.py @@ -166,14 +166,14 @@ def get_network_info_from_network(self): return NetworkInfo( block_hash=info.get("top_block_hash"), - block_height=info.get("height"), + block_height=info.get("height", 0), hard_fork=info.get("hard_fork"), immutable_block_hash=info.get("immutable_block_hash"), - immutable_block_height=info.get("immutable_height"), - max_stakers=info.get("max_contributors"), + immutable_block_height=info.get("immutable_height", 0), + max_stakers=info.get("max_contributors", 0), min_operator_contribution=info.get("min_operator_contribution"), nettype=info.get("nettype"), - pulse_target_timestamp=info.get("pulse_target_timestamp"), - staking_requirement=info.get("staking_requirement"), + pulse_target_timestamp=info.get("pulse_target_timestamp", 0), + staking_requirement=info.get("staking_requirement", 0), version=info.get("version"), ) diff --git a/src/staking/fetcher.py b/src/staking/fetcher.py index cc202bf..7d8ec49 100644 --- a/src/staking/fetcher.py +++ b/src/staking/fetcher.py @@ -184,9 +184,6 @@ def run(self): self.log.perf.start("loop") network = self.rpc.get_network_info_from_network() - if network.immutable_block_height is None: - network = NetworkInfo(*network, immutable_block_height=0) - network_last_fetched_height = ( self.db_reader.get_last_fetched_network_block_height() ) From 167713a8237f82ed679c67a976efa859b110a023 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 7 Feb 2025 10:52:50 +1100 Subject: [PATCH 104/138] fix: default config db schemas --- src/config_defaults.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config_defaults.py b/src/config_defaults.py index 8a23f5e..eedac05 100644 --- a/src/config_defaults.py +++ b/src/config_defaults.py @@ -24,7 +24,7 @@ class Backend: log_level_generic = None # Logs from other packages will use log_level if this is not set oxen_wallet_regex: str = "" sqlite_db: str = "ssb.db" - sqlite_schema: str = "db/schema.sql" + sqlite_schema: str = "src/staking/schema.sql" rpc_shared: list[str] = "" rpc_shared_cache: int = 2 disable_db_file_rewrite: bool = False @@ -46,7 +46,7 @@ class Backend: # NOTE: This can be the same DB as the main API, but you must manually run the registrations/schema.sql script in # the main db so it can be populated with the required tables. registration_sqlite_db: str = "ssb-registrations.db" - registration_sqlite_schema: str = "registration/schema.sql" + registration_sqlite_schema: str = "src/registration/schema.sql" # Creates a request per period limit by IP address (default is 100 requests per hour) registration_api_rate_limit: int = 100 registration_api_rate_limit_period: int = 3600 From f3d05408b3b480b8fd650c8a781412aa01a6e82d Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 7 Feb 2025 10:56:10 +1100 Subject: [PATCH 105/138] fix: add defaults for all net info fields --- src/oxen/rpc.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/oxen/rpc.py b/src/oxen/rpc.py index ee75c18..a49ca5c 100644 --- a/src/oxen/rpc.py +++ b/src/oxen/rpc.py @@ -165,15 +165,15 @@ def get_network_info_from_network(self): self.log.silly("get_network_info_from_network info: {}".format(info)) return NetworkInfo( - block_hash=info.get("top_block_hash"), + block_hash=info.get("top_block_hash", "NA"), block_height=info.get("height", 0), - hard_fork=info.get("hard_fork"), - immutable_block_hash=info.get("immutable_block_hash"), + hard_fork=info.get("hard_fork", "NA"), + immutable_block_hash=info.get("immutable_block_hash", "NA"), immutable_block_height=info.get("immutable_height", 0), max_stakers=info.get("max_contributors", 0), - min_operator_contribution=info.get("min_operator_contribution"), - nettype=info.get("nettype"), + min_operator_contribution=info.get("min_operator_contribution", 0), + nettype=info.get("nettype", "NA"), pulse_target_timestamp=info.get("pulse_target_timestamp", 0), staking_requirement=info.get("staking_requirement", 0), - version=info.get("version"), + version=info.get("version", "NA"), ) From 1fc42b2dc23b82d31caeced146e627501896c894 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 7 Mar 2025 15:56:46 +1100 Subject: [PATCH 106/138] fix: update db snapshot task with new config --- src/app_snapshot.py | 16 ++++++++++++++-- src/snapshot/app.py | 36 +++++++++++++++++++++--------------- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/src/app_snapshot.py b/src/app_snapshot.py index bbf2485..6658e57 100644 --- a/src/app_snapshot.py +++ b/src/app_snapshot.py @@ -1,6 +1,18 @@ #!/usr/bin/env python3 from src import config -from src.snapshot.app import create_app +from src.snapshot.app import create_app, SnapshotAppConfig -app = create_app(config) \ No newline at end of file +snapshot_app_config = SnapshotAppConfig( + name="snapshot", + log_level=config.backend.log_level, + enable_perf=config.backend.performance_logging, + sqlite_db=config.backend.sqlite_db, + log_level_generic=config.backend.log_level_generic, + cache_stale_time_seconds=config.backend.stale_time_seconds, + sqlite_db_snapshot=config.backend.sqlite_db_snapshot, + snapshot_on_startup=config.backend.snapshot_on_startup, + snapshot_time_interval_seconds=config.backend.snapshot_time_interval_seconds, +) + +app = create_app(snapshot_app_config) \ No newline at end of file diff --git a/src/snapshot/app.py b/src/snapshot/app.py index 059e31c..12aaf9d 100644 --- a/src/snapshot/app.py +++ b/src/snapshot/app.py @@ -1,42 +1,48 @@ #!/usr/bin/env python3 +from dataclasses import dataclass from uwsgidecorators import timer from ..staking.snapshot import DBSnapshot -from ..util.flask_utils import FlaskApp +from ..util.flask_utils import FlaskApp, FlaskAppConfig -class App(FlaskApp): - def __init__(self, config): - name = config.backend.snapshot_task_name if config.backend.snapshot_task_name else __name__ - super().__init__(name, enable_perf=config.backend.performance_logging, - log_level=config.backend.log_level) +@dataclass +class SnapshotAppConfig(FlaskAppConfig): + sqlite_db: str = None + sqlite_db_snapshot: str = None + snapshot_time_interval_seconds: int = None + snapshot_on_startup: bool = None + - assert config.backend.sqlite_snapshot_time_interval_seconds > 29, "Snapshot interval must be greater than 29 seconds" +class App(FlaskApp): + def __init__(self, config: SnapshotAppConfig): + super().__init__(config) + assert config.snapshot_time_interval_seconds >= 1, "Snapshot interval must be at least 1 second" self.db_snapshot = DBSnapshot( - source_db_path=config.backend.sqlite_db, - snapshot_db_path=config.backend.sqlite_db_snapshot, + source_db_path=config.sqlite_db, + snapshot_db_path=config.sqlite_db_snapshot, excluded_tables={ # Staging rows, these are committed to the main db once the immutable height is reached, this can be synced by the end user "service_nodes_staging", "service_nodes_contributions_staging", }, - log_level=config.backend.log_level, - perf=config.backend.performance_logging, + log_level=config.log_level, + perf=config.enable_perf, ) - self.log.info("Snapshot task initialized, will run every {} seconds.".format( - config.backend.sqlite_snapshot_time_interval_seconds)) + self.log.info(f"Snapshot task initialized, will run every {config.snapshot_time_interval_seconds} seconds.") def create_app(config) -> App: app = App(config) - @timer(config.backend.sqlite_snapshot_time_interval_seconds) + @timer(config.snapshot_time_interval_seconds) def snapshot_db(signum): app.db_snapshot.snapshot() - if config.backend.snapshot_on_startup: + if config.snapshot_on_startup: + app.log.info("Snapshotting database on startup, set snapshot_on_startup=False to disable") snapshot_db(None) return app From f1ce6dd3987a300a7f64e0cb113f24a00dd8993b Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 7 Mar 2025 15:58:46 +1100 Subject: [PATCH 107/138] fix: update default config and config validator --- src/config_defaults.py | 85 ++++++++++++++++++++++++++++++------------ src/config_validate.py | 75 ++++++++++++------------------------- 2 files changed, 85 insertions(+), 75 deletions(-) diff --git a/src/config_defaults.py b/src/config_defaults.py index eedac05..b2f5689 100644 --- a/src/config_defaults.py +++ b/src/config_defaults.py @@ -22,27 +22,50 @@ class Backend: """ log_level = logging.INFO log_level_generic = None # Logs from other packages will use log_level if this is not set + performance_logging: bool = False oxen_wallet_regex: str = "" - sqlite_db: str = "ssb.db" - sqlite_schema: str = "src/staking/schema.sql" rpc_shared: list[str] = "" rpc_shared_cache: int = 2 - disable_db_file_rewrite: bool = False + """ + WEB3 Config + """ + web3_provider_urls: list[str] = ["http://localhost:8545"] # Default hardhat private chain node address) + web3_provider_urls_eth: list[str] = ["http://localhost:8545"] + web3_caller_address: str | None = None + web3_private_key: str | None = None + addr_reward_rate_pool: str = "0x0000000000000000000000000000000000000000" + addr_token: str = "0x0000000000000000000000000000000000000000" + addr_sn_contrib: str = "0x0000000000000000000000000000000000000000" + addr_sn_contrib_factory: str = "0x0000000000000000000000000000000000000000" + addr_sn_rewards: str = "0x0000000000000000000000000000000000000000" + # All block scanning will take this as the starting block, no log events can occur before a contract is deployed + # so this is the first block that can be used to scan for events. Using 0 significantly slows down the scan. + # genesis_block: int = 114505919 + genesis_block: int = 114500919 + + """ + DB CONFIG + """ + sqlite_db: str = "ssb.db" + sqlite_schema: str = "src/staking/schema.sql" + disable_db_file_rewrite: bool = False + db_reset_events_on_startup: bool = False + db_reset_contrib_on_startup: bool = False """ API CONFIG """ - api_name: str = "api" - rpc_api: str = "" - rpc_api_cache: int = 2 - rpc_api_usage_logging: bool = False - rpc_api_usage_logging_interval: int = 300 + rpc_api: str = "" + rpc_api_cache: int = 2 + rpc_api_usage_logging: bool = False + rpc_api_usage_logging_interval: int = 300 + stale_time_seconds: int = 5 + stale_time_seconds_contract_abis: int = 300 """ REGISTRATION CONFIG """ - registration_api_name: str = "registration_api" # NOTE: This can be the same DB as the main API, but you must manually run the registrations/schema.sql script in # the main db so it can be populated with the required tables. registration_sqlite_db: str = "ssb-registrations.db" @@ -58,17 +81,9 @@ class Backend: # Arbitrum runs at ~4 blocks per second, and the rpc node has a limit of 30m, so scan for 120 blocks arbitrum_rescan_safety_blocks: int = 60 arbitrum_scan_start_chunk_size: int = 20 - addr_reward_rate_pool: str = "0x0000000000000000000000000000000000000000" - addr_token: str = "0x0000000000000000000000000000000000000000" - addr_sn_contrib: str = "0x0000000000000000000000000000000000000000" - addr_sn_contrib_factory: str = "0x0000000000000000000000000000000000000000" - addr_sn_rewards: str = "0x0000000000000000000000000000000000000000" - refresh_rate_seconds_arbitrum: int = 30 + refresh_rate_seconds_arbitrum: int = 10 max_time_keeper_events: int = 10_000 - fetcher_name: str = "fetcher" - performance_logging: bool = False - rpc_fetcher: str = "" - rpc_fetcher_cache: int = 2 + rpc_fetcher_cache: int = 1 rpc_fetcher_usage_logging: bool = False stale_time_seconds: int = 30 stale_time_seconds_contract_abis: int = 300 @@ -76,19 +91,41 @@ class Backend: web3_caller_address: str | None = None web3_private_key: str | None = None web3_provider_urls: list[str] = ["http://localhost:8545"] # Default hardhat private chain node address) + write_rpc_fail_reasons_to_file: bool = False + + """ + VESTING + """ + vesting_contract_details_csv: str = "vesting.csv" + reset_vesting_contracts_on_startup: bool = False + + """ + WEB SOCKETS + """ + # This will disable Arbitrum event scanning in the fetcher + ws_enabled: bool = True + ws_providers: list[str] = [] + # The default ws size is set to 1GB to ensure all bootstrapping event scans go through, this value is extremely large + # to ensure it isn't exceeded in the future, as with time the size of the full network event scan will increase. if + # your Arbitrum node doesn't support high enough websocket sizes you should download the snapshot database and start + # running from there. TODO: add event scan chunking to allow for smaller websocket sizes + ws_max_size: int = 1_000_000_000 + # The maximum depth of the event scanner bootstrap loop. This is to prevent infinite loops in the event scanner. + # While it shouldn't be possible for this to happen, it is a safety measure. + ws_max_run_depth: int = 10 + # Only enable this if you want to track Approve and Transfer events for the token contract + ws_watch_token_events: bool = False """ SNAPSHOT CONFIG """ - snapshot_task_name: str = "snapshot" sqlite_db_snapshot: str = "static/backend-snapshot.db" - sqlite_snapshot_time_interval_seconds: int = 600 + snapshot_time_interval_seconds: int = 600 snapshot_on_startup: bool = False """ PRICE FETCHER CONFIG """ - prices_api_name: str = "prices_api" enable_price_fetcher: bool = False coingecko_api_key: str = "" coingecko_api_url: str = "https://api.coingecko.com/api" @@ -112,7 +149,6 @@ class Backend: # Session testnet contracts testnet_backend = Backend() -testnet_backend.oxen_wallet_regex = f"T[{B58_ALPHABET}]{{96}}" testnet_backend.rpc_shared = "ipc://oxend/testnet.sock" testnet_backend.sqlite_db = "ssb-testnet.db" @@ -129,7 +165,6 @@ class Backend: # Session stagenet.v3 contracts stagenet_backend = Backend() -stagenet_backend.web3_provider_urls = ["http://10.24.0.2/arb_sepolia"] stagenet_backend.addr_reward_rate_pool = "0xaAD853fE7091728dac0DAa7b69990ee68abFC636" stagenet_backend.addr_token = "0x7D7fD4E91834A96cD9Fb2369E7f4EB72383bbdEd" stagenet_backend.addr_sn_contrib_factory = "0x36Ee2Da54a7E727cC996A441826BBEdda6336B71" @@ -137,6 +172,8 @@ class Backend: stagenet_backend.oxen_wallet_regex = f"ST[{B58_ALPHABET}]{{95}}" stagenet_backend.rpc_shared = "tcp://localhost:6786" stagenet_backend.sqlite_db = "ssb-stagenet.db" +stagenet_backend.ws_providers = ["ws://10.24.0.1/arb_sepolia/ws"] +stagenet_backend.web3_provider_urls = ["http://10.24.0.1/arb_sepolia"] # Assign the active backend to be used in the sent-staking-backend backend = stagenet_backend diff --git a/src/config_validate.py b/src/config_validate.py index 7d6d79b..bee0fa4 100644 --- a/src/config_validate.py +++ b/src/config_validate.py @@ -1,68 +1,46 @@ import logging -from . import config +from eth_utils import is_checksum_address + from .log import Log from .oxen.rpc import OxenRPC -from .util import is_not_empty_string, valid_address_assertion -from .web3client.abi_manager import ABIManager +from .util import is_not_empty_string from .web3client.client import Web3Client +log = Log("config_validate").logger -def validate_config(conf: config): - log = Log("config_validate").logger - log.perf.start("validate_config") - log.info("Validating config") - - """ - Production warnings - """ - if conf.backend.log_level < logging.INFO: +def validate_log_config(conf): + if conf.log_level < logging.INFO: log.warning( "Log level is set to {} which is less than INFO. This is not recommended for production.".format( - conf.backend.log_level + conf.log_level ) ) - elif conf.backend.log_level > logging.ERROR: + elif conf.log_level > logging.ERROR: log.warning( "Log level is set to {} which is greater than ERROR. This is not recommended for production.".format( - conf.backend.log_level + conf.log_level ) ) - if conf.backend.performance_logging: - log.warning("Performance logging is enabled. This is not recommended for production.") - - """ - Validations - """ +def validate_contract_addresses(conf): + assert is_checksum_address(conf.addr_token), "addr_token is not a valid checksum address" + assert is_checksum_address(conf.addr_sn_contrib_factory), "addr_sn_contrib_factory is not a valid checksum address" + assert is_checksum_address(conf.addr_sn_rewards), "addr_sn_rewards is not a valid checksum address" + assert is_checksum_address(conf.addr_reward_rate_pool), "addr_reward_rate_pool is not a valid checksum address" - assert is_not_empty_string(conf.backend.sqlite_db), "sqlite_db is not set in config.py" - rpc_url = conf.backend.rpc_fetcher if conf.backend.rpc_fetcher else conf.backend.rpc_shared - assert is_not_empty_string(rpc_url), "rpc url is not set in config.py requires rpc_fetcher or rpc" - assert is_not_empty_string( - conf.backend.oxen_wallet_regex - ), "oxen_wallet_regex is not set in config.py" - - # Assert all contract addresses are valid - valid_address_assertion(conf.backend.addr_sn_contrib, "addr_sn_contrib") - valid_address_assertion(conf.backend.addr_token, "addr_sent") - valid_address_assertion(conf.backend.addr_sn_rewards, "addr_sn_rewards") - valid_address_assertion(conf.backend.addr_reward_rate_pool, "addr_reward_rate_pool") - - assert conf.backend.web3_provider_urls is not None and len( - conf.backend.web3_provider_urls +def validate_web3_client(conf): + assert conf.web3_provider_urls is not None and len( + conf.web3_provider_urls ) > 0, "web3_provider_urls is not set in config.py" - for web3_provider_url in conf.backend.web3_provider_urls: + for web3_provider_url in conf.web3_provider_urls: assert is_not_empty_string(web3_provider_url), "web3_provider_urls is not set properly in config.py" - """ - Web3 client validations - """ web3_client = Web3Client( - conf.backend.web3_provider_urls, - conf.backend.web3_caller_address, - conf.backend.web3_private_key, + conf.web3_provider_urls, + conf.web3_caller_address, + conf.web3_private_key, log, ) @@ -70,15 +48,10 @@ def validate_config(conf: config): log.debug("Config validation block number: {}".format(block_number)) assert block_number is not None, "Failed to get block number from web3 provider" - """ - Oxen RPC validations - """ - rpc = OxenRPC(log, rpc_url, 0) +def validate_oxen_rpc(conf): + rpc = OxenRPC(log, conf.rpc_shared, 0) res = rpc.get_info().get() log.debug("Config validation rpc response status: {}".format(res.get("status"))) assert ( res is not None and res.get("status") == "OK" - ), "Oxen RPC ping to {} failed with response: {}".format(conf.backend.rpc_fetcher, res) - - log.info("Config validation finished") - log.perf.end("validate_config") + ), "Oxen RPC ping to {} failed with response: {}".format(conf.rpc_shared, res) From 2e9b5d9540288f3e43ed5cfba0f67981019d6a9d Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 7 Mar 2025 15:59:06 +1100 Subject: [PATCH 108/138] chore: remove unused config items --- src/config_defaults.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/config_defaults.py b/src/config_defaults.py index b2f5689..a06da19 100644 --- a/src/config_defaults.py +++ b/src/config_defaults.py @@ -85,12 +85,6 @@ class Backend: max_time_keeper_events: int = 10_000 rpc_fetcher_cache: int = 1 rpc_fetcher_usage_logging: bool = False - stale_time_seconds: int = 30 - stale_time_seconds_contract_abis: int = 300 - thread_pool_max_workers: int = 50 - web3_caller_address: str | None = None - web3_private_key: str | None = None - web3_provider_urls: list[str] = ["http://localhost:8545"] # Default hardhat private chain node address) write_rpc_fail_reasons_to_file: bool = False """ From ae4524b8bcc1484df42f57dbb33ed257edbf8ce8 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 7 Mar 2025 16:03:21 +1100 Subject: [PATCH 109/138] chore: update abis --- src/web3client/abis/RewardRatePool.json | 8 +- .../abis/ServiceNodeContribution.json | 6 +- .../abis/ServiceNodeContributionFactory.json | 4 +- src/web3client/abis/ServiceNodeRewards.json | 159 +++--- src/web3client/abis/Token.json | 8 +- src/web3client/abis/TokenVestingStaking.json | 537 ++++++++++++++++++ src/web3client/contracts/token.py | 15 - 7 files changed, 622 insertions(+), 115 deletions(-) create mode 100644 src/web3client/abis/TokenVestingStaking.json diff --git a/src/web3client/abis/RewardRatePool.json b/src/web3client/abis/RewardRatePool.json index 48ebf9a..aeac884 100644 --- a/src/web3client/abis/RewardRatePool.json +++ b/src/web3client/abis/RewardRatePool.json @@ -178,7 +178,7 @@ }, { "inputs": [], - "name": "SENT", + "name": "SESH", "outputs": [ { "internalType": "contract IERC20", @@ -268,7 +268,7 @@ }, { "internalType": "address", - "name": "_sent", + "name": "_sesh", "type": "address" } ], @@ -383,8 +383,8 @@ "type": "function" } ], - "bytecode": "0x6080604052348015600f57600080fd5b50610ba98061001f6000396000f3fe608060405234801561001057600080fd5b50600436106100e65760003560e01c80631357e1dc146100eb5780631c31f7101461010757806332f7c3841461011c57806338af3eed14610124578063485cc9551461014457806360e52ecb14610157578063715018a61461016a57806379ba5097146101725780637b0a47ee1461017a5780638da5cb5b14610182578063ab1a4a2f1461018a578063ab5fcdc91461019f578063ca598087146101a8578063d66fcb69146101b0578063e1f1c4a7146101c3578063e30c3978146101cc578063f2fde38b146101d4578063f85bb0f0146101e7575b600080fd5b6100f460025481565b6040519081526020015b60405180910390f35b61011a6101153660046109fc565b6101ef565b005b6100f461024d565b600154610137906001600160a01b031681565b6040516100fe9190610a17565b61011a610152366004610a2b565b6102ef565b600054610137906001600160a01b031681565b61011a610422565b61011a610436565b6100f461047e565b6101376104b2565b610192609781565b6040516100fe9190610a5e565b6100f460035481565b6100f46104cd565b6100f46101be366004610a72565b610554565b6101926103e881565b610137610598565b61011a6101e23660046109fc565b6105a3565b61011a610614565b6101f7610690565b600180546001600160a01b0319166001600160a01b0383161790556040517feee59a71c694e68368a1cb0d135c448051bbfb12289e6c2223b0ceb100c2321d90610242908390610a17565b60405180910390a150565b6000806003544261025e9190610aaa565b6000546040516370a0823160e01b81529192506102dc916001600160a01b03909116906370a0823190610295903090600401610a17565b602060405180830381865afa1580156102b2573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906102d69190610abd565b82610554565b6002546102e99190610ad6565b91505090565b60006102f96106c2565b805490915060ff600160401b82041615906001600160401b03166000811580156103205750825b90506000826001600160401b0316600114801561033c5750303b155b90508115801561034a575080155b156103685760405163f92ee8a960e01b815260040160405180910390fd5b84546001600160401b0319166001178555831561039157845460ff60401b1916600160401b1785555b600180546001600160a01b03808a166001600160a01b0319928316179092554260035560008054928916929091169190911790556103ce336106e6565b831561041957845460ff60401b191685556040517fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29061041090600190610a5e565b60405180910390a15b50505050505050565b61042a610690565b61043460006106f7565b565b3380610440610598565b6001600160a01b031614610472578060405163118cdaa760e01b81526004016104699190610a17565b60405180910390fd5b61047b816106f7565b50565b60008061048961024d565b905060006104956104cd565b90506104ab6104a48383610aaa565b6078610554565b9250505090565b6000806104bd61071a565b546001600160a01b031692915050565b600254600080546040516370a0823160e01b81529192916001600160a01b03909116906370a0823190610504903090600401610a17565b602060405180830381865afa158015610521573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906105459190610abd565b61054f9190610ad6565b905090565b60006105666103e86301e13380610ae9565b6001600160401b03168261057b609786610b12565b6105859190610b12565b61058f9190610b29565b90505b92915050565b6000806104bd61073e565b6105ab610690565b60006105b561073e565b80546001600160a01b0319166001600160a01b03841690811782559091506105db6104b2565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b600061061e61024d565b90506000600254826106309190610aaa565b6002839055426003556040518181529091507f952b264c8e0a06cddb4bbaa6d6af1d565145329fd95bbe72cb2b53942b2dc9669060200160405180910390a160015460005461068c916001600160a01b03918216911683610762565b5050565b336106996104b2565b6001600160a01b031614610434573360405163118cdaa760e01b81526004016104699190610a17565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0090565b6106ee6107b9565b61047b816107de565b600061070161073e565b80546001600160a01b0319168155905061068c82610810565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930090565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0090565b604080516001600160a01b038416602482015260448082018490528251808303909101815260649091019091526020810180516001600160e01b031663a9059cbb60e01b1790526107b490849061086c565b505050565b6107c16108c6565b61043457604051631afcd79f60e31b815260040160405180910390fd5b6107e66107b9565b6001600160a01b038116610472576000604051631e4fbdf760e01b81526004016104699190610a17565b600061081a61071a565b80546001600160a01b038481166001600160a01b031983168117845560405193945091169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a3505050565b60006108816001600160a01b038416836108e0565b905080516000141580156108a65750808060200190518101906108a49190610b4b565b155b156107b45782604051635274afe760e01b81526004016104699190610a17565b60006108d06106c2565b54600160401b900460ff16919050565b606061058f8383600084600080856001600160a01b031684866040516109069190610b6d565b60006040518083038185875af1925050503d8060008114610943576040519150601f19603f3d011682016040523d82523d6000602084013e610948565b606091505b5091509150610958868383610964565b925050505b9392505050565b60608261097957610974826109b7565b61095d565b815115801561099057506001600160a01b0384163b155b156109b05783604051639996b31560e01b81526004016104699190610a17565b508061095d565b8051156109c75780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b80356001600160a01b03811681146109f757600080fd5b919050565b600060208284031215610a0e57600080fd5b61058f826109e0565b6001600160a01b0391909116815260200190565b60008060408385031215610a3e57600080fd5b610a47836109e0565b9150610a55602084016109e0565b90509250929050565b6001600160401b0391909116815260200190565b60008060408385031215610a8557600080fd5b50508035926020909101359150565b634e487b7160e01b600052601160045260246000fd5b8181038181111561059257610592610a94565b600060208284031215610acf57600080fd5b5051919050565b8082018082111561059257610592610a94565b6001600160401b038181168382160290811690818114610b0b57610b0b610a94565b5092915050565b808202811582820484141761059257610592610a94565b600082610b4657634e487b7160e01b600052601260045260246000fd5b500490565b600060208284031215610b5d57600080fd5b8151801515811461095d57600080fd5b6000825160005b81811015610b8e5760208186018101518583015201610b74565b50600092019182525091905056fea164736f6c634300081a000a", - "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100e65760003560e01c80631357e1dc146100eb5780631c31f7101461010757806332f7c3841461011c57806338af3eed14610124578063485cc9551461014457806360e52ecb14610157578063715018a61461016a57806379ba5097146101725780637b0a47ee1461017a5780638da5cb5b14610182578063ab1a4a2f1461018a578063ab5fcdc91461019f578063ca598087146101a8578063d66fcb69146101b0578063e1f1c4a7146101c3578063e30c3978146101cc578063f2fde38b146101d4578063f85bb0f0146101e7575b600080fd5b6100f460025481565b6040519081526020015b60405180910390f35b61011a6101153660046109fc565b6101ef565b005b6100f461024d565b600154610137906001600160a01b031681565b6040516100fe9190610a17565b61011a610152366004610a2b565b6102ef565b600054610137906001600160a01b031681565b61011a610422565b61011a610436565b6100f461047e565b6101376104b2565b610192609781565b6040516100fe9190610a5e565b6100f460035481565b6100f46104cd565b6100f46101be366004610a72565b610554565b6101926103e881565b610137610598565b61011a6101e23660046109fc565b6105a3565b61011a610614565b6101f7610690565b600180546001600160a01b0319166001600160a01b0383161790556040517feee59a71c694e68368a1cb0d135c448051bbfb12289e6c2223b0ceb100c2321d90610242908390610a17565b60405180910390a150565b6000806003544261025e9190610aaa565b6000546040516370a0823160e01b81529192506102dc916001600160a01b03909116906370a0823190610295903090600401610a17565b602060405180830381865afa1580156102b2573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906102d69190610abd565b82610554565b6002546102e99190610ad6565b91505090565b60006102f96106c2565b805490915060ff600160401b82041615906001600160401b03166000811580156103205750825b90506000826001600160401b0316600114801561033c5750303b155b90508115801561034a575080155b156103685760405163f92ee8a960e01b815260040160405180910390fd5b84546001600160401b0319166001178555831561039157845460ff60401b1916600160401b1785555b600180546001600160a01b03808a166001600160a01b0319928316179092554260035560008054928916929091169190911790556103ce336106e6565b831561041957845460ff60401b191685556040517fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29061041090600190610a5e565b60405180910390a15b50505050505050565b61042a610690565b61043460006106f7565b565b3380610440610598565b6001600160a01b031614610472578060405163118cdaa760e01b81526004016104699190610a17565b60405180910390fd5b61047b816106f7565b50565b60008061048961024d565b905060006104956104cd565b90506104ab6104a48383610aaa565b6078610554565b9250505090565b6000806104bd61071a565b546001600160a01b031692915050565b600254600080546040516370a0823160e01b81529192916001600160a01b03909116906370a0823190610504903090600401610a17565b602060405180830381865afa158015610521573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906105459190610abd565b61054f9190610ad6565b905090565b60006105666103e86301e13380610ae9565b6001600160401b03168261057b609786610b12565b6105859190610b12565b61058f9190610b29565b90505b92915050565b6000806104bd61073e565b6105ab610690565b60006105b561073e565b80546001600160a01b0319166001600160a01b03841690811782559091506105db6104b2565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b600061061e61024d565b90506000600254826106309190610aaa565b6002839055426003556040518181529091507f952b264c8e0a06cddb4bbaa6d6af1d565145329fd95bbe72cb2b53942b2dc9669060200160405180910390a160015460005461068c916001600160a01b03918216911683610762565b5050565b336106996104b2565b6001600160a01b031614610434573360405163118cdaa760e01b81526004016104699190610a17565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0090565b6106ee6107b9565b61047b816107de565b600061070161073e565b80546001600160a01b0319168155905061068c82610810565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930090565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0090565b604080516001600160a01b038416602482015260448082018490528251808303909101815260649091019091526020810180516001600160e01b031663a9059cbb60e01b1790526107b490849061086c565b505050565b6107c16108c6565b61043457604051631afcd79f60e31b815260040160405180910390fd5b6107e66107b9565b6001600160a01b038116610472576000604051631e4fbdf760e01b81526004016104699190610a17565b600061081a61071a565b80546001600160a01b038481166001600160a01b031983168117845560405193945091169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a3505050565b60006108816001600160a01b038416836108e0565b905080516000141580156108a65750808060200190518101906108a49190610b4b565b155b156107b45782604051635274afe760e01b81526004016104699190610a17565b60006108d06106c2565b54600160401b900460ff16919050565b606061058f8383600084600080856001600160a01b031684866040516109069190610b6d565b60006040518083038185875af1925050503d8060008114610943576040519150601f19603f3d011682016040523d82523d6000602084013e610948565b606091505b5091509150610958868383610964565b925050505b9392505050565b60608261097957610974826109b7565b61095d565b815115801561099057506001600160a01b0384163b155b156109b05783604051639996b31560e01b81526004016104699190610a17565b508061095d565b8051156109c75780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b80356001600160a01b03811681146109f757600080fd5b919050565b600060208284031215610a0e57600080fd5b61058f826109e0565b6001600160a01b0391909116815260200190565b60008060408385031215610a3e57600080fd5b610a47836109e0565b9150610a55602084016109e0565b90509250929050565b6001600160401b0391909116815260200190565b60008060408385031215610a8557600080fd5b50508035926020909101359150565b634e487b7160e01b600052601160045260246000fd5b8181038181111561059257610592610a94565b600060208284031215610acf57600080fd5b5051919050565b8082018082111561059257610592610a94565b6001600160401b038181168382160290811690818114610b0b57610b0b610a94565b5092915050565b808202811582820484141761059257610592610a94565b600082610b4657634e487b7160e01b600052601260045260246000fd5b500490565b600060208284031215610b5d57600080fd5b8151801515811461095d57600080fd5b6000825160005b81811015610b8e5760208186018101518583015201610b74565b50600092019182525091905056fea164736f6c634300081a000a", + "bytecode": "0x6080604052348015600f57600080fd5b50610ba98061001f6000396000f3fe608060405234801561001057600080fd5b50600436106100e65760003560e01c80631357e1dc146100eb5780631c31f7101461010757806332f7c3841461011c57806338af3eed14610124578063485cc95514610144578063715018a61461015757806379ba50971461015f5780637b0a47ee146101675780638da5cb5b1461016f578063ab1a4a2f14610177578063ab5fcdc91461018c578063c3aca55814610195578063ca598087146101a8578063d66fcb69146101b0578063e1f1c4a7146101c3578063e30c3978146101cc578063f2fde38b146101d4578063f85bb0f0146101e7575b600080fd5b6100f460025481565b6040519081526020015b60405180910390f35b61011a6101153660046109fc565b6101ef565b005b6100f461024d565b600154610137906001600160a01b031681565b6040516100fe9190610a17565b61011a610152366004610a2b565b6102ef565b61011a610422565b61011a610436565b6100f461047e565b6101376104b2565b61017f609781565b6040516100fe9190610a5e565b6100f460035481565b600054610137906001600160a01b031681565b6100f46104cd565b6100f46101be366004610a72565b610554565b61017f6103e881565b610137610598565b61011a6101e23660046109fc565b6105a3565b61011a610614565b6101f7610690565b600180546001600160a01b0319166001600160a01b0383161790556040517feee59a71c694e68368a1cb0d135c448051bbfb12289e6c2223b0ceb100c2321d90610242908390610a17565b60405180910390a150565b6000806003544261025e9190610aaa565b6000546040516370a0823160e01b81529192506102dc916001600160a01b03909116906370a0823190610295903090600401610a17565b602060405180830381865afa1580156102b2573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906102d69190610abd565b82610554565b6002546102e99190610ad6565b91505090565b60006102f96106c2565b805490915060ff600160401b82041615906001600160401b03166000811580156103205750825b90506000826001600160401b0316600114801561033c5750303b155b90508115801561034a575080155b156103685760405163f92ee8a960e01b815260040160405180910390fd5b84546001600160401b0319166001178555831561039157845460ff60401b1916600160401b1785555b600180546001600160a01b03808a166001600160a01b0319928316179092554260035560008054928916929091169190911790556103ce336106e6565b831561041957845460ff60401b191685556040517fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29061041090600190610a5e565b60405180910390a15b50505050505050565b61042a610690565b61043460006106f7565b565b3380610440610598565b6001600160a01b031614610472578060405163118cdaa760e01b81526004016104699190610a17565b60405180910390fd5b61047b816106f7565b50565b60008061048961024d565b905060006104956104cd565b90506104ab6104a48383610aaa565b6078610554565b9250505090565b6000806104bd61071a565b546001600160a01b031692915050565b600254600080546040516370a0823160e01b81529192916001600160a01b03909116906370a0823190610504903090600401610a17565b602060405180830381865afa158015610521573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906105459190610abd565b61054f9190610ad6565b905090565b60006105666103e86301e13380610ae9565b6001600160401b03168261057b609786610b12565b6105859190610b12565b61058f9190610b29565b90505b92915050565b6000806104bd61073e565b6105ab610690565b60006105b561073e565b80546001600160a01b0319166001600160a01b03841690811782559091506105db6104b2565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b600061061e61024d565b90506000600254826106309190610aaa565b6002839055426003556040518181529091507f952b264c8e0a06cddb4bbaa6d6af1d565145329fd95bbe72cb2b53942b2dc9669060200160405180910390a160015460005461068c916001600160a01b03918216911683610762565b5050565b336106996104b2565b6001600160a01b031614610434573360405163118cdaa760e01b81526004016104699190610a17565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0090565b6106ee6107b9565b61047b816107de565b600061070161073e565b80546001600160a01b0319168155905061068c82610810565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930090565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0090565b604080516001600160a01b038416602482015260448082018490528251808303909101815260649091019091526020810180516001600160e01b031663a9059cbb60e01b1790526107b490849061086c565b505050565b6107c16108c6565b61043457604051631afcd79f60e31b815260040160405180910390fd5b6107e66107b9565b6001600160a01b038116610472576000604051631e4fbdf760e01b81526004016104699190610a17565b600061081a61071a565b80546001600160a01b038481166001600160a01b031983168117845560405193945091169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a3505050565b60006108816001600160a01b038416836108e0565b905080516000141580156108a65750808060200190518101906108a49190610b4b565b155b156107b45782604051635274afe760e01b81526004016104699190610a17565b60006108d06106c2565b54600160401b900460ff16919050565b606061058f8383600084600080856001600160a01b031684866040516109069190610b6d565b60006040518083038185875af1925050503d8060008114610943576040519150601f19603f3d011682016040523d82523d6000602084013e610948565b606091505b5091509150610958868383610964565b925050505b9392505050565b60608261097957610974826109b7565b61095d565b815115801561099057506001600160a01b0384163b155b156109b05783604051639996b31560e01b81526004016104699190610a17565b508061095d565b8051156109c75780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b80356001600160a01b03811681146109f757600080fd5b919050565b600060208284031215610a0e57600080fd5b61058f826109e0565b6001600160a01b0391909116815260200190565b60008060408385031215610a3e57600080fd5b610a47836109e0565b9150610a55602084016109e0565b90509250929050565b6001600160401b0391909116815260200190565b60008060408385031215610a8557600080fd5b50508035926020909101359150565b634e487b7160e01b600052601160045260246000fd5b8181038181111561059257610592610a94565b600060208284031215610acf57600080fd5b5051919050565b8082018082111561059257610592610a94565b6001600160401b038181168382160290811690818114610b0b57610b0b610a94565b5092915050565b808202811582820484141761059257610592610a94565b600082610b4657634e487b7160e01b600052601260045260246000fd5b500490565b600060208284031215610b5d57600080fd5b8151801515811461095d57600080fd5b6000825160005b81811015610b8e5760208186018101518583015201610b74565b50600092019182525091905056fea164736f6c634300081a000a", + "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100e65760003560e01c80631357e1dc146100eb5780631c31f7101461010757806332f7c3841461011c57806338af3eed14610124578063485cc95514610144578063715018a61461015757806379ba50971461015f5780637b0a47ee146101675780638da5cb5b1461016f578063ab1a4a2f14610177578063ab5fcdc91461018c578063c3aca55814610195578063ca598087146101a8578063d66fcb69146101b0578063e1f1c4a7146101c3578063e30c3978146101cc578063f2fde38b146101d4578063f85bb0f0146101e7575b600080fd5b6100f460025481565b6040519081526020015b60405180910390f35b61011a6101153660046109fc565b6101ef565b005b6100f461024d565b600154610137906001600160a01b031681565b6040516100fe9190610a17565b61011a610152366004610a2b565b6102ef565b61011a610422565b61011a610436565b6100f461047e565b6101376104b2565b61017f609781565b6040516100fe9190610a5e565b6100f460035481565b600054610137906001600160a01b031681565b6100f46104cd565b6100f46101be366004610a72565b610554565b61017f6103e881565b610137610598565b61011a6101e23660046109fc565b6105a3565b61011a610614565b6101f7610690565b600180546001600160a01b0319166001600160a01b0383161790556040517feee59a71c694e68368a1cb0d135c448051bbfb12289e6c2223b0ceb100c2321d90610242908390610a17565b60405180910390a150565b6000806003544261025e9190610aaa565b6000546040516370a0823160e01b81529192506102dc916001600160a01b03909116906370a0823190610295903090600401610a17565b602060405180830381865afa1580156102b2573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906102d69190610abd565b82610554565b6002546102e99190610ad6565b91505090565b60006102f96106c2565b805490915060ff600160401b82041615906001600160401b03166000811580156103205750825b90506000826001600160401b0316600114801561033c5750303b155b90508115801561034a575080155b156103685760405163f92ee8a960e01b815260040160405180910390fd5b84546001600160401b0319166001178555831561039157845460ff60401b1916600160401b1785555b600180546001600160a01b03808a166001600160a01b0319928316179092554260035560008054928916929091169190911790556103ce336106e6565b831561041957845460ff60401b191685556040517fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29061041090600190610a5e565b60405180910390a15b50505050505050565b61042a610690565b61043460006106f7565b565b3380610440610598565b6001600160a01b031614610472578060405163118cdaa760e01b81526004016104699190610a17565b60405180910390fd5b61047b816106f7565b50565b60008061048961024d565b905060006104956104cd565b90506104ab6104a48383610aaa565b6078610554565b9250505090565b6000806104bd61071a565b546001600160a01b031692915050565b600254600080546040516370a0823160e01b81529192916001600160a01b03909116906370a0823190610504903090600401610a17565b602060405180830381865afa158015610521573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906105459190610abd565b61054f9190610ad6565b905090565b60006105666103e86301e13380610ae9565b6001600160401b03168261057b609786610b12565b6105859190610b12565b61058f9190610b29565b90505b92915050565b6000806104bd61073e565b6105ab610690565b60006105b561073e565b80546001600160a01b0319166001600160a01b03841690811782559091506105db6104b2565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b600061061e61024d565b90506000600254826106309190610aaa565b6002839055426003556040518181529091507f952b264c8e0a06cddb4bbaa6d6af1d565145329fd95bbe72cb2b53942b2dc9669060200160405180910390a160015460005461068c916001600160a01b03918216911683610762565b5050565b336106996104b2565b6001600160a01b031614610434573360405163118cdaa760e01b81526004016104699190610a17565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0090565b6106ee6107b9565b61047b816107de565b600061070161073e565b80546001600160a01b0319168155905061068c82610810565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930090565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0090565b604080516001600160a01b038416602482015260448082018490528251808303909101815260649091019091526020810180516001600160e01b031663a9059cbb60e01b1790526107b490849061086c565b505050565b6107c16108c6565b61043457604051631afcd79f60e31b815260040160405180910390fd5b6107e66107b9565b6001600160a01b038116610472576000604051631e4fbdf760e01b81526004016104699190610a17565b600061081a61071a565b80546001600160a01b038481166001600160a01b031983168117845560405193945091169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a3505050565b60006108816001600160a01b038416836108e0565b905080516000141580156108a65750808060200190518101906108a49190610b4b565b155b156107b45782604051635274afe760e01b81526004016104699190610a17565b60006108d06106c2565b54600160401b900460ff16919050565b606061058f8383600084600080856001600160a01b031684866040516109069190610b6d565b60006040518083038185875af1925050503d8060008114610943576040519150601f19603f3d011682016040523d82523d6000602084013e610948565b606091505b5091509150610958868383610964565b925050505b9392505050565b60608261097957610974826109b7565b61095d565b815115801561099057506001600160a01b0384163b155b156109b05783604051639996b31560e01b81526004016104699190610a17565b508061095d565b8051156109c75780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b80356001600160a01b03811681146109f757600080fd5b919050565b600060208284031215610a0e57600080fd5b61058f826109e0565b6001600160a01b0391909116815260200190565b60008060408385031215610a3e57600080fd5b610a47836109e0565b9150610a55602084016109e0565b90509250929050565b6001600160401b0391909116815260200190565b60008060408385031215610a8557600080fd5b50508035926020909101359150565b634e487b7160e01b600052601160045260246000fd5b8181038181111561059257610592610a94565b600060208284031215610acf57600080fd5b5051919050565b8082018082111561059257610592610a94565b6001600160401b038181168382160290811690818114610b0b57610b0b610a94565b5092915050565b808202811582820484141761059257610592610a94565b600082610b4657634e487b7160e01b600052601260045260246000fd5b500490565b600060208284031215610b5d57600080fd5b8151801515811461095d57600080fd5b6000825160005b81811015610b8e5760208186018101518583015201610b74565b50600092019182525091905056fea164736f6c634300081a000a", "linkReferences": {}, "deployedLinkReferences": {} } diff --git a/src/web3client/abis/ServiceNodeContribution.json b/src/web3client/abis/ServiceNodeContribution.json index b2ff513..9abc20c 100644 --- a/src/web3client/abis/ServiceNodeContribution.json +++ b/src/web3client/abis/ServiceNodeContribution.json @@ -661,7 +661,7 @@ }, { "inputs": [], - "name": "SENT", + "name": "SESH", "outputs": [ { "internalType": "contract IERC20", @@ -1542,8 +1542,8 @@ "type": "function" } ], - "bytecode": "0x610120604052600f805460ff1916905534801561001b57600080fd5b5060405161543d38038061543d83398101604081905261003a9161196c565b866001600160a01b0381166100a45760405162461bcd60e51b815260206004820152602560248201527f5368617265643a205a65726f2d61646472657373206973206e6f74207065726d6044820152641a5d1d195960da1b60648201526084015b60405180910390fd5b86806000036100f55760405162461bcd60e51b815260206004820152601b60248201527f5368617265643a2075696e7420696e70757420697320656d7074790000000000604482015260640161009b565b6001600160a01b03891660a081905260408051630ada733d60e31b815290516356d399e8916004808201926020929091908290030181865afa15801561013f573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101639190611a30565b60c0818152505060a0516001600160a01b0316637c89d2f06040518163ffffffff1660e01b8152600401602060405180830381865afa1580156101aa573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101ce9190611a49565b6001600160a01b03166080526101008890523260e05260006101f588888888888680610204565b50505050505050505050611c7f565b61020c61026c565b845160208601516040870151610225928a928a92610398565b6060850151610233906104ad565b61023c84610537565b600f805461ff0019166101008515150217905580156102635760e051610263908383610859565b50505050505050565b600c5460005b81811015610334576000600c828154811061028f5761028f611a66565b600091825260208083206002909202909101546001600160a01b0316808352600a9091526040909120549091506003600f5460ff1660038111156102d5576102d5611751565b141580156102e35750600081115b156102ff576080516102ff906001600160a01b03168383610e71565b61032a826001600160a01b03166000908152600a60209081526040808320839055600b909152812055565b5050600101610272565b50610341600c60006116cd565b600f805460ff191690556040805160008082526020820190925281610388565b60408051808201909152600080825260208201528152602001906001900390816103615790505b50905061039481610537565b5050565b6000600f5460ff1660038111156103b1576103b1611751565b146103d657600f54604051639a0293fd60e01b815261009b9160ff1690600401611a7c565b60a05160e0516040805160016226579360e01b03198152885160048201526020808a01516024830152885160448301528801516064820152908701516084820152606087015160a48201526001600160a01b0391821660c482015260e4810186905291169063ffd9a86d9061010401600060405180830381600087803b15801561045f57600080fd5b505af1158015610473573d6000803e3d6000fd5b5050865160005550506020948501516001558351600655938301516007556040830151600855606090920151600955600255600355600455565b6000600f5460ff1660038111156104c6576104c6611751565b146104eb57600f5460405163ad88fc8f60e01b815261009b9160ff1690600401611a7c565b61271061ffff8216111561051f57604051624ae8fd60e41b815261ffff82166004820152612710602482015260440161009b565b6005805461ffff191661ffff92909216919091179055565b6000600f5460ff16600381111561055057610550611751565b1461057557600f546040516312ba63f960e11b815261009b9160ff1690600401611a7c565b600e5460005b818110156105d457600d6000600e838154811061059a5761059a611a66565b60009182526020808320909101546001600160a01b0316835282019290925260400181209081556001908101805460ff191690550161057b565b506105e1600e60006116ee565b5060c051610100518251111561061957815161010051604051630962235760e31b81526004810192909252602482015260440161009b565b815160005b8181101561085357806000036106955760e0516001600160a01b031684828151811061064c5761064c611a66565b6020026020010151600001516001600160a01b0316146106955760e05160405163ef77b46760e01b8152600481018390526001600160a01b03909116602482015260440161009b565b60006001600160a01b03168482815181106106b2576106b2611a66565b6020026020010151600001516001600160a01b0316036106e85760405163621caef560e11b81526004810182905260240161009b565b6000600d600086848151811061070057610700611a66565b6020026020010151600001516001600160a01b03166001600160a01b0316815260200190815260200160002090508060000154600014610756576040516308308c2360e21b81526004810183905260240161009b565b600061076c858461010051610ece60201b60201c565b9050600086848151811061078257610782611a66565b6020026020010151602001519050818110156107b75783818360405163c4aa8aa360e01b815260040161009b93929190611aa4565b808610156107de57838187604051636d3ae3a760e01b815260040161009b93929190611aa4565b6107e88187611ad0565b9550600e8785815181106107fe576107fe611a66565b6020908102919091018101515182546001808201855560009485529290932090920180546001600160a01b0319166001600160a01b0390931692909217909155908355918201805460ff19169055500161061e565b50505050565b6000600f5460ff16600381111561087257610872611751565b1415801561089757506001600f5460ff16600381111561089457610894611751565b14155b156108bc57600f54604051630295f95f60e51b815261009b9160ff1690600401611a7c565b60a0516001600160a01b03166386fa50636040518163ffffffff1660e01b8152600401602060405180830381865afa1580156108fc573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109209190611a30565b61010051146109b3576101005160a0516001600160a01b03166386fa50636040518163ffffffff1660e01b8152600401602060405180830381865afa15801561096d573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109919190611a30565b604051631248081b60e31b81526004810192909252602482015260440161009b565b60a0516001600160a01b03166356d399e86040518163ffffffff1660e01b8152600401602060405180830381865afa1580156109f3573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610a179190611a30565b60c05114610a625760c05160a0516001600160a01b03166356d399e86040518163ffffffff1660e01b8152600401602060405180830381865afa15801561096d573d6000803e3d6000fd5b6000600f5460ff166003811115610a7b57610a7b611751565b03610b145760e0516001600160a01b0316836001600160a01b031614610ab95760e0516040516316a4ebc160e21b815261009b918591600401611ae3565b600f805460ff1916600117905560e05160025460055460405161ffff90911681526001600160a01b03909216917fe52a01bb4ffcc3baf2c5d17111d7d5b676a7485809eb10671092a89d2bb22ff69060200160405180910390a35b6001600160a01b0383166000908152600d60205260409020805415801590610b415750600181015460ff16155b15610b8a578054821015610b7557805460405163454cd8e160e01b815261009b918491600401918252602082015260400190565b6001818101805460ff19169091179055610be6565b6001600160a01b0384166000908152600a6020526040902054158015610bb65750610bb3610f65565b82105b15610be65781610bc4610f65565b6040516310e45ff160e21b81526004810192909252602482015260440161009b565b6001600160a01b0384166000908152600a60205260408120549003610cb7576000610c11858561102b565b604080518082019091526001600160a01b03808816825291821660208201908152600c8054600181018255600091909152915160029092027fdf6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c7810180549385166001600160a01b031994851617905590517fdf6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c8909101805491909316911617905550610cc1565b610cc18484611049565b6001600160a01b0384166000908152600a602052604081208054849290610ce9908490611afd565b90915550506001600160a01b0384166000908152600b60205260408120429055610d116111ca565b90506000610d1d611236565b60c051909150610d2d8284611afd565b1115610d5457818160c05160405163513b428b60e11b815260040161009b93929190611aa4565b61010051600c541115610d8157610100516040516315b5c3d560e21b815260040161009b91815260200190565b60c0518203610dd35760e0516002546040516001600160a01b03909216917f48996614f3380f3dcd5a2d2e6eaddfd66207ff3457fee339e09b4735bc9ec95890600090a3600f805460ff191660021790555b856001600160a01b03167fbdaa686eb6f59012d211a74523da260341c516896e9e5be954163d6ecf26ffa285604051610e0e91815260200190565b60405180910390a2608051610e2e906001600160a01b03168730876112a8565b6002600f5460ff166003811115610e4757610e47611751565b148015610e5c5750600f54610100900460ff16155b15610e6957610e696112e1565b505050505050565b610ec983846001600160a01b031663a9059cbb8585604051602401610e97929190611b10565b60408051808303601f1901815291905260208101805160e09390931b6001600160e01b03938416179052915061154216565b505050565b6000828211610efa576040516376675ed960e11b8152600481018490526024810183905260440161009b565b82600003610f2b576004610f0f600186611ad0565b610f199190611b29565b610f24906001611afd565b9050610f5e565b6000610f378484611ad0565b905080610f45600187611ad0565b610f4f9190611b29565b610f5a906001611afd565b9150505b9392505050565b600e5460009081908190815b81811015610fe8576000600e8281548110610f8e57610f8e611a66565b60009182526020808320909101546001600160a01b0316808352600d909152604090912060018101549192509060ff16610fde578054610fce9087611afd565b9550610fdb600186611afd565b94505b5050600101610f71565b5061102383610ff56111ca565b60c0516110029190611ad0565b61100c9190611ad0565b600c5461101a908590611afd565b61010051610ece565b935050505090565b60006001600160a01b038216156110425781610f5e565b5090919050565b6001600f5460ff16600381111561106257611062611751565b1415801561108757506002600f5460ff16600381111561108457611084611751565b14155b156110ac576002546040516310004b0160e11b8152600481019190915260240161009b565b60006110b8838361102b565b600c549091506000908190815b8181101561115e576000600c82815481106110e2576110e2611a66565b6000918252602090912060029091020180549091506001600160a01b03808a169116036111555760018101546001600160a01b0380881691160361112a575050505050505050565b600190810180546001600160a01b038881166001600160a01b0319831617909255169450925061115e565b506001016110c5565b508161117f5785604051632fd768d360e11b815260040161009b9190611b4b565b856001600160a01b03167f25639ce407c98fba722dddf1f023c8242e3c77732cfe4660d1c04930bf4cbe7284866040516111ba929190611ae3565b60405180910390a2505050505050565b600c54600090815b81811015611231576000600c82815481106111ef576111ef611a66565b600091825260208083206002909202909101546001600160a01b0316808352600a9091526040909120549091506112269085611afd565b9350506001016111d2565b505090565b600e54600090815b81811015611231576000600e828154811061125b5761125b611a66565b60009182526020808320909101546001600160a01b0316808352600d909152604090912060018101549192509060ff1661129e57805461129b9086611afd565b94505b505060010161123e565b6040516001600160a01b0384811660248301528381166044830152606482018390526108539186918216906323b872dd90608401610e97565b6002600f5460ff1660038111156112fa576112fa611751565b1461131f57600f546040516383696f6f60e01b815261009b9160ff1690600401611a7c565b600f805460ff191660031790556002546040517f839cf22e1ba87ce2f5b9bbf46cf0175a09eed52febdfaac8852478e68203c76390600090a2600c546000816001600160401b038111156113755761137561177c565b6040519080825280602002602001820160405280156113c957816020015b60408051608081018252600091810182815260608201839052815260208101919091528152602001906001900390816113935790505b50905060005b82811015611464576000600c82815481106113ec576113ec611a66565b6000918252602080832060408051608081018252600290940290910180546001600160a01b03908116858401818152600184015490921660608701529085528552600a83529320549082015284519192509084908490811061145057611450611a66565b6020908102919091010152506001016113cf565b506080516001600160a01b031663095ea7b360a05160c0516040518363ffffffff1660e01b8152600401611499929190611b10565b6020604051808303816000875af11580156114b8573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906114dc9190611b5f565b5060a0516001600160a01b031663bc7efecc600060066002856040518563ffffffff1660e01b81526004016115149493929190611bd9565b600060405180830381600087803b15801561152e57600080fd5b505af1158015610e69573d6000803e3d6000fd5b60006115576001600160a01b0384168361159c565b9050805160001415801561157c57508080602001905181019061157a9190611b5f565b155b15610ec95782604051635274afe760e01b815260040161009b9190611b4b565b6060610f5e838360006115b0565b92915050565b6060814710156115d5573060405163cd78605960e01b815260040161009b9190611b4b565b600080856001600160a01b031684866040516115f19190611c50565b60006040518083038185875af1925050503d806000811461162e576040519150601f19603f3d011682016040523d82523d6000602084013e611633565b606091505b50909250905061164486838361164e565b9695505050505050565b6060826116635761165e826116a1565b610f5e565b815115801561167a57506001600160a01b0384163b155b1561169a5783604051639996b31560e01b815260040161009b9190611b4b565b5080610f5e565b8051156116b15780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b50565b50805460008255600202906000526020600020908101906116ca919061170c565b50805460008255906000526020600020908101906116ca919061173c565b5b808211156117385780546001600160a01b03199081168255600182018054909116905560020161170d565b5090565b5b80821115611738576000815560010161173d565b634e487b7160e01b600052602160045260246000fd5b6001600160a01b03811681146116ca57600080fd5b634e487b7160e01b600052604160045260246000fd5b604051608081016001600160401b03811182821017156117b4576117b461177c565b60405290565b604080519081016001600160401b03811182821017156117b4576117b461177c565b604051601f8201601f191681016001600160401b03811182821017156118045761180461177c565b604052919050565b60006080828403121561181e57600080fd5b611826611792565b8251815260208084015190820152604080840151908201526060928301519281019290925250919050565b60006080828403121561186357600080fd5b61186b611792565b825181526020808401519082015260408084015190820152606083015190915061ffff8116811461189b57600080fd5b606082015292915050565b600082601f8301126118b757600080fd5b81516001600160401b038111156118d0576118d061177c565b6118df60208260051b016117dc565b8082825260208201915060208360061b86010192508583111561190157600080fd5b602085015b8381101561194d576040818803121561191e57600080fd5b6119266117ba565b815161193181611767565b8152602082810151818301529084529290920191604001611906565b5095945050505050565b8051801515811461196757600080fd5b919050565b60008060008060008060008789036101c081121561198957600080fd5b885161199481611767565b60208a015190985096506040603f19820112156119b057600080fd5b506119b96117ba565b604089015181526060890151602082015294506119d98960808a0161180c565b93506119e9896101008a01611851565b6101808901519093506001600160401b03811115611a0657600080fd5b611a128a828b016118a6565b925050611a226101a08901611957565b905092959891949750929550565b600060208284031215611a4257600080fd5b5051919050565b600060208284031215611a5b57600080fd5b8151610f5e81611767565b634e487b7160e01b600052603260045260246000fd5b6020810160048310611a9e57634e487b7160e01b600052602160045260246000fd5b91905290565b9283526020830191909152604082015260600190565b634e487b7160e01b600052601160045260246000fd5b818103818111156115aa576115aa611aba565b6001600160a01b0392831681529116602082015260400190565b808201808211156115aa576115aa611aba565b6001600160a01b03929092168252602082015260400190565b600082611b4657634e487b7160e01b600052601260045260246000fd5b500490565b6001600160a01b0391909116815260200190565b600060208284031215611b7157600080fd5b610f5e82611957565b600081518084526020840193506020830160005b82811015611bcf578151805180516001600160a01b039081168952602091820151168189015290810151604088015260609096019590910190600101611b8e565b5093949350505050565b8454815260018501546020820152835460408201526001840154606082015260028401546080820152600384015460a0820152825460c0820152600183015460e0820152600283015461010082015261ffff6003840154166101208201526101606101408201526000611644610160830184611b7a565b6000825160005b81811015611c715760208186018101518583015201611c57565b506000920191825250919050565b60805160a05160c05160e051610100516135f2611e4b6000396000818161052c01528181610f36015281816115b4015281816115dd0152818161179701528181611e3001528181611e56015281816123b501526123e90152600081816104660152818161064b01528181610673015281816106d601528181610a2801528181610a5001528181610afe01528181610b2601528181610bcb01528181610bf301528181610c5b01528181610c9301528181610cbb01528181610d0d01528181610d350152818161104b01528181611073015281816111b5015281816112140152818161123c01528181611469015281816114910152818161162e015281816116a00152818161205901528181612093015281816120f50152818161244c015281816129480152612a7d01526000818161043f01528181610ef80152818161159201528181611f9601528181611fbc01528181612342015281816123750152818161241701526127b601526000818161034a01528181611dae01528181611e7701528181611f1401528181611fdd015281816127940152818161283901526129170152600081816104a001528181611ab201528181611cef015281816124f2015261276701526135f26000f3fe608060405234801561001057600080fd5b50600436106101e25760003560e01c8062e9a407146101e75780630aaffd2a146101fc5780630d616d201461020f5780630d9639ba146102175780630dcf4b8f146102375780630ebb172a1461024d57806312fa329b1461026f578063200d2ed2146102905780632816ee73146102aa5780632c6cda93146102bd5780632c7baaf3146102d0578063313f336b1461030f5780633362379414610345578063356c299d1461037957806342e94c90146103b75780634564168b146103d75780634a387cdf146103f35780634bb278f3146103fb5780634e7320ce14610403578063565a9d481461041857806356d399e81461043a578063570ca7351461046157806358b810d31461048857806360e52ecb1461049b5780636786452c146104c25780636cf72aa4146104d55780637c0b4bfb146104e857806386fa5063146105275780638c7e7a311461054e5780638dd0855714610561578063937e09b11461058157806396a608c0146105895780639ca002a614610591578063a4b6aa83146105a4578063bc063e1a146105ac578063ccec3716146105c8578063d26146a5146105db578063d826f88f146105ee578063dda0a81a146105f6578063e020e4c41461060d578063e50d50d814610620575b600080fd5b6101fa6101f5366004612e8f565b610640565b005b6101fa61020a366004612ecb565b6106c2565b6101fa6106cc565b61021f6107b7565b60405161022e93929190612f5d565b60405180910390f35b61023f61096d565b60405190815260200161022e565b6102576201518081565b6040516001600160401b03909116815260200161022e565b61028261027d366004612fca565b6109d9565b60405161022e929190612fe3565b600f5461029d9060ff1681565b60405161022e9190613013565b6101fa6102b836600461303b565b610a12565b6101fa6102cb366004613079565b610a1d565b6102d8610a93565b60405161022e919081518152602080830151908201526040808301519082015260609182015161ffff169181019190915260800190565b6006546007546008546009546103259392919084565b60408051948552602085019390935291830152606082015260800161022e565b61036c7f000000000000000000000000000000000000000000000000000000000000000081565b60405161022e9190613094565b6002546003546004546005546103939392919061ffff1684565b6040805194855260208501939093529183015261ffff16606082015260800161022e565b61023f6103c5366004612ecb565b600a6020526000908152604090205481565b6000546001546103e5919082565b60405161022e9291906130a8565b600c5461023f565b6101fa610af3565b61040b610b68565b60405161022e91906130dc565b600f5461042a90610100900460ff1681565b604051901515815260200161022e565b61023f7f000000000000000000000000000000000000000000000000000000000000000081565b61036c7f000000000000000000000000000000000000000000000000000000000000000081565b6101fa610496366004613103565b610bc0565b61036c7f000000000000000000000000000000000000000000000000000000000000000081565b6101fa6104d03660046131f1565b610c88565b6101fa6104e3366004613242565b610d02565b6105126104f6366004612ecb565b600d602052600090815260409020805460019091015460ff1682565b6040805192835290151560208301520161022e565b61023f7f000000000000000000000000000000000000000000000000000000000000000081565b61023f61055c36600461325f565b610d78565b61057461056f366004612fca565b610e09565b60405161022e91906132a6565b61023f610e62565b61023f610f62565b61023f61059f366004612fca565b610fb9565b61023f610fce565b6105b561271081565b60405161ffff909116815260200161022e565b6101fa6105d6366004612ecb565b611040565b61036c6105e9366004612fca565b6111df565b6101fa611209565b6105fe61127e565b60405161022e939291906132b4565b6101fa61061b3660046132ed565b61145e565b61023f61062e366004612ecb565b600b6020526000908152604090205481565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146106b657337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b60405180910390fd5b6106bf816114e3565b50565b6106bf33826118a8565b6001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016330361070657610704611a29565b565b336000908152600b602052604081205461072090426133e4565b90506201518081101561076a57336000908152600b602052604090819020549051630429b06960e31b815260048101919091524260248201526201518060448201526064016106ad565b600061077533611b4c565b905080156107b35760405181815233907f249a82fbe5056a1940b4e996665ba2a82c340ae9fa1e069fd1ababf5508f396e9060200160405180910390a25b5050565b600e5460609081908190806001600160401b038111156107d9576107d9612d34565b604051908082528060200260200182016040528015610802578160200160208202803683370190505b509350806001600160401b0381111561081d5761081d612d34565b604051908082528060200260200182016040528015610846578160200160208202803683370190505b509250806001600160401b0381111561086157610861612d34565b60405190808252806020026020018201604052801561088a578160200160208202803683370190505b50915060005b81811015610966576000600e82815481106108ad576108ad6133f7565b60009182526020808320909101546001600160a01b0316808352600d909152604090912087519192509082908890859081106108eb576108eb6133f7565b60200260200101906001600160a01b031690816001600160a01b0316815250508060000154868481518110610922576109226133f7565b60209081029190910101526001810154855160ff9091169086908590811061094c5761094c6133f7565b911515602092830291909101909101525050600101610890565b5050909192565b600c54600090815b818110156109d4576000600c8281548110610992576109926133f7565b600091825260208083206002909202909101546001600160a01b0316808352600a9091526040909120549091506109c9908561340d565b935050600101610975565b505090565b600c81815481106109e957600080fd5b6000918252602090912060029091020180546001909101546001600160a01b0391821692501682565b6107b3338284611d49565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610a8a57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6106bf8161255d565b610ac26040518060800160405280600081526020016000815260200160008152602001600061ffff1681525090565b5060408051608081018252600254815260035460208201526004549181019190915260055461ffff16606082015290565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610b6057337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6107046125e7565b610b936040518060800160405280600081526020016000815260200160008152602001600081525090565b50604080516080810182526006548152600754602082015260085491810191909152600954606082015290565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610c2d57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b610c35611a29565b610c3e8561255d565b610c47846114e3565b610c50836128a5565b8015610c8157610c817f00000000000000000000000000000000000000000000000000000000000000008383611d49565b5050505050565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610cf557337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b610c8185858585856128bf565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610d6f57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6106bf816128a5565b6000828211610d9e5782826040516376675ed960e11b81526004016106ad9291906130a8565b82600003610dcf576004610db36001866133e4565b610dbd9190613420565b610dc890600161340d565b9050610e02565b6000610ddb84846133e4565b905080610de96001876133e4565b610df39190613420565b610dfe90600161340d565b9150505b9392505050565b610e11612c79565b600c8281548110610e2457610e246133f7565b60009182526020918290206040805180820190915260029092020180546001600160a01b039081168352600190910154169181019190915292915050565b600e5460009081908190815b81811015610ee5576000600e8281548110610e8b57610e8b6133f7565b60009182526020808320909101546001600160a01b0316808352600d909152604090912060018101549192509060ff16610edb578054610ecb908761340d565b9550610ed860018661340d565b94505b5050600101610e6e565b50610f5a83610ef261096d565b610f1c907f00000000000000000000000000000000000000000000000000000000000000006133e4565b610f2691906133e4565b600c54610f3490859061340d565b7f0000000000000000000000000000000000000000000000000000000000000000610d78565b935050505090565b600c54600090610f725750600090565b600a6000600c600081548110610f8a57610f8a6133f7565b600091825260208083206002909202909101546001600160a01b03168352820192909252604001902054905090565b6000610fc88260006001610d78565b92915050565b600e54600090815b818110156109d4576000600e8281548110610ff357610ff36133f7565b60009182526020808320909101546001600160a01b0316808352600d909152604090912060018101549192509060ff16611036578054611033908661340d565b94505b5050600101610fd6565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146110ad57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6003600f5460ff1660038111156110c6576110c6612ffd565b141580156110ea57506000600f5460ff1660038111156110e8576110e8612ffd565b145b1561110f57600f54604051632f3e69dd60e01b81526106ad9160ff1690600401613013565b6040516370a0823160e01b815281906000906001600160a01b038316906370a0823190611140903090600401613094565b602060405180830381865afa15801561115d573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906111819190613442565b9050600081116111a6578260405163e932c57360e01b81526004016106ad9190613094565b6111da6001600160a01b0383167f0000000000000000000000000000000000000000000000000000000000000000836129da565b505050565b600e81815481106111ef57600080fd5b6000918252602090912001546001600160a01b0316905081565b336001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000161461127657337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b610704611a29565b600c5460609081908190806001600160401b038111156112a0576112a0612d34565b6040519080825280602002602001820160405280156112c9578160200160208202803683370190505b509350806001600160401b038111156112e4576112e4612d34565b60405190808252806020026020018201604052801561130d578160200160208202803683370190505b509250806001600160401b0381111561132857611328612d34565b604051908082528060200260200182016040528015611351578160200160208202803683370190505b50915060005b81811015610966576000600c8281548110611374576113746133f7565b60009182526020909120600290910201805487519192506001600160a01b0316908790849081106113a7576113a76133f7565b6001600160a01b039283166020918202929092010152600182015486519116908690849081106113d9576113d96133f7565b60200260200101906001600160a01b031690816001600160a01b031681525050600a600087848151811061140f5761140f6133f7565b60200260200101516001600160a01b03166001600160a01b031681526020019081526020016000205484838151811061144a5761144a6133f7565b602090810291909101015250600101611357565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146114cb57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6114da87878787878787612a32565b50505050505050565b6000600f5460ff1660038111156114fc576114fc612ffd565b1461152157600f546040516312ba63f960e11b81526106ad9160ff1690600401613013565b600e5460005b8181101561158057600d6000600e8381548110611546576115466133f7565b60009182526020808320909101546001600160a01b0316835282019290925260400181209081556001908101805460ff1916905501611527565b5061158d600e6000612c90565b5080517f0000000000000000000000000000000000000000000000000000000000000000907f000000000000000000000000000000000000000000000000000000000000000010156116175781517f0000000000000000000000000000000000000000000000000000000000000000604051630962235760e31b81526004016106ad9291906130a8565b815160005b818110156118a257806000036116cd577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316848281518110611668576116686133f7565b6020026020010151600001516001600160a01b0316146116cd5760405163ef77b46760e01b8152600481018290526001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001660248201526044016106ad565b60006001600160a01b03168482815181106116ea576116ea6133f7565b6020026020010151600001516001600160a01b0316036117205760405163621caef560e11b8152600481018290526024016106ad565b6000600d6000868481518110611738576117386133f7565b6020026020010151600001516001600160a01b03166001600160a01b031681526020019081526020016000209050806000015460001461178e576040516308308c2360e21b8152600481018390526024016106ad565b60006117bb85847f0000000000000000000000000000000000000000000000000000000000000000610d78565b905060008684815181106117d1576117d16133f7565b6020026020010151602001519050818110156118065783818360405163c4aa8aa360e01b81526004016106ad9392919061345b565b8086101561182d57838187604051636d3ae3a760e01b81526004016106ad9392919061345b565b61183781876133e4565b9550600e87858151811061184d5761184d6133f7565b6020908102919091018101515182546001808201855560009485529290932090920180546001600160a01b0319166001600160a01b0390931692909217909155908355918201805460ff19169055500161161c565b50505050565b6001600f5460ff1660038111156118c1576118c1612ffd565b141580156118e657506002600f5460ff1660038111156118e3576118e3612ffd565b14155b1561190b576002546040516310004b0160e11b815260048101919091526024016106ad565b60006119178383612aa3565b600c549091506000908190815b818110156119bd576000600c8281548110611941576119416133f7565b6000918252602090912060029091020180549091506001600160a01b03808a169116036119b45760018101546001600160a01b03808816911603611989575050505050505050565b600190810180546001600160a01b038881166001600160a01b031983161790925516945092506119bd565b50600101611924565b50816119de5785604051632fd768d360e11b81526004016106ad9190613094565b856001600160a01b03167f25639ce407c98fba722dddf1f023c8242e3c77732cfe4660d1c04930bf4cbe728486604051611a19929190612fe3565b60405180910390a2505050505050565b600c5460005b81811015611aec576000600c8281548110611a4c57611a4c6133f7565b600091825260208083206002909202909101546001600160a01b0316808352600a9091526040909120549091506003600f5460ff166003811115611a9257611a92612ffd565b14158015611aa05750600081115b15611ad957611ad96001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001683836129da565b611ae282612ac1565b5050600101611a2f565b50611af9600c6000612cae565b600f805460ff191690556040805160008082526020820190925281611b40565b6040805180820190915260008082526020820152815260200190600190039081611b195790505b5090506107b3816114e3565b6001600160a01b0381166000908152600a602052604081205490819003611b7257919050565b611b7b82612ac1565b600c5460005b81811015611c9657600c8181548110611b9c57611b9c6133f7565b60009182526020909120600290910201546001600160a01b0390811690851603611c8e57600c611bcd6001846133e4565b81548110611bdd57611bdd6133f7565b9060005260206000209060020201600c8281548110611bfe57611bfe6133f7565b60009182526020909120825460029092020180546001600160a01b039283166001600160a01b0319918216178255600193840154939091018054939092169216919091179055600c805480611c5557611c55613471565b60008281526020902060026000199092019182020180546001600160a01b03199081168255600191909101805490911690559055611c96565b600101611b81565b506001600160a01b0383166000908152600d6020526040902060018101805460ff191690556003600f5460ff166003811115611cd457611cd4612ffd565b03611ce25760009250611d42565b611d166001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001685856129da565b6002600f5460ff166003811115611d2f57611d2f612ffd565b03611d4257600f805460ff191660011790555b5050919050565b6000600f5460ff166003811115611d6257611d62612ffd565b14158015611d8757506001600f5460ff166003811115611d8457611d84612ffd565b14155b15611dac57600f54604051630295f95f60e51b81526106ad9160ff1690600401613013565b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166386fa50636040518163ffffffff1660e01b8152600401602060405180830381865afa158015611e0a573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611e2e9190613442565b7f000000000000000000000000000000000000000000000000000000000000000014611f12577f00000000000000000000000000000000000000000000000000000000000000007f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166386fa50636040518163ffffffff1660e01b8152600401602060405180830381865afa158015611ed3573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611ef79190613442565b604051631248081b60e31b81526004016106ad9291906130a8565b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166356d399e86040518163ffffffff1660e01b8152600401602060405180830381865afa158015611f70573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611f949190613442565b7f000000000000000000000000000000000000000000000000000000000000000014612039577f00000000000000000000000000000000000000000000000000000000000000007f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166356d399e86040518163ffffffff1660e01b8152600401602060405180830381865afa158015611ed3573d6000803e3d6000fd5b6000600f5460ff16600381111561205257612052612ffd565b03612145577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316836001600160a01b0316146120cd57827f00000000000000000000000000000000000000000000000000000000000000006040516316a4ebc160e21b81526004016106ad929190612fe3565b600f805460ff1916600117905560025460055460405161ffff90911681526001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001691907fe52a01bb4ffcc3baf2c5d17111d7d5b676a7485809eb10671092a89d2bb22ff69060200160405180910390a35b6001600160a01b0383166000908152600d602052604090208054158015906121725750600181015460ff16155b156121b257805482101561219d57805460405163454cd8e160e01b81526106ad9184916004016130a8565b6001818101805460ff19169091179055612207565b6001600160a01b0384166000908152600a60205260409020541580156121de57506121db610e62565b82105b1561220757816121ec610e62565b6040516310e45ff160e21b81526004016106ad9291906130a8565b6001600160a01b0384166000908152600a602052604081205490036122d85760006122328585612aa3565b604080518082019091526001600160a01b03808816825291821660208201908152600c8054600181018255600091909152915160029092027fdf6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c7810180549385166001600160a01b031994851617905590517fdf6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c89091018054919093169116179055506122e2565b6122e284846118a8565b6001600160a01b0384166000908152600a60205260408120805484929061230a90849061340d565b90915550506001600160a01b0384166000908152600b6020526040812042905561233261096d565b9050600061233e610fce565b90507f000000000000000000000000000000000000000000000000000000000000000061236b828461340d565b11156123b05781817f000000000000000000000000000000000000000000000000000000000000000060405163513b428b60e11b81526004016106ad9392919061345b565b600c547f00000000000000000000000000000000000000000000000000000000000000001015612415576040516315b5c3d560e21b81527f000000000000000000000000000000000000000000000000000000000000000060048201526024016106ad565b7f000000000000000000000000000000000000000000000000000000000000000082036124a2576002546040516001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001691907f48996614f3380f3dcd5a2d2e6eaddfd66207ff3457fee339e09b4735bc9ec95890600090a3600f805460ff191660021790555b856001600160a01b03167fbdaa686eb6f59012d211a74523da260341c516896e9e5be954163d6ecf26ffa2856040516124dd91815260200190565b60405180910390a261251a6001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016873087612ae8565b6002600f5460ff16600381111561253357612533612ffd565b1480156125485750600f54610100900460ff16155b15612555576125556125e7565b505050505050565b6000600f5460ff16600381111561257657612576612ffd565b1461259b57600f5460405163ad88fc8f60e01b81526106ad9160ff1690600401613013565b61271061ffff821611156125cf57604051624ae8fd60e41b815261ffff8216600482015261271060248201526044016106ad565b6005805461ffff191661ffff92909216919091179055565b6002600f5460ff16600381111561260057612600612ffd565b1461262557600f546040516383696f6f60e01b81526106ad9160ff1690600401613013565b600f805460ff191660031790556002546040517f839cf22e1ba87ce2f5b9bbf46cf0175a09eed52febdfaac8852478e68203c76390600090a2600c546000816001600160401b0381111561267b5761267b612d34565b6040519080825280602002602001820160405280156126b457816020015b6126a1612ccf565b8152602001906001900390816126995790505b50905060005b8281101561274f576000600c82815481106126d7576126d76133f7565b6000918252602080832060408051608081018252600290940290910180546001600160a01b03908116858401818152600184015490921660608701529085528552600a83529320549082015284519192509084908490811061273b5761273b6133f7565b6020908102919091010152506001016126ba565b5060405163095ea7b360e01b81526001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169063095ea7b3906127de907f0000000000000000000000000000000000000000000000000000000000000000907f000000000000000000000000000000000000000000000000000000000000000090600401613487565b6020604051808303816000875af11580156127fd573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061282191906134a0565b50604051632f1fbfb360e21b81526001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169063bc7efecc90612877906000906006906002908790600401613504565b600060405180830381600087803b15801561289157600080fd5b505af1158015612555573d6000803e3d6000fd5b600f80549115156101000261ff0019909216919091179055565b6000600f5460ff1660038111156128d8576128d8612ffd565b146128fd57600f54604051639a0293fd60e01b81526106ad9160ff1690600401613013565b60405160016226579360e01b031981526001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169063ffd9a86d9061297290889088907f000000000000000000000000000000000000000000000000000000000000000090899060040161357b565b600060405180830381600087803b15801561298c57600080fd5b505af11580156129a0573d6000803e3d6000fd5b5050865160005550506020948501516001558351600655938301516007556040830151600855606090920151600955600255600355600455565b6111da83846001600160a01b031663a9059cbb8585604051602401612a00929190613487565b604051602081830303815290604052915060e01b6020820180516001600160e01b038381831617835250505050612b21565b612a3a611a29565b612a5387878760000151886020015189604001516128bf565b612a60856060015161255d565b612a69846114e3565b612a72836128a5565b80156114da576114da7f00000000000000000000000000000000000000000000000000000000000000008383611d49565b60006001600160a01b03821615612aba5781610e02565b5090919050565b6001600160a01b03166000908152600a60209081526040808320839055600b909152812055565b6040516001600160a01b0384811660248301528381166044830152606482018390526118a29186918216906323b872dd90608401612a00565b6000612b366001600160a01b03841683612b7b565b90508051600014158015612b5b575080806020019051810190612b5991906134a0565b155b156111da5782604051635274afe760e01b81526004016106ad9190613094565b6060610e028383600084600080856001600160a01b03168486604051612ba191906135b6565b60006040518083038185875af1925050503d8060008114612bde576040519150601f19603f3d011682016040523d82523d6000602084013e612be3565b606091505b5091509150612bf3868383612bfd565b9695505050505050565b606082612c1257612c0d82612c50565b610e02565b8151158015612c2957506001600160a01b0384163b155b15612c495783604051639996b31560e01b81526004016106ad9190613094565b5080610e02565b805115612c605780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b604080518082019091526000808252602082015290565b50805460008255906000526020600020908101906106bf9190612cef565b50805460008255600202906000526020600020908101906106bf9190612d08565b6040518060400160405280612ce2612c79565b8152602001600081525090565b5b80821115612d045760008155600101612cf0565b5090565b5b80821115612d045780546001600160a01b031990811682556001820180549091169055600201612d09565b634e487b7160e01b600052604160045260246000fd5b604080519081016001600160401b0381118282101715612d6c57612d6c612d34565b60405290565b604051608081016001600160401b0381118282101715612d6c57612d6c612d34565b604051601f8201601f191681016001600160401b0381118282101715612dbc57612dbc612d34565b604052919050565b80356001600160a01b0381168114612ddb57600080fd5b919050565b600082601f830112612df157600080fd5b81356001600160401b03811115612e0a57612e0a612d34565b612e1960208260051b01612d94565b8082825260208201915060208360061b860101925085831115612e3b57600080fd5b602085015b83811015612e855760408188031215612e5857600080fd5b612e60612d4a565b612e6982612dc4565b8152602082810135818301529084529290920191604001612e40565b5095945050505050565b600060208284031215612ea157600080fd5b81356001600160401b03811115612eb757600080fd5b612ec384828501612de0565b949350505050565b600060208284031215612edd57600080fd5b610e0282612dc4565b600081518084526020840193506020830160005b82811015612f215781516001600160a01b0316865260209586019590910190600101612efa565b5093949350505050565b600081518084526020840193506020830160005b82811015612f21578151865260209586019590910190600101612f3f565b606081526000612f706060830186612ee6565b8281036020840152612f828186612f2b565b83810360408501528451808252602080870193509091019060005b81811015612fbd5783511515835260209384019390920191600101612f9d565b5090979650505050505050565b600060208284031215612fdc57600080fd5b5035919050565b6001600160a01b0392831681529116602082015260400190565b634e487b7160e01b600052602160045260246000fd5b602081016004831061303557634e487b7160e01b600052602160045260246000fd5b91905290565b6000806040838503121561304e57600080fd5b8235915061305e60208401612dc4565b90509250929050565b803561ffff81168114612ddb57600080fd5b60006020828403121561308b57600080fd5b610e0282613067565b6001600160a01b0391909116815260200190565b918252602082015260400190565b805182526020810151602083015260408101516040830152606081015160608301525050565b60808101610fc882846130b6565b80151581146106bf57600080fd5b8035612ddb816130ea565b600080600080600060a0868803121561311b57600080fd5b61312486613067565b945060208601356001600160401b0381111561313f57600080fd5b61314b88828901612de0565b945050604086013561315c816130ea565b925061316a60608701612dc4565b949793965091946080013592915050565b60006040828403121561318d57600080fd5b613195612d4a565b823581526020928301359281019290925250919050565b6000608082840312156131be57600080fd5b6131c6612d72565b8235815260208084013590820152604080840135908201526060928301359281019290925250919050565b6000806000806000610120868803121561320a57600080fd5b613214878761317b565b945061322387604088016131ac565b949794965050505060c08301359260e081013592610100909101359150565b60006020828403121561325457600080fd5b8135610e02816130ea565b60008060006060848603121561327457600080fd5b505081359360208301359350604090920135919050565b80516001600160a01b03908116835260209182015116910152565b60408101610fc8828461328b565b6060815260006132c76060830186612ee6565b82810360208401526132d98186612ee6565b90508281036040840152612bf38185612f2b565b60008060008060008060008789036101c081121561330a57600080fd5b6133148a8a61317b565b97506133238a60408b016131ac565b9650608060bf198201121561333757600080fd5b50613340612d72565b60c0890135815260e0890135602082015261010089013560408201526133696101208a01613067565b606082015294506101408801356001600160401b0381111561338a57600080fd5b6133968a828b01612de0565b9450506133a661016089016130f8565b92506133b56101808901612dc4565b9699959850939692959194919350506101a09091013590565b634e487b7160e01b600052601160045260246000fd5b81810381811115610fc857610fc86133ce565b634e487b7160e01b600052603260045260246000fd5b80820180821115610fc857610fc86133ce565b60008261343d57634e487b7160e01b600052601260045260246000fd5b500490565b60006020828403121561345457600080fd5b5051919050565b9283526020830191909152604082015260600190565b634e487b7160e01b600052603160045260246000fd5b6001600160a01b03929092168252602082015260400190565b6000602082840312156134b257600080fd5b8151610e02816130ea565b600081518084526020840193506020830160005b82811015612f215781516134e687825161328b565b602090810151604088015260609096019591909101906001016134d1565b8454815260018501546020820152835460408201526001840154606082015260028401546080820152600384015460a0820152825460c0820152600183015460e0820152600283015461010082015261ffff6003840154166101208201526101606101408201526000612bf36101608301846134bd565b8451815260208086015190820152610100810161359b60408301866130b6565b6001600160a01b039390931660c082015260e0015292915050565b6000825160005b818110156135d757602081860181015185830152016135bd565b50600092019182525091905056fea164736f6c634300081a000a", - "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106101e25760003560e01c8062e9a407146101e75780630aaffd2a146101fc5780630d616d201461020f5780630d9639ba146102175780630dcf4b8f146102375780630ebb172a1461024d57806312fa329b1461026f578063200d2ed2146102905780632816ee73146102aa5780632c6cda93146102bd5780632c7baaf3146102d0578063313f336b1461030f5780633362379414610345578063356c299d1461037957806342e94c90146103b75780634564168b146103d75780634a387cdf146103f35780634bb278f3146103fb5780634e7320ce14610403578063565a9d481461041857806356d399e81461043a578063570ca7351461046157806358b810d31461048857806360e52ecb1461049b5780636786452c146104c25780636cf72aa4146104d55780637c0b4bfb146104e857806386fa5063146105275780638c7e7a311461054e5780638dd0855714610561578063937e09b11461058157806396a608c0146105895780639ca002a614610591578063a4b6aa83146105a4578063bc063e1a146105ac578063ccec3716146105c8578063d26146a5146105db578063d826f88f146105ee578063dda0a81a146105f6578063e020e4c41461060d578063e50d50d814610620575b600080fd5b6101fa6101f5366004612e8f565b610640565b005b6101fa61020a366004612ecb565b6106c2565b6101fa6106cc565b61021f6107b7565b60405161022e93929190612f5d565b60405180910390f35b61023f61096d565b60405190815260200161022e565b6102576201518081565b6040516001600160401b03909116815260200161022e565b61028261027d366004612fca565b6109d9565b60405161022e929190612fe3565b600f5461029d9060ff1681565b60405161022e9190613013565b6101fa6102b836600461303b565b610a12565b6101fa6102cb366004613079565b610a1d565b6102d8610a93565b60405161022e919081518152602080830151908201526040808301519082015260609182015161ffff169181019190915260800190565b6006546007546008546009546103259392919084565b60408051948552602085019390935291830152606082015260800161022e565b61036c7f000000000000000000000000000000000000000000000000000000000000000081565b60405161022e9190613094565b6002546003546004546005546103939392919061ffff1684565b6040805194855260208501939093529183015261ffff16606082015260800161022e565b61023f6103c5366004612ecb565b600a6020526000908152604090205481565b6000546001546103e5919082565b60405161022e9291906130a8565b600c5461023f565b6101fa610af3565b61040b610b68565b60405161022e91906130dc565b600f5461042a90610100900460ff1681565b604051901515815260200161022e565b61023f7f000000000000000000000000000000000000000000000000000000000000000081565b61036c7f000000000000000000000000000000000000000000000000000000000000000081565b6101fa610496366004613103565b610bc0565b61036c7f000000000000000000000000000000000000000000000000000000000000000081565b6101fa6104d03660046131f1565b610c88565b6101fa6104e3366004613242565b610d02565b6105126104f6366004612ecb565b600d602052600090815260409020805460019091015460ff1682565b6040805192835290151560208301520161022e565b61023f7f000000000000000000000000000000000000000000000000000000000000000081565b61023f61055c36600461325f565b610d78565b61057461056f366004612fca565b610e09565b60405161022e91906132a6565b61023f610e62565b61023f610f62565b61023f61059f366004612fca565b610fb9565b61023f610fce565b6105b561271081565b60405161ffff909116815260200161022e565b6101fa6105d6366004612ecb565b611040565b61036c6105e9366004612fca565b6111df565b6101fa611209565b6105fe61127e565b60405161022e939291906132b4565b6101fa61061b3660046132ed565b61145e565b61023f61062e366004612ecb565b600b6020526000908152604090205481565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146106b657337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b60405180910390fd5b6106bf816114e3565b50565b6106bf33826118a8565b6001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016330361070657610704611a29565b565b336000908152600b602052604081205461072090426133e4565b90506201518081101561076a57336000908152600b602052604090819020549051630429b06960e31b815260048101919091524260248201526201518060448201526064016106ad565b600061077533611b4c565b905080156107b35760405181815233907f249a82fbe5056a1940b4e996665ba2a82c340ae9fa1e069fd1ababf5508f396e9060200160405180910390a25b5050565b600e5460609081908190806001600160401b038111156107d9576107d9612d34565b604051908082528060200260200182016040528015610802578160200160208202803683370190505b509350806001600160401b0381111561081d5761081d612d34565b604051908082528060200260200182016040528015610846578160200160208202803683370190505b509250806001600160401b0381111561086157610861612d34565b60405190808252806020026020018201604052801561088a578160200160208202803683370190505b50915060005b81811015610966576000600e82815481106108ad576108ad6133f7565b60009182526020808320909101546001600160a01b0316808352600d909152604090912087519192509082908890859081106108eb576108eb6133f7565b60200260200101906001600160a01b031690816001600160a01b0316815250508060000154868481518110610922576109226133f7565b60209081029190910101526001810154855160ff9091169086908590811061094c5761094c6133f7565b911515602092830291909101909101525050600101610890565b5050909192565b600c54600090815b818110156109d4576000600c8281548110610992576109926133f7565b600091825260208083206002909202909101546001600160a01b0316808352600a9091526040909120549091506109c9908561340d565b935050600101610975565b505090565b600c81815481106109e957600080fd5b6000918252602090912060029091020180546001909101546001600160a01b0391821692501682565b6107b3338284611d49565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610a8a57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6106bf8161255d565b610ac26040518060800160405280600081526020016000815260200160008152602001600061ffff1681525090565b5060408051608081018252600254815260035460208201526004549181019190915260055461ffff16606082015290565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610b6057337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6107046125e7565b610b936040518060800160405280600081526020016000815260200160008152602001600081525090565b50604080516080810182526006548152600754602082015260085491810191909152600954606082015290565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610c2d57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b610c35611a29565b610c3e8561255d565b610c47846114e3565b610c50836128a5565b8015610c8157610c817f00000000000000000000000000000000000000000000000000000000000000008383611d49565b5050505050565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610cf557337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b610c8185858585856128bf565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610d6f57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6106bf816128a5565b6000828211610d9e5782826040516376675ed960e11b81526004016106ad9291906130a8565b82600003610dcf576004610db36001866133e4565b610dbd9190613420565b610dc890600161340d565b9050610e02565b6000610ddb84846133e4565b905080610de96001876133e4565b610df39190613420565b610dfe90600161340d565b9150505b9392505050565b610e11612c79565b600c8281548110610e2457610e246133f7565b60009182526020918290206040805180820190915260029092020180546001600160a01b039081168352600190910154169181019190915292915050565b600e5460009081908190815b81811015610ee5576000600e8281548110610e8b57610e8b6133f7565b60009182526020808320909101546001600160a01b0316808352600d909152604090912060018101549192509060ff16610edb578054610ecb908761340d565b9550610ed860018661340d565b94505b5050600101610e6e565b50610f5a83610ef261096d565b610f1c907f00000000000000000000000000000000000000000000000000000000000000006133e4565b610f2691906133e4565b600c54610f3490859061340d565b7f0000000000000000000000000000000000000000000000000000000000000000610d78565b935050505090565b600c54600090610f725750600090565b600a6000600c600081548110610f8a57610f8a6133f7565b600091825260208083206002909202909101546001600160a01b03168352820192909252604001902054905090565b6000610fc88260006001610d78565b92915050565b600e54600090815b818110156109d4576000600e8281548110610ff357610ff36133f7565b60009182526020808320909101546001600160a01b0316808352600d909152604090912060018101549192509060ff16611036578054611033908661340d565b94505b5050600101610fd6565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146110ad57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6003600f5460ff1660038111156110c6576110c6612ffd565b141580156110ea57506000600f5460ff1660038111156110e8576110e8612ffd565b145b1561110f57600f54604051632f3e69dd60e01b81526106ad9160ff1690600401613013565b6040516370a0823160e01b815281906000906001600160a01b038316906370a0823190611140903090600401613094565b602060405180830381865afa15801561115d573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906111819190613442565b9050600081116111a6578260405163e932c57360e01b81526004016106ad9190613094565b6111da6001600160a01b0383167f0000000000000000000000000000000000000000000000000000000000000000836129da565b505050565b600e81815481106111ef57600080fd5b6000918252602090912001546001600160a01b0316905081565b336001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000161461127657337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b610704611a29565b600c5460609081908190806001600160401b038111156112a0576112a0612d34565b6040519080825280602002602001820160405280156112c9578160200160208202803683370190505b509350806001600160401b038111156112e4576112e4612d34565b60405190808252806020026020018201604052801561130d578160200160208202803683370190505b509250806001600160401b0381111561132857611328612d34565b604051908082528060200260200182016040528015611351578160200160208202803683370190505b50915060005b81811015610966576000600c8281548110611374576113746133f7565b60009182526020909120600290910201805487519192506001600160a01b0316908790849081106113a7576113a76133f7565b6001600160a01b039283166020918202929092010152600182015486519116908690849081106113d9576113d96133f7565b60200260200101906001600160a01b031690816001600160a01b031681525050600a600087848151811061140f5761140f6133f7565b60200260200101516001600160a01b03166001600160a01b031681526020019081526020016000205484838151811061144a5761144a6133f7565b602090810291909101015250600101611357565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146114cb57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6114da87878787878787612a32565b50505050505050565b6000600f5460ff1660038111156114fc576114fc612ffd565b1461152157600f546040516312ba63f960e11b81526106ad9160ff1690600401613013565b600e5460005b8181101561158057600d6000600e8381548110611546576115466133f7565b60009182526020808320909101546001600160a01b0316835282019290925260400181209081556001908101805460ff1916905501611527565b5061158d600e6000612c90565b5080517f0000000000000000000000000000000000000000000000000000000000000000907f000000000000000000000000000000000000000000000000000000000000000010156116175781517f0000000000000000000000000000000000000000000000000000000000000000604051630962235760e31b81526004016106ad9291906130a8565b815160005b818110156118a257806000036116cd577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316848281518110611668576116686133f7565b6020026020010151600001516001600160a01b0316146116cd5760405163ef77b46760e01b8152600481018290526001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001660248201526044016106ad565b60006001600160a01b03168482815181106116ea576116ea6133f7565b6020026020010151600001516001600160a01b0316036117205760405163621caef560e11b8152600481018290526024016106ad565b6000600d6000868481518110611738576117386133f7565b6020026020010151600001516001600160a01b03166001600160a01b031681526020019081526020016000209050806000015460001461178e576040516308308c2360e21b8152600481018390526024016106ad565b60006117bb85847f0000000000000000000000000000000000000000000000000000000000000000610d78565b905060008684815181106117d1576117d16133f7565b6020026020010151602001519050818110156118065783818360405163c4aa8aa360e01b81526004016106ad9392919061345b565b8086101561182d57838187604051636d3ae3a760e01b81526004016106ad9392919061345b565b61183781876133e4565b9550600e87858151811061184d5761184d6133f7565b6020908102919091018101515182546001808201855560009485529290932090920180546001600160a01b0319166001600160a01b0390931692909217909155908355918201805460ff19169055500161161c565b50505050565b6001600f5460ff1660038111156118c1576118c1612ffd565b141580156118e657506002600f5460ff1660038111156118e3576118e3612ffd565b14155b1561190b576002546040516310004b0160e11b815260048101919091526024016106ad565b60006119178383612aa3565b600c549091506000908190815b818110156119bd576000600c8281548110611941576119416133f7565b6000918252602090912060029091020180549091506001600160a01b03808a169116036119b45760018101546001600160a01b03808816911603611989575050505050505050565b600190810180546001600160a01b038881166001600160a01b031983161790925516945092506119bd565b50600101611924565b50816119de5785604051632fd768d360e11b81526004016106ad9190613094565b856001600160a01b03167f25639ce407c98fba722dddf1f023c8242e3c77732cfe4660d1c04930bf4cbe728486604051611a19929190612fe3565b60405180910390a2505050505050565b600c5460005b81811015611aec576000600c8281548110611a4c57611a4c6133f7565b600091825260208083206002909202909101546001600160a01b0316808352600a9091526040909120549091506003600f5460ff166003811115611a9257611a92612ffd565b14158015611aa05750600081115b15611ad957611ad96001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001683836129da565b611ae282612ac1565b5050600101611a2f565b50611af9600c6000612cae565b600f805460ff191690556040805160008082526020820190925281611b40565b6040805180820190915260008082526020820152815260200190600190039081611b195790505b5090506107b3816114e3565b6001600160a01b0381166000908152600a602052604081205490819003611b7257919050565b611b7b82612ac1565b600c5460005b81811015611c9657600c8181548110611b9c57611b9c6133f7565b60009182526020909120600290910201546001600160a01b0390811690851603611c8e57600c611bcd6001846133e4565b81548110611bdd57611bdd6133f7565b9060005260206000209060020201600c8281548110611bfe57611bfe6133f7565b60009182526020909120825460029092020180546001600160a01b039283166001600160a01b0319918216178255600193840154939091018054939092169216919091179055600c805480611c5557611c55613471565b60008281526020902060026000199092019182020180546001600160a01b03199081168255600191909101805490911690559055611c96565b600101611b81565b506001600160a01b0383166000908152600d6020526040902060018101805460ff191690556003600f5460ff166003811115611cd457611cd4612ffd565b03611ce25760009250611d42565b611d166001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001685856129da565b6002600f5460ff166003811115611d2f57611d2f612ffd565b03611d4257600f805460ff191660011790555b5050919050565b6000600f5460ff166003811115611d6257611d62612ffd565b14158015611d8757506001600f5460ff166003811115611d8457611d84612ffd565b14155b15611dac57600f54604051630295f95f60e51b81526106ad9160ff1690600401613013565b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166386fa50636040518163ffffffff1660e01b8152600401602060405180830381865afa158015611e0a573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611e2e9190613442565b7f000000000000000000000000000000000000000000000000000000000000000014611f12577f00000000000000000000000000000000000000000000000000000000000000007f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166386fa50636040518163ffffffff1660e01b8152600401602060405180830381865afa158015611ed3573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611ef79190613442565b604051631248081b60e31b81526004016106ad9291906130a8565b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166356d399e86040518163ffffffff1660e01b8152600401602060405180830381865afa158015611f70573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611f949190613442565b7f000000000000000000000000000000000000000000000000000000000000000014612039577f00000000000000000000000000000000000000000000000000000000000000007f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166356d399e86040518163ffffffff1660e01b8152600401602060405180830381865afa158015611ed3573d6000803e3d6000fd5b6000600f5460ff16600381111561205257612052612ffd565b03612145577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316836001600160a01b0316146120cd57827f00000000000000000000000000000000000000000000000000000000000000006040516316a4ebc160e21b81526004016106ad929190612fe3565b600f805460ff1916600117905560025460055460405161ffff90911681526001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001691907fe52a01bb4ffcc3baf2c5d17111d7d5b676a7485809eb10671092a89d2bb22ff69060200160405180910390a35b6001600160a01b0383166000908152600d602052604090208054158015906121725750600181015460ff16155b156121b257805482101561219d57805460405163454cd8e160e01b81526106ad9184916004016130a8565b6001818101805460ff19169091179055612207565b6001600160a01b0384166000908152600a60205260409020541580156121de57506121db610e62565b82105b1561220757816121ec610e62565b6040516310e45ff160e21b81526004016106ad9291906130a8565b6001600160a01b0384166000908152600a602052604081205490036122d85760006122328585612aa3565b604080518082019091526001600160a01b03808816825291821660208201908152600c8054600181018255600091909152915160029092027fdf6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c7810180549385166001600160a01b031994851617905590517fdf6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c89091018054919093169116179055506122e2565b6122e284846118a8565b6001600160a01b0384166000908152600a60205260408120805484929061230a90849061340d565b90915550506001600160a01b0384166000908152600b6020526040812042905561233261096d565b9050600061233e610fce565b90507f000000000000000000000000000000000000000000000000000000000000000061236b828461340d565b11156123b05781817f000000000000000000000000000000000000000000000000000000000000000060405163513b428b60e11b81526004016106ad9392919061345b565b600c547f00000000000000000000000000000000000000000000000000000000000000001015612415576040516315b5c3d560e21b81527f000000000000000000000000000000000000000000000000000000000000000060048201526024016106ad565b7f000000000000000000000000000000000000000000000000000000000000000082036124a2576002546040516001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001691907f48996614f3380f3dcd5a2d2e6eaddfd66207ff3457fee339e09b4735bc9ec95890600090a3600f805460ff191660021790555b856001600160a01b03167fbdaa686eb6f59012d211a74523da260341c516896e9e5be954163d6ecf26ffa2856040516124dd91815260200190565b60405180910390a261251a6001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016873087612ae8565b6002600f5460ff16600381111561253357612533612ffd565b1480156125485750600f54610100900460ff16155b15612555576125556125e7565b505050505050565b6000600f5460ff16600381111561257657612576612ffd565b1461259b57600f5460405163ad88fc8f60e01b81526106ad9160ff1690600401613013565b61271061ffff821611156125cf57604051624ae8fd60e41b815261ffff8216600482015261271060248201526044016106ad565b6005805461ffff191661ffff92909216919091179055565b6002600f5460ff16600381111561260057612600612ffd565b1461262557600f546040516383696f6f60e01b81526106ad9160ff1690600401613013565b600f805460ff191660031790556002546040517f839cf22e1ba87ce2f5b9bbf46cf0175a09eed52febdfaac8852478e68203c76390600090a2600c546000816001600160401b0381111561267b5761267b612d34565b6040519080825280602002602001820160405280156126b457816020015b6126a1612ccf565b8152602001906001900390816126995790505b50905060005b8281101561274f576000600c82815481106126d7576126d76133f7565b6000918252602080832060408051608081018252600290940290910180546001600160a01b03908116858401818152600184015490921660608701529085528552600a83529320549082015284519192509084908490811061273b5761273b6133f7565b6020908102919091010152506001016126ba565b5060405163095ea7b360e01b81526001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169063095ea7b3906127de907f0000000000000000000000000000000000000000000000000000000000000000907f000000000000000000000000000000000000000000000000000000000000000090600401613487565b6020604051808303816000875af11580156127fd573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061282191906134a0565b50604051632f1fbfb360e21b81526001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169063bc7efecc90612877906000906006906002908790600401613504565b600060405180830381600087803b15801561289157600080fd5b505af1158015612555573d6000803e3d6000fd5b600f80549115156101000261ff0019909216919091179055565b6000600f5460ff1660038111156128d8576128d8612ffd565b146128fd57600f54604051639a0293fd60e01b81526106ad9160ff1690600401613013565b60405160016226579360e01b031981526001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169063ffd9a86d9061297290889088907f000000000000000000000000000000000000000000000000000000000000000090899060040161357b565b600060405180830381600087803b15801561298c57600080fd5b505af11580156129a0573d6000803e3d6000fd5b5050865160005550506020948501516001558351600655938301516007556040830151600855606090920151600955600255600355600455565b6111da83846001600160a01b031663a9059cbb8585604051602401612a00929190613487565b604051602081830303815290604052915060e01b6020820180516001600160e01b038381831617835250505050612b21565b612a3a611a29565b612a5387878760000151886020015189604001516128bf565b612a60856060015161255d565b612a69846114e3565b612a72836128a5565b80156114da576114da7f00000000000000000000000000000000000000000000000000000000000000008383611d49565b60006001600160a01b03821615612aba5781610e02565b5090919050565b6001600160a01b03166000908152600a60209081526040808320839055600b909152812055565b6040516001600160a01b0384811660248301528381166044830152606482018390526118a29186918216906323b872dd90608401612a00565b6000612b366001600160a01b03841683612b7b565b90508051600014158015612b5b575080806020019051810190612b5991906134a0565b155b156111da5782604051635274afe760e01b81526004016106ad9190613094565b6060610e028383600084600080856001600160a01b03168486604051612ba191906135b6565b60006040518083038185875af1925050503d8060008114612bde576040519150601f19603f3d011682016040523d82523d6000602084013e612be3565b606091505b5091509150612bf3868383612bfd565b9695505050505050565b606082612c1257612c0d82612c50565b610e02565b8151158015612c2957506001600160a01b0384163b155b15612c495783604051639996b31560e01b81526004016106ad9190613094565b5080610e02565b805115612c605780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b604080518082019091526000808252602082015290565b50805460008255906000526020600020908101906106bf9190612cef565b50805460008255600202906000526020600020908101906106bf9190612d08565b6040518060400160405280612ce2612c79565b8152602001600081525090565b5b80821115612d045760008155600101612cf0565b5090565b5b80821115612d045780546001600160a01b031990811682556001820180549091169055600201612d09565b634e487b7160e01b600052604160045260246000fd5b604080519081016001600160401b0381118282101715612d6c57612d6c612d34565b60405290565b604051608081016001600160401b0381118282101715612d6c57612d6c612d34565b604051601f8201601f191681016001600160401b0381118282101715612dbc57612dbc612d34565b604052919050565b80356001600160a01b0381168114612ddb57600080fd5b919050565b600082601f830112612df157600080fd5b81356001600160401b03811115612e0a57612e0a612d34565b612e1960208260051b01612d94565b8082825260208201915060208360061b860101925085831115612e3b57600080fd5b602085015b83811015612e855760408188031215612e5857600080fd5b612e60612d4a565b612e6982612dc4565b8152602082810135818301529084529290920191604001612e40565b5095945050505050565b600060208284031215612ea157600080fd5b81356001600160401b03811115612eb757600080fd5b612ec384828501612de0565b949350505050565b600060208284031215612edd57600080fd5b610e0282612dc4565b600081518084526020840193506020830160005b82811015612f215781516001600160a01b0316865260209586019590910190600101612efa565b5093949350505050565b600081518084526020840193506020830160005b82811015612f21578151865260209586019590910190600101612f3f565b606081526000612f706060830186612ee6565b8281036020840152612f828186612f2b565b83810360408501528451808252602080870193509091019060005b81811015612fbd5783511515835260209384019390920191600101612f9d565b5090979650505050505050565b600060208284031215612fdc57600080fd5b5035919050565b6001600160a01b0392831681529116602082015260400190565b634e487b7160e01b600052602160045260246000fd5b602081016004831061303557634e487b7160e01b600052602160045260246000fd5b91905290565b6000806040838503121561304e57600080fd5b8235915061305e60208401612dc4565b90509250929050565b803561ffff81168114612ddb57600080fd5b60006020828403121561308b57600080fd5b610e0282613067565b6001600160a01b0391909116815260200190565b918252602082015260400190565b805182526020810151602083015260408101516040830152606081015160608301525050565b60808101610fc882846130b6565b80151581146106bf57600080fd5b8035612ddb816130ea565b600080600080600060a0868803121561311b57600080fd5b61312486613067565b945060208601356001600160401b0381111561313f57600080fd5b61314b88828901612de0565b945050604086013561315c816130ea565b925061316a60608701612dc4565b949793965091946080013592915050565b60006040828403121561318d57600080fd5b613195612d4a565b823581526020928301359281019290925250919050565b6000608082840312156131be57600080fd5b6131c6612d72565b8235815260208084013590820152604080840135908201526060928301359281019290925250919050565b6000806000806000610120868803121561320a57600080fd5b613214878761317b565b945061322387604088016131ac565b949794965050505060c08301359260e081013592610100909101359150565b60006020828403121561325457600080fd5b8135610e02816130ea565b60008060006060848603121561327457600080fd5b505081359360208301359350604090920135919050565b80516001600160a01b03908116835260209182015116910152565b60408101610fc8828461328b565b6060815260006132c76060830186612ee6565b82810360208401526132d98186612ee6565b90508281036040840152612bf38185612f2b565b60008060008060008060008789036101c081121561330a57600080fd5b6133148a8a61317b565b97506133238a60408b016131ac565b9650608060bf198201121561333757600080fd5b50613340612d72565b60c0890135815260e0890135602082015261010089013560408201526133696101208a01613067565b606082015294506101408801356001600160401b0381111561338a57600080fd5b6133968a828b01612de0565b9450506133a661016089016130f8565b92506133b56101808901612dc4565b9699959850939692959194919350506101a09091013590565b634e487b7160e01b600052601160045260246000fd5b81810381811115610fc857610fc86133ce565b634e487b7160e01b600052603260045260246000fd5b80820180821115610fc857610fc86133ce565b60008261343d57634e487b7160e01b600052601260045260246000fd5b500490565b60006020828403121561345457600080fd5b5051919050565b9283526020830191909152604082015260600190565b634e487b7160e01b600052603160045260246000fd5b6001600160a01b03929092168252602082015260400190565b6000602082840312156134b257600080fd5b8151610e02816130ea565b600081518084526020840193506020830160005b82811015612f215781516134e687825161328b565b602090810151604088015260609096019591909101906001016134d1565b8454815260018501546020820152835460408201526001840154606082015260028401546080820152600384015460a0820152825460c0820152600183015460e0820152600283015461010082015261ffff6003840154166101208201526101606101408201526000612bf36101608301846134bd565b8451815260208086015190820152610100810161359b60408301866130b6565b6001600160a01b039390931660c082015260e0015292915050565b6000825160005b818110156135d757602081860181015185830152016135bd565b50600092019182525091905056fea164736f6c634300081a000a", + "bytecode": "0x610120604052600f805460ff1916905534801561001b57600080fd5b5060405161543d38038061543d83398101604081905261003a9161196c565b866001600160a01b0381166100a45760405162461bcd60e51b815260206004820152602560248201527f5368617265643a205a65726f2d61646472657373206973206e6f74207065726d6044820152641a5d1d195960da1b60648201526084015b60405180910390fd5b86806000036100f55760405162461bcd60e51b815260206004820152601b60248201527f5368617265643a2075696e7420696e70757420697320656d7074790000000000604482015260640161009b565b6001600160a01b03891660a081905260408051630ada733d60e31b815290516356d399e8916004808201926020929091908290030181865afa15801561013f573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101639190611a30565b60c0818152505060a0516001600160a01b0316637c89d2f06040518163ffffffff1660e01b8152600401602060405180830381865afa1580156101aa573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101ce9190611a49565b6001600160a01b03166080526101008890523260e05260006101f588888888888680610204565b50505050505050505050611c7f565b61020c61026c565b845160208601516040870151610225928a928a92610398565b6060850151610233906104ad565b61023c84610537565b600f805461ff0019166101008515150217905580156102635760e051610263908383610859565b50505050505050565b600c5460005b81811015610334576000600c828154811061028f5761028f611a66565b600091825260208083206002909202909101546001600160a01b0316808352600a9091526040909120549091506003600f5460ff1660038111156102d5576102d5611751565b141580156102e35750600081115b156102ff576080516102ff906001600160a01b03168383610e71565b61032a826001600160a01b03166000908152600a60209081526040808320839055600b909152812055565b5050600101610272565b50610341600c60006116cd565b600f805460ff191690556040805160008082526020820190925281610388565b60408051808201909152600080825260208201528152602001906001900390816103615790505b50905061039481610537565b5050565b6000600f5460ff1660038111156103b1576103b1611751565b146103d657600f54604051639a0293fd60e01b815261009b9160ff1690600401611a7c565b60a05160e0516040805160016226579360e01b03198152885160048201526020808a01516024830152885160448301528801516064820152908701516084820152606087015160a48201526001600160a01b0391821660c482015260e4810186905291169063ffd9a86d9061010401600060405180830381600087803b15801561045f57600080fd5b505af1158015610473573d6000803e3d6000fd5b5050865160005550506020948501516001558351600655938301516007556040830151600855606090920151600955600255600355600455565b6000600f5460ff1660038111156104c6576104c6611751565b146104eb57600f5460405163ad88fc8f60e01b815261009b9160ff1690600401611a7c565b61271061ffff8216111561051f57604051624ae8fd60e41b815261ffff82166004820152612710602482015260440161009b565b6005805461ffff191661ffff92909216919091179055565b6000600f5460ff16600381111561055057610550611751565b1461057557600f546040516312ba63f960e11b815261009b9160ff1690600401611a7c565b600e5460005b818110156105d457600d6000600e838154811061059a5761059a611a66565b60009182526020808320909101546001600160a01b0316835282019290925260400181209081556001908101805460ff191690550161057b565b506105e1600e60006116ee565b5060c051610100518251111561061957815161010051604051630962235760e31b81526004810192909252602482015260440161009b565b815160005b8181101561085357806000036106955760e0516001600160a01b031684828151811061064c5761064c611a66565b6020026020010151600001516001600160a01b0316146106955760e05160405163ef77b46760e01b8152600481018390526001600160a01b03909116602482015260440161009b565b60006001600160a01b03168482815181106106b2576106b2611a66565b6020026020010151600001516001600160a01b0316036106e85760405163621caef560e11b81526004810182905260240161009b565b6000600d600086848151811061070057610700611a66565b6020026020010151600001516001600160a01b03166001600160a01b0316815260200190815260200160002090508060000154600014610756576040516308308c2360e21b81526004810183905260240161009b565b600061076c858461010051610ece60201b60201c565b9050600086848151811061078257610782611a66565b6020026020010151602001519050818110156107b75783818360405163c4aa8aa360e01b815260040161009b93929190611aa4565b808610156107de57838187604051636d3ae3a760e01b815260040161009b93929190611aa4565b6107e88187611ad0565b9550600e8785815181106107fe576107fe611a66565b6020908102919091018101515182546001808201855560009485529290932090920180546001600160a01b0319166001600160a01b0390931692909217909155908355918201805460ff19169055500161061e565b50505050565b6000600f5460ff16600381111561087257610872611751565b1415801561089757506001600f5460ff16600381111561089457610894611751565b14155b156108bc57600f54604051630295f95f60e51b815261009b9160ff1690600401611a7c565b60a0516001600160a01b03166386fa50636040518163ffffffff1660e01b8152600401602060405180830381865afa1580156108fc573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109209190611a30565b61010051146109b3576101005160a0516001600160a01b03166386fa50636040518163ffffffff1660e01b8152600401602060405180830381865afa15801561096d573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109919190611a30565b604051631248081b60e31b81526004810192909252602482015260440161009b565b60a0516001600160a01b03166356d399e86040518163ffffffff1660e01b8152600401602060405180830381865afa1580156109f3573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610a179190611a30565b60c05114610a625760c05160a0516001600160a01b03166356d399e86040518163ffffffff1660e01b8152600401602060405180830381865afa15801561096d573d6000803e3d6000fd5b6000600f5460ff166003811115610a7b57610a7b611751565b03610b145760e0516001600160a01b0316836001600160a01b031614610ab95760e0516040516316a4ebc160e21b815261009b918591600401611ae3565b600f805460ff1916600117905560e05160025460055460405161ffff90911681526001600160a01b03909216917fe52a01bb4ffcc3baf2c5d17111d7d5b676a7485809eb10671092a89d2bb22ff69060200160405180910390a35b6001600160a01b0383166000908152600d60205260409020805415801590610b415750600181015460ff16155b15610b8a578054821015610b7557805460405163454cd8e160e01b815261009b918491600401918252602082015260400190565b6001818101805460ff19169091179055610be6565b6001600160a01b0384166000908152600a6020526040902054158015610bb65750610bb3610f65565b82105b15610be65781610bc4610f65565b6040516310e45ff160e21b81526004810192909252602482015260440161009b565b6001600160a01b0384166000908152600a60205260408120549003610cb7576000610c11858561102b565b604080518082019091526001600160a01b03808816825291821660208201908152600c8054600181018255600091909152915160029092027fdf6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c7810180549385166001600160a01b031994851617905590517fdf6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c8909101805491909316911617905550610cc1565b610cc18484611049565b6001600160a01b0384166000908152600a602052604081208054849290610ce9908490611afd565b90915550506001600160a01b0384166000908152600b60205260408120429055610d116111ca565b90506000610d1d611236565b60c051909150610d2d8284611afd565b1115610d5457818160c05160405163513b428b60e11b815260040161009b93929190611aa4565b61010051600c541115610d8157610100516040516315b5c3d560e21b815260040161009b91815260200190565b60c0518203610dd35760e0516002546040516001600160a01b03909216917f48996614f3380f3dcd5a2d2e6eaddfd66207ff3457fee339e09b4735bc9ec95890600090a3600f805460ff191660021790555b856001600160a01b03167fbdaa686eb6f59012d211a74523da260341c516896e9e5be954163d6ecf26ffa285604051610e0e91815260200190565b60405180910390a2608051610e2e906001600160a01b03168730876112a8565b6002600f5460ff166003811115610e4757610e47611751565b148015610e5c5750600f54610100900460ff16155b15610e6957610e696112e1565b505050505050565b610ec983846001600160a01b031663a9059cbb8585604051602401610e97929190611b10565b60408051808303601f1901815291905260208101805160e09390931b6001600160e01b03938416179052915061154216565b505050565b6000828211610efa576040516376675ed960e11b8152600481018490526024810183905260440161009b565b82600003610f2b576004610f0f600186611ad0565b610f199190611b29565b610f24906001611afd565b9050610f5e565b6000610f378484611ad0565b905080610f45600187611ad0565b610f4f9190611b29565b610f5a906001611afd565b9150505b9392505050565b600e5460009081908190815b81811015610fe8576000600e8281548110610f8e57610f8e611a66565b60009182526020808320909101546001600160a01b0316808352600d909152604090912060018101549192509060ff16610fde578054610fce9087611afd565b9550610fdb600186611afd565b94505b5050600101610f71565b5061102383610ff56111ca565b60c0516110029190611ad0565b61100c9190611ad0565b600c5461101a908590611afd565b61010051610ece565b935050505090565b60006001600160a01b038216156110425781610f5e565b5090919050565b6001600f5460ff16600381111561106257611062611751565b1415801561108757506002600f5460ff16600381111561108457611084611751565b14155b156110ac576002546040516310004b0160e11b8152600481019190915260240161009b565b60006110b8838361102b565b600c549091506000908190815b8181101561115e576000600c82815481106110e2576110e2611a66565b6000918252602090912060029091020180549091506001600160a01b03808a169116036111555760018101546001600160a01b0380881691160361112a575050505050505050565b600190810180546001600160a01b038881166001600160a01b0319831617909255169450925061115e565b506001016110c5565b508161117f5785604051632fd768d360e11b815260040161009b9190611b4b565b856001600160a01b03167f25639ce407c98fba722dddf1f023c8242e3c77732cfe4660d1c04930bf4cbe7284866040516111ba929190611ae3565b60405180910390a2505050505050565b600c54600090815b81811015611231576000600c82815481106111ef576111ef611a66565b600091825260208083206002909202909101546001600160a01b0316808352600a9091526040909120549091506112269085611afd565b9350506001016111d2565b505090565b600e54600090815b81811015611231576000600e828154811061125b5761125b611a66565b60009182526020808320909101546001600160a01b0316808352600d909152604090912060018101549192509060ff1661129e57805461129b9086611afd565b94505b505060010161123e565b6040516001600160a01b0384811660248301528381166044830152606482018390526108539186918216906323b872dd90608401610e97565b6002600f5460ff1660038111156112fa576112fa611751565b1461131f57600f546040516383696f6f60e01b815261009b9160ff1690600401611a7c565b600f805460ff191660031790556002546040517f839cf22e1ba87ce2f5b9bbf46cf0175a09eed52febdfaac8852478e68203c76390600090a2600c546000816001600160401b038111156113755761137561177c565b6040519080825280602002602001820160405280156113c957816020015b60408051608081018252600091810182815260608201839052815260208101919091528152602001906001900390816113935790505b50905060005b82811015611464576000600c82815481106113ec576113ec611a66565b6000918252602080832060408051608081018252600290940290910180546001600160a01b03908116858401818152600184015490921660608701529085528552600a83529320549082015284519192509084908490811061145057611450611a66565b6020908102919091010152506001016113cf565b506080516001600160a01b031663095ea7b360a05160c0516040518363ffffffff1660e01b8152600401611499929190611b10565b6020604051808303816000875af11580156114b8573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906114dc9190611b5f565b5060a0516001600160a01b031663bc7efecc600060066002856040518563ffffffff1660e01b81526004016115149493929190611bd9565b600060405180830381600087803b15801561152e57600080fd5b505af1158015610e69573d6000803e3d6000fd5b60006115576001600160a01b0384168361159c565b9050805160001415801561157c57508080602001905181019061157a9190611b5f565b155b15610ec95782604051635274afe760e01b815260040161009b9190611b4b565b6060610f5e838360006115b0565b92915050565b6060814710156115d5573060405163cd78605960e01b815260040161009b9190611b4b565b600080856001600160a01b031684866040516115f19190611c50565b60006040518083038185875af1925050503d806000811461162e576040519150601f19603f3d011682016040523d82523d6000602084013e611633565b606091505b50909250905061164486838361164e565b9695505050505050565b6060826116635761165e826116a1565b610f5e565b815115801561167a57506001600160a01b0384163b155b1561169a5783604051639996b31560e01b815260040161009b9190611b4b565b5080610f5e565b8051156116b15780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b50565b50805460008255600202906000526020600020908101906116ca919061170c565b50805460008255906000526020600020908101906116ca919061173c565b5b808211156117385780546001600160a01b03199081168255600182018054909116905560020161170d565b5090565b5b80821115611738576000815560010161173d565b634e487b7160e01b600052602160045260246000fd5b6001600160a01b03811681146116ca57600080fd5b634e487b7160e01b600052604160045260246000fd5b604051608081016001600160401b03811182821017156117b4576117b461177c565b60405290565b604080519081016001600160401b03811182821017156117b4576117b461177c565b604051601f8201601f191681016001600160401b03811182821017156118045761180461177c565b604052919050565b60006080828403121561181e57600080fd5b611826611792565b8251815260208084015190820152604080840151908201526060928301519281019290925250919050565b60006080828403121561186357600080fd5b61186b611792565b825181526020808401519082015260408084015190820152606083015190915061ffff8116811461189b57600080fd5b606082015292915050565b600082601f8301126118b757600080fd5b81516001600160401b038111156118d0576118d061177c565b6118df60208260051b016117dc565b8082825260208201915060208360061b86010192508583111561190157600080fd5b602085015b8381101561194d576040818803121561191e57600080fd5b6119266117ba565b815161193181611767565b8152602082810151818301529084529290920191604001611906565b5095945050505050565b8051801515811461196757600080fd5b919050565b60008060008060008060008789036101c081121561198957600080fd5b885161199481611767565b60208a015190985096506040603f19820112156119b057600080fd5b506119b96117ba565b604089015181526060890151602082015294506119d98960808a0161180c565b93506119e9896101008a01611851565b6101808901519093506001600160401b03811115611a0657600080fd5b611a128a828b016118a6565b925050611a226101a08901611957565b905092959891949750929550565b600060208284031215611a4257600080fd5b5051919050565b600060208284031215611a5b57600080fd5b8151610f5e81611767565b634e487b7160e01b600052603260045260246000fd5b6020810160048310611a9e57634e487b7160e01b600052602160045260246000fd5b91905290565b9283526020830191909152604082015260600190565b634e487b7160e01b600052601160045260246000fd5b818103818111156115aa576115aa611aba565b6001600160a01b0392831681529116602082015260400190565b808201808211156115aa576115aa611aba565b6001600160a01b03929092168252602082015260400190565b600082611b4657634e487b7160e01b600052601260045260246000fd5b500490565b6001600160a01b0391909116815260200190565b600060208284031215611b7157600080fd5b610f5e82611957565b600081518084526020840193506020830160005b82811015611bcf578151805180516001600160a01b039081168952602091820151168189015290810151604088015260609096019590910190600101611b8e565b5093949350505050565b8454815260018501546020820152835460408201526001840154606082015260028401546080820152600384015460a0820152825460c0820152600183015460e0820152600283015461010082015261ffff6003840154166101208201526101606101408201526000611644610160830184611b7a565b6000825160005b81811015611c715760208186018101518583015201611c57565b506000920191825250919050565b60805160a05160c05160e051610100516135f2611e4b6000396000818161050501528181610f36015281816115b4015281816115dd0152818161179701528181611e3001528181611e56015281816123b501526123e90152600081816104660152818161064b01528181610673015281816106d601528181610a2801528181610a5001528181610afe01528181610b2601528181610bcb01528181610bf301528181610c5b01528181610c9301528181610cbb01528181610d0d01528181610d350152818161104b01528181611073015281816111b5015281816112140152818161123c01528181611469015281816114910152818161162e015281816116a00152818161205901528181612093015281816120f50152818161244c015281816129480152612a7d01526000818161043f01528181610ef80152818161159201528181611f9601528181611fbc01528181612342015281816123750152818161241701526127b601526000818161034a01528181611dae01528181611e7701528181611f1401528181611fdd015281816127940152818161283901526129170152600081816105a601528181611ab201528181611cef015281816124f2015261276701526135f26000f3fe608060405234801561001057600080fd5b50600436106101e25760003560e01c8062e9a407146101e75780630aaffd2a146101fc5780630d616d201461020f5780630d9639ba146102175780630dcf4b8f146102375780630ebb172a1461024d57806312fa329b1461026f578063200d2ed2146102905780632816ee73146102aa5780632c6cda93146102bd5780632c7baaf3146102d0578063313f336b1461030f5780633362379414610345578063356c299d1461037957806342e94c90146103b75780634564168b146103d75780634a387cdf146103f35780634bb278f3146103fb5780634e7320ce14610403578063565a9d481461041857806356d399e81461043a578063570ca7351461046157806358b810d3146104885780636786452c1461049b5780636cf72aa4146104ae5780637c0b4bfb146104c157806386fa5063146105005780638c7e7a31146105275780638dd085571461053a578063937e09b11461055a57806396a608c0146105625780639ca002a61461056a578063a4b6aa831461057d578063bc063e1a14610585578063c3aca558146105a1578063ccec3716146105c8578063d26146a5146105db578063d826f88f146105ee578063dda0a81a146105f6578063e020e4c41461060d578063e50d50d814610620575b600080fd5b6101fa6101f5366004612e8f565b610640565b005b6101fa61020a366004612ecb565b6106c2565b6101fa6106cc565b61021f6107b7565b60405161022e93929190612f5d565b60405180910390f35b61023f61096d565b60405190815260200161022e565b6102576201518081565b6040516001600160401b03909116815260200161022e565b61028261027d366004612fca565b6109d9565b60405161022e929190612fe3565b600f5461029d9060ff1681565b60405161022e9190613013565b6101fa6102b836600461303b565b610a12565b6101fa6102cb366004613079565b610a1d565b6102d8610a93565b60405161022e919081518152602080830151908201526040808301519082015260609182015161ffff169181019190915260800190565b6006546007546008546009546103259392919084565b60408051948552602085019390935291830152606082015260800161022e565b61036c7f000000000000000000000000000000000000000000000000000000000000000081565b60405161022e9190613094565b6002546003546004546005546103939392919061ffff1684565b6040805194855260208501939093529183015261ffff16606082015260800161022e565b61023f6103c5366004612ecb565b600a6020526000908152604090205481565b6000546001546103e5919082565b60405161022e9291906130a8565b600c5461023f565b6101fa610af3565b61040b610b68565b60405161022e91906130dc565b600f5461042a90610100900460ff1681565b604051901515815260200161022e565b61023f7f000000000000000000000000000000000000000000000000000000000000000081565b61036c7f000000000000000000000000000000000000000000000000000000000000000081565b6101fa610496366004613103565b610bc0565b6101fa6104a93660046131f1565b610c88565b6101fa6104bc366004613242565b610d02565b6104eb6104cf366004612ecb565b600d602052600090815260409020805460019091015460ff1682565b6040805192835290151560208301520161022e565b61023f7f000000000000000000000000000000000000000000000000000000000000000081565b61023f61053536600461325f565b610d78565b61054d610548366004612fca565b610e09565b60405161022e91906132a6565b61023f610e62565b61023f610f62565b61023f610578366004612fca565b610fb9565b61023f610fce565b61058e61271081565b60405161ffff909116815260200161022e565b61036c7f000000000000000000000000000000000000000000000000000000000000000081565b6101fa6105d6366004612ecb565b611040565b61036c6105e9366004612fca565b6111df565b6101fa611209565b6105fe61127e565b60405161022e939291906132b4565b6101fa61061b3660046132ed565b61145e565b61023f61062e366004612ecb565b600b6020526000908152604090205481565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146106b657337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b60405180910390fd5b6106bf816114e3565b50565b6106bf33826118a8565b6001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016330361070657610704611a29565b565b336000908152600b602052604081205461072090426133e4565b90506201518081101561076a57336000908152600b602052604090819020549051630429b06960e31b815260048101919091524260248201526201518060448201526064016106ad565b600061077533611b4c565b905080156107b35760405181815233907f249a82fbe5056a1940b4e996665ba2a82c340ae9fa1e069fd1ababf5508f396e9060200160405180910390a25b5050565b600e5460609081908190806001600160401b038111156107d9576107d9612d34565b604051908082528060200260200182016040528015610802578160200160208202803683370190505b509350806001600160401b0381111561081d5761081d612d34565b604051908082528060200260200182016040528015610846578160200160208202803683370190505b509250806001600160401b0381111561086157610861612d34565b60405190808252806020026020018201604052801561088a578160200160208202803683370190505b50915060005b81811015610966576000600e82815481106108ad576108ad6133f7565b60009182526020808320909101546001600160a01b0316808352600d909152604090912087519192509082908890859081106108eb576108eb6133f7565b60200260200101906001600160a01b031690816001600160a01b0316815250508060000154868481518110610922576109226133f7565b60209081029190910101526001810154855160ff9091169086908590811061094c5761094c6133f7565b911515602092830291909101909101525050600101610890565b5050909192565b600c54600090815b818110156109d4576000600c8281548110610992576109926133f7565b600091825260208083206002909202909101546001600160a01b0316808352600a9091526040909120549091506109c9908561340d565b935050600101610975565b505090565b600c81815481106109e957600080fd5b6000918252602090912060029091020180546001909101546001600160a01b0391821692501682565b6107b3338284611d49565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610a8a57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6106bf8161255d565b610ac26040518060800160405280600081526020016000815260200160008152602001600061ffff1681525090565b5060408051608081018252600254815260035460208201526004549181019190915260055461ffff16606082015290565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610b6057337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6107046125e7565b610b936040518060800160405280600081526020016000815260200160008152602001600081525090565b50604080516080810182526006548152600754602082015260085491810191909152600954606082015290565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610c2d57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b610c35611a29565b610c3e8561255d565b610c47846114e3565b610c50836128a5565b8015610c8157610c817f00000000000000000000000000000000000000000000000000000000000000008383611d49565b5050505050565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610cf557337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b610c8185858585856128bf565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610d6f57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6106bf816128a5565b6000828211610d9e5782826040516376675ed960e11b81526004016106ad9291906130a8565b82600003610dcf576004610db36001866133e4565b610dbd9190613420565b610dc890600161340d565b9050610e02565b6000610ddb84846133e4565b905080610de96001876133e4565b610df39190613420565b610dfe90600161340d565b9150505b9392505050565b610e11612c79565b600c8281548110610e2457610e246133f7565b60009182526020918290206040805180820190915260029092020180546001600160a01b039081168352600190910154169181019190915292915050565b600e5460009081908190815b81811015610ee5576000600e8281548110610e8b57610e8b6133f7565b60009182526020808320909101546001600160a01b0316808352600d909152604090912060018101549192509060ff16610edb578054610ecb908761340d565b9550610ed860018661340d565b94505b5050600101610e6e565b50610f5a83610ef261096d565b610f1c907f00000000000000000000000000000000000000000000000000000000000000006133e4565b610f2691906133e4565b600c54610f3490859061340d565b7f0000000000000000000000000000000000000000000000000000000000000000610d78565b935050505090565b600c54600090610f725750600090565b600a6000600c600081548110610f8a57610f8a6133f7565b600091825260208083206002909202909101546001600160a01b03168352820192909252604001902054905090565b6000610fc88260006001610d78565b92915050565b600e54600090815b818110156109d4576000600e8281548110610ff357610ff36133f7565b60009182526020808320909101546001600160a01b0316808352600d909152604090912060018101549192509060ff16611036578054611033908661340d565b94505b5050600101610fd6565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146110ad57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6003600f5460ff1660038111156110c6576110c6612ffd565b141580156110ea57506000600f5460ff1660038111156110e8576110e8612ffd565b145b1561110f57600f54604051632f3e69dd60e01b81526106ad9160ff1690600401613013565b6040516370a0823160e01b815281906000906001600160a01b038316906370a0823190611140903090600401613094565b602060405180830381865afa15801561115d573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906111819190613442565b9050600081116111a6578260405163e932c57360e01b81526004016106ad9190613094565b6111da6001600160a01b0383167f0000000000000000000000000000000000000000000000000000000000000000836129da565b505050565b600e81815481106111ef57600080fd5b6000918252602090912001546001600160a01b0316905081565b336001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000161461127657337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b610704611a29565b600c5460609081908190806001600160401b038111156112a0576112a0612d34565b6040519080825280602002602001820160405280156112c9578160200160208202803683370190505b509350806001600160401b038111156112e4576112e4612d34565b60405190808252806020026020018201604052801561130d578160200160208202803683370190505b509250806001600160401b0381111561132857611328612d34565b604051908082528060200260200182016040528015611351578160200160208202803683370190505b50915060005b81811015610966576000600c8281548110611374576113746133f7565b60009182526020909120600290910201805487519192506001600160a01b0316908790849081106113a7576113a76133f7565b6001600160a01b039283166020918202929092010152600182015486519116908690849081106113d9576113d96133f7565b60200260200101906001600160a01b031690816001600160a01b031681525050600a600087848151811061140f5761140f6133f7565b60200260200101516001600160a01b03166001600160a01b031681526020019081526020016000205484838151811061144a5761144a6133f7565b602090810291909101015250600101611357565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146114cb57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6114da87878787878787612a32565b50505050505050565b6000600f5460ff1660038111156114fc576114fc612ffd565b1461152157600f546040516312ba63f960e11b81526106ad9160ff1690600401613013565b600e5460005b8181101561158057600d6000600e8381548110611546576115466133f7565b60009182526020808320909101546001600160a01b0316835282019290925260400181209081556001908101805460ff1916905501611527565b5061158d600e6000612c90565b5080517f0000000000000000000000000000000000000000000000000000000000000000907f000000000000000000000000000000000000000000000000000000000000000010156116175781517f0000000000000000000000000000000000000000000000000000000000000000604051630962235760e31b81526004016106ad9291906130a8565b815160005b818110156118a257806000036116cd577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316848281518110611668576116686133f7565b6020026020010151600001516001600160a01b0316146116cd5760405163ef77b46760e01b8152600481018290526001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001660248201526044016106ad565b60006001600160a01b03168482815181106116ea576116ea6133f7565b6020026020010151600001516001600160a01b0316036117205760405163621caef560e11b8152600481018290526024016106ad565b6000600d6000868481518110611738576117386133f7565b6020026020010151600001516001600160a01b03166001600160a01b031681526020019081526020016000209050806000015460001461178e576040516308308c2360e21b8152600481018390526024016106ad565b60006117bb85847f0000000000000000000000000000000000000000000000000000000000000000610d78565b905060008684815181106117d1576117d16133f7565b6020026020010151602001519050818110156118065783818360405163c4aa8aa360e01b81526004016106ad9392919061345b565b8086101561182d57838187604051636d3ae3a760e01b81526004016106ad9392919061345b565b61183781876133e4565b9550600e87858151811061184d5761184d6133f7565b6020908102919091018101515182546001808201855560009485529290932090920180546001600160a01b0319166001600160a01b0390931692909217909155908355918201805460ff19169055500161161c565b50505050565b6001600f5460ff1660038111156118c1576118c1612ffd565b141580156118e657506002600f5460ff1660038111156118e3576118e3612ffd565b14155b1561190b576002546040516310004b0160e11b815260048101919091526024016106ad565b60006119178383612aa3565b600c549091506000908190815b818110156119bd576000600c8281548110611941576119416133f7565b6000918252602090912060029091020180549091506001600160a01b03808a169116036119b45760018101546001600160a01b03808816911603611989575050505050505050565b600190810180546001600160a01b038881166001600160a01b031983161790925516945092506119bd565b50600101611924565b50816119de5785604051632fd768d360e11b81526004016106ad9190613094565b856001600160a01b03167f25639ce407c98fba722dddf1f023c8242e3c77732cfe4660d1c04930bf4cbe728486604051611a19929190612fe3565b60405180910390a2505050505050565b600c5460005b81811015611aec576000600c8281548110611a4c57611a4c6133f7565b600091825260208083206002909202909101546001600160a01b0316808352600a9091526040909120549091506003600f5460ff166003811115611a9257611a92612ffd565b14158015611aa05750600081115b15611ad957611ad96001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001683836129da565b611ae282612ac1565b5050600101611a2f565b50611af9600c6000612cae565b600f805460ff191690556040805160008082526020820190925281611b40565b6040805180820190915260008082526020820152815260200190600190039081611b195790505b5090506107b3816114e3565b6001600160a01b0381166000908152600a602052604081205490819003611b7257919050565b611b7b82612ac1565b600c5460005b81811015611c9657600c8181548110611b9c57611b9c6133f7565b60009182526020909120600290910201546001600160a01b0390811690851603611c8e57600c611bcd6001846133e4565b81548110611bdd57611bdd6133f7565b9060005260206000209060020201600c8281548110611bfe57611bfe6133f7565b60009182526020909120825460029092020180546001600160a01b039283166001600160a01b0319918216178255600193840154939091018054939092169216919091179055600c805480611c5557611c55613471565b60008281526020902060026000199092019182020180546001600160a01b03199081168255600191909101805490911690559055611c96565b600101611b81565b506001600160a01b0383166000908152600d6020526040902060018101805460ff191690556003600f5460ff166003811115611cd457611cd4612ffd565b03611ce25760009250611d42565b611d166001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001685856129da565b6002600f5460ff166003811115611d2f57611d2f612ffd565b03611d4257600f805460ff191660011790555b5050919050565b6000600f5460ff166003811115611d6257611d62612ffd565b14158015611d8757506001600f5460ff166003811115611d8457611d84612ffd565b14155b15611dac57600f54604051630295f95f60e51b81526106ad9160ff1690600401613013565b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166386fa50636040518163ffffffff1660e01b8152600401602060405180830381865afa158015611e0a573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611e2e9190613442565b7f000000000000000000000000000000000000000000000000000000000000000014611f12577f00000000000000000000000000000000000000000000000000000000000000007f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166386fa50636040518163ffffffff1660e01b8152600401602060405180830381865afa158015611ed3573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611ef79190613442565b604051631248081b60e31b81526004016106ad9291906130a8565b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166356d399e86040518163ffffffff1660e01b8152600401602060405180830381865afa158015611f70573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611f949190613442565b7f000000000000000000000000000000000000000000000000000000000000000014612039577f00000000000000000000000000000000000000000000000000000000000000007f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166356d399e86040518163ffffffff1660e01b8152600401602060405180830381865afa158015611ed3573d6000803e3d6000fd5b6000600f5460ff16600381111561205257612052612ffd565b03612145577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316836001600160a01b0316146120cd57827f00000000000000000000000000000000000000000000000000000000000000006040516316a4ebc160e21b81526004016106ad929190612fe3565b600f805460ff1916600117905560025460055460405161ffff90911681526001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001691907fe52a01bb4ffcc3baf2c5d17111d7d5b676a7485809eb10671092a89d2bb22ff69060200160405180910390a35b6001600160a01b0383166000908152600d602052604090208054158015906121725750600181015460ff16155b156121b257805482101561219d57805460405163454cd8e160e01b81526106ad9184916004016130a8565b6001818101805460ff19169091179055612207565b6001600160a01b0384166000908152600a60205260409020541580156121de57506121db610e62565b82105b1561220757816121ec610e62565b6040516310e45ff160e21b81526004016106ad9291906130a8565b6001600160a01b0384166000908152600a602052604081205490036122d85760006122328585612aa3565b604080518082019091526001600160a01b03808816825291821660208201908152600c8054600181018255600091909152915160029092027fdf6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c7810180549385166001600160a01b031994851617905590517fdf6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c89091018054919093169116179055506122e2565b6122e284846118a8565b6001600160a01b0384166000908152600a60205260408120805484929061230a90849061340d565b90915550506001600160a01b0384166000908152600b6020526040812042905561233261096d565b9050600061233e610fce565b90507f000000000000000000000000000000000000000000000000000000000000000061236b828461340d565b11156123b05781817f000000000000000000000000000000000000000000000000000000000000000060405163513b428b60e11b81526004016106ad9392919061345b565b600c547f00000000000000000000000000000000000000000000000000000000000000001015612415576040516315b5c3d560e21b81527f000000000000000000000000000000000000000000000000000000000000000060048201526024016106ad565b7f000000000000000000000000000000000000000000000000000000000000000082036124a2576002546040516001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001691907f48996614f3380f3dcd5a2d2e6eaddfd66207ff3457fee339e09b4735bc9ec95890600090a3600f805460ff191660021790555b856001600160a01b03167fbdaa686eb6f59012d211a74523da260341c516896e9e5be954163d6ecf26ffa2856040516124dd91815260200190565b60405180910390a261251a6001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016873087612ae8565b6002600f5460ff16600381111561253357612533612ffd565b1480156125485750600f54610100900460ff16155b15612555576125556125e7565b505050505050565b6000600f5460ff16600381111561257657612576612ffd565b1461259b57600f5460405163ad88fc8f60e01b81526106ad9160ff1690600401613013565b61271061ffff821611156125cf57604051624ae8fd60e41b815261ffff8216600482015261271060248201526044016106ad565b6005805461ffff191661ffff92909216919091179055565b6002600f5460ff16600381111561260057612600612ffd565b1461262557600f546040516383696f6f60e01b81526106ad9160ff1690600401613013565b600f805460ff191660031790556002546040517f839cf22e1ba87ce2f5b9bbf46cf0175a09eed52febdfaac8852478e68203c76390600090a2600c546000816001600160401b0381111561267b5761267b612d34565b6040519080825280602002602001820160405280156126b457816020015b6126a1612ccf565b8152602001906001900390816126995790505b50905060005b8281101561274f576000600c82815481106126d7576126d76133f7565b6000918252602080832060408051608081018252600290940290910180546001600160a01b03908116858401818152600184015490921660608701529085528552600a83529320549082015284519192509084908490811061273b5761273b6133f7565b6020908102919091010152506001016126ba565b5060405163095ea7b360e01b81526001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169063095ea7b3906127de907f0000000000000000000000000000000000000000000000000000000000000000907f000000000000000000000000000000000000000000000000000000000000000090600401613487565b6020604051808303816000875af11580156127fd573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061282191906134a0565b50604051632f1fbfb360e21b81526001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169063bc7efecc90612877906000906006906002908790600401613504565b600060405180830381600087803b15801561289157600080fd5b505af1158015612555573d6000803e3d6000fd5b600f80549115156101000261ff0019909216919091179055565b6000600f5460ff1660038111156128d8576128d8612ffd565b146128fd57600f54604051639a0293fd60e01b81526106ad9160ff1690600401613013565b60405160016226579360e01b031981526001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169063ffd9a86d9061297290889088907f000000000000000000000000000000000000000000000000000000000000000090899060040161357b565b600060405180830381600087803b15801561298c57600080fd5b505af11580156129a0573d6000803e3d6000fd5b5050865160005550506020948501516001558351600655938301516007556040830151600855606090920151600955600255600355600455565b6111da83846001600160a01b031663a9059cbb8585604051602401612a00929190613487565b604051602081830303815290604052915060e01b6020820180516001600160e01b038381831617835250505050612b21565b612a3a611a29565b612a5387878760000151886020015189604001516128bf565b612a60856060015161255d565b612a69846114e3565b612a72836128a5565b80156114da576114da7f00000000000000000000000000000000000000000000000000000000000000008383611d49565b60006001600160a01b03821615612aba5781610e02565b5090919050565b6001600160a01b03166000908152600a60209081526040808320839055600b909152812055565b6040516001600160a01b0384811660248301528381166044830152606482018390526118a29186918216906323b872dd90608401612a00565b6000612b366001600160a01b03841683612b7b565b90508051600014158015612b5b575080806020019051810190612b5991906134a0565b155b156111da5782604051635274afe760e01b81526004016106ad9190613094565b6060610e028383600084600080856001600160a01b03168486604051612ba191906135b6565b60006040518083038185875af1925050503d8060008114612bde576040519150601f19603f3d011682016040523d82523d6000602084013e612be3565b606091505b5091509150612bf3868383612bfd565b9695505050505050565b606082612c1257612c0d82612c50565b610e02565b8151158015612c2957506001600160a01b0384163b155b15612c495783604051639996b31560e01b81526004016106ad9190613094565b5080610e02565b805115612c605780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b604080518082019091526000808252602082015290565b50805460008255906000526020600020908101906106bf9190612cef565b50805460008255600202906000526020600020908101906106bf9190612d08565b6040518060400160405280612ce2612c79565b8152602001600081525090565b5b80821115612d045760008155600101612cf0565b5090565b5b80821115612d045780546001600160a01b031990811682556001820180549091169055600201612d09565b634e487b7160e01b600052604160045260246000fd5b604080519081016001600160401b0381118282101715612d6c57612d6c612d34565b60405290565b604051608081016001600160401b0381118282101715612d6c57612d6c612d34565b604051601f8201601f191681016001600160401b0381118282101715612dbc57612dbc612d34565b604052919050565b80356001600160a01b0381168114612ddb57600080fd5b919050565b600082601f830112612df157600080fd5b81356001600160401b03811115612e0a57612e0a612d34565b612e1960208260051b01612d94565b8082825260208201915060208360061b860101925085831115612e3b57600080fd5b602085015b83811015612e855760408188031215612e5857600080fd5b612e60612d4a565b612e6982612dc4565b8152602082810135818301529084529290920191604001612e40565b5095945050505050565b600060208284031215612ea157600080fd5b81356001600160401b03811115612eb757600080fd5b612ec384828501612de0565b949350505050565b600060208284031215612edd57600080fd5b610e0282612dc4565b600081518084526020840193506020830160005b82811015612f215781516001600160a01b0316865260209586019590910190600101612efa565b5093949350505050565b600081518084526020840193506020830160005b82811015612f21578151865260209586019590910190600101612f3f565b606081526000612f706060830186612ee6565b8281036020840152612f828186612f2b565b83810360408501528451808252602080870193509091019060005b81811015612fbd5783511515835260209384019390920191600101612f9d565b5090979650505050505050565b600060208284031215612fdc57600080fd5b5035919050565b6001600160a01b0392831681529116602082015260400190565b634e487b7160e01b600052602160045260246000fd5b602081016004831061303557634e487b7160e01b600052602160045260246000fd5b91905290565b6000806040838503121561304e57600080fd5b8235915061305e60208401612dc4565b90509250929050565b803561ffff81168114612ddb57600080fd5b60006020828403121561308b57600080fd5b610e0282613067565b6001600160a01b0391909116815260200190565b918252602082015260400190565b805182526020810151602083015260408101516040830152606081015160608301525050565b60808101610fc882846130b6565b80151581146106bf57600080fd5b8035612ddb816130ea565b600080600080600060a0868803121561311b57600080fd5b61312486613067565b945060208601356001600160401b0381111561313f57600080fd5b61314b88828901612de0565b945050604086013561315c816130ea565b925061316a60608701612dc4565b949793965091946080013592915050565b60006040828403121561318d57600080fd5b613195612d4a565b823581526020928301359281019290925250919050565b6000608082840312156131be57600080fd5b6131c6612d72565b8235815260208084013590820152604080840135908201526060928301359281019290925250919050565b6000806000806000610120868803121561320a57600080fd5b613214878761317b565b945061322387604088016131ac565b949794965050505060c08301359260e081013592610100909101359150565b60006020828403121561325457600080fd5b8135610e02816130ea565b60008060006060848603121561327457600080fd5b505081359360208301359350604090920135919050565b80516001600160a01b03908116835260209182015116910152565b60408101610fc8828461328b565b6060815260006132c76060830186612ee6565b82810360208401526132d98186612ee6565b90508281036040840152612bf38185612f2b565b60008060008060008060008789036101c081121561330a57600080fd5b6133148a8a61317b565b97506133238a60408b016131ac565b9650608060bf198201121561333757600080fd5b50613340612d72565b60c0890135815260e0890135602082015261010089013560408201526133696101208a01613067565b606082015294506101408801356001600160401b0381111561338a57600080fd5b6133968a828b01612de0565b9450506133a661016089016130f8565b92506133b56101808901612dc4565b9699959850939692959194919350506101a09091013590565b634e487b7160e01b600052601160045260246000fd5b81810381811115610fc857610fc86133ce565b634e487b7160e01b600052603260045260246000fd5b80820180821115610fc857610fc86133ce565b60008261343d57634e487b7160e01b600052601260045260246000fd5b500490565b60006020828403121561345457600080fd5b5051919050565b9283526020830191909152604082015260600190565b634e487b7160e01b600052603160045260246000fd5b6001600160a01b03929092168252602082015260400190565b6000602082840312156134b257600080fd5b8151610e02816130ea565b600081518084526020840193506020830160005b82811015612f215781516134e687825161328b565b602090810151604088015260609096019591909101906001016134d1565b8454815260018501546020820152835460408201526001840154606082015260028401546080820152600384015460a0820152825460c0820152600183015460e0820152600283015461010082015261ffff6003840154166101208201526101606101408201526000612bf36101608301846134bd565b8451815260208086015190820152610100810161359b60408301866130b6565b6001600160a01b039390931660c082015260e0015292915050565b6000825160005b818110156135d757602081860181015185830152016135bd565b50600092019182525091905056fea164736f6c634300081a000a", + "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106101e25760003560e01c8062e9a407146101e75780630aaffd2a146101fc5780630d616d201461020f5780630d9639ba146102175780630dcf4b8f146102375780630ebb172a1461024d57806312fa329b1461026f578063200d2ed2146102905780632816ee73146102aa5780632c6cda93146102bd5780632c7baaf3146102d0578063313f336b1461030f5780633362379414610345578063356c299d1461037957806342e94c90146103b75780634564168b146103d75780634a387cdf146103f35780634bb278f3146103fb5780634e7320ce14610403578063565a9d481461041857806356d399e81461043a578063570ca7351461046157806358b810d3146104885780636786452c1461049b5780636cf72aa4146104ae5780637c0b4bfb146104c157806386fa5063146105005780638c7e7a31146105275780638dd085571461053a578063937e09b11461055a57806396a608c0146105625780639ca002a61461056a578063a4b6aa831461057d578063bc063e1a14610585578063c3aca558146105a1578063ccec3716146105c8578063d26146a5146105db578063d826f88f146105ee578063dda0a81a146105f6578063e020e4c41461060d578063e50d50d814610620575b600080fd5b6101fa6101f5366004612e8f565b610640565b005b6101fa61020a366004612ecb565b6106c2565b6101fa6106cc565b61021f6107b7565b60405161022e93929190612f5d565b60405180910390f35b61023f61096d565b60405190815260200161022e565b6102576201518081565b6040516001600160401b03909116815260200161022e565b61028261027d366004612fca565b6109d9565b60405161022e929190612fe3565b600f5461029d9060ff1681565b60405161022e9190613013565b6101fa6102b836600461303b565b610a12565b6101fa6102cb366004613079565b610a1d565b6102d8610a93565b60405161022e919081518152602080830151908201526040808301519082015260609182015161ffff169181019190915260800190565b6006546007546008546009546103259392919084565b60408051948552602085019390935291830152606082015260800161022e565b61036c7f000000000000000000000000000000000000000000000000000000000000000081565b60405161022e9190613094565b6002546003546004546005546103939392919061ffff1684565b6040805194855260208501939093529183015261ffff16606082015260800161022e565b61023f6103c5366004612ecb565b600a6020526000908152604090205481565b6000546001546103e5919082565b60405161022e9291906130a8565b600c5461023f565b6101fa610af3565b61040b610b68565b60405161022e91906130dc565b600f5461042a90610100900460ff1681565b604051901515815260200161022e565b61023f7f000000000000000000000000000000000000000000000000000000000000000081565b61036c7f000000000000000000000000000000000000000000000000000000000000000081565b6101fa610496366004613103565b610bc0565b6101fa6104a93660046131f1565b610c88565b6101fa6104bc366004613242565b610d02565b6104eb6104cf366004612ecb565b600d602052600090815260409020805460019091015460ff1682565b6040805192835290151560208301520161022e565b61023f7f000000000000000000000000000000000000000000000000000000000000000081565b61023f61053536600461325f565b610d78565b61054d610548366004612fca565b610e09565b60405161022e91906132a6565b61023f610e62565b61023f610f62565b61023f610578366004612fca565b610fb9565b61023f610fce565b61058e61271081565b60405161ffff909116815260200161022e565b61036c7f000000000000000000000000000000000000000000000000000000000000000081565b6101fa6105d6366004612ecb565b611040565b61036c6105e9366004612fca565b6111df565b6101fa611209565b6105fe61127e565b60405161022e939291906132b4565b6101fa61061b3660046132ed565b61145e565b61023f61062e366004612ecb565b600b6020526000908152604090205481565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146106b657337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b60405180910390fd5b6106bf816114e3565b50565b6106bf33826118a8565b6001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016330361070657610704611a29565b565b336000908152600b602052604081205461072090426133e4565b90506201518081101561076a57336000908152600b602052604090819020549051630429b06960e31b815260048101919091524260248201526201518060448201526064016106ad565b600061077533611b4c565b905080156107b35760405181815233907f249a82fbe5056a1940b4e996665ba2a82c340ae9fa1e069fd1ababf5508f396e9060200160405180910390a25b5050565b600e5460609081908190806001600160401b038111156107d9576107d9612d34565b604051908082528060200260200182016040528015610802578160200160208202803683370190505b509350806001600160401b0381111561081d5761081d612d34565b604051908082528060200260200182016040528015610846578160200160208202803683370190505b509250806001600160401b0381111561086157610861612d34565b60405190808252806020026020018201604052801561088a578160200160208202803683370190505b50915060005b81811015610966576000600e82815481106108ad576108ad6133f7565b60009182526020808320909101546001600160a01b0316808352600d909152604090912087519192509082908890859081106108eb576108eb6133f7565b60200260200101906001600160a01b031690816001600160a01b0316815250508060000154868481518110610922576109226133f7565b60209081029190910101526001810154855160ff9091169086908590811061094c5761094c6133f7565b911515602092830291909101909101525050600101610890565b5050909192565b600c54600090815b818110156109d4576000600c8281548110610992576109926133f7565b600091825260208083206002909202909101546001600160a01b0316808352600a9091526040909120549091506109c9908561340d565b935050600101610975565b505090565b600c81815481106109e957600080fd5b6000918252602090912060029091020180546001909101546001600160a01b0391821692501682565b6107b3338284611d49565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610a8a57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6106bf8161255d565b610ac26040518060800160405280600081526020016000815260200160008152602001600061ffff1681525090565b5060408051608081018252600254815260035460208201526004549181019190915260055461ffff16606082015290565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610b6057337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6107046125e7565b610b936040518060800160405280600081526020016000815260200160008152602001600081525090565b50604080516080810182526006548152600754602082015260085491810191909152600954606082015290565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610c2d57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b610c35611a29565b610c3e8561255d565b610c47846114e3565b610c50836128a5565b8015610c8157610c817f00000000000000000000000000000000000000000000000000000000000000008383611d49565b5050505050565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610cf557337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b610c8185858585856128bf565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610d6f57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6106bf816128a5565b6000828211610d9e5782826040516376675ed960e11b81526004016106ad9291906130a8565b82600003610dcf576004610db36001866133e4565b610dbd9190613420565b610dc890600161340d565b9050610e02565b6000610ddb84846133e4565b905080610de96001876133e4565b610df39190613420565b610dfe90600161340d565b9150505b9392505050565b610e11612c79565b600c8281548110610e2457610e246133f7565b60009182526020918290206040805180820190915260029092020180546001600160a01b039081168352600190910154169181019190915292915050565b600e5460009081908190815b81811015610ee5576000600e8281548110610e8b57610e8b6133f7565b60009182526020808320909101546001600160a01b0316808352600d909152604090912060018101549192509060ff16610edb578054610ecb908761340d565b9550610ed860018661340d565b94505b5050600101610e6e565b50610f5a83610ef261096d565b610f1c907f00000000000000000000000000000000000000000000000000000000000000006133e4565b610f2691906133e4565b600c54610f3490859061340d565b7f0000000000000000000000000000000000000000000000000000000000000000610d78565b935050505090565b600c54600090610f725750600090565b600a6000600c600081548110610f8a57610f8a6133f7565b600091825260208083206002909202909101546001600160a01b03168352820192909252604001902054905090565b6000610fc88260006001610d78565b92915050565b600e54600090815b818110156109d4576000600e8281548110610ff357610ff36133f7565b60009182526020808320909101546001600160a01b0316808352600d909152604090912060018101549192509060ff16611036578054611033908661340d565b94505b5050600101610fd6565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146110ad57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6003600f5460ff1660038111156110c6576110c6612ffd565b141580156110ea57506000600f5460ff1660038111156110e8576110e8612ffd565b145b1561110f57600f54604051632f3e69dd60e01b81526106ad9160ff1690600401613013565b6040516370a0823160e01b815281906000906001600160a01b038316906370a0823190611140903090600401613094565b602060405180830381865afa15801561115d573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906111819190613442565b9050600081116111a6578260405163e932c57360e01b81526004016106ad9190613094565b6111da6001600160a01b0383167f0000000000000000000000000000000000000000000000000000000000000000836129da565b505050565b600e81815481106111ef57600080fd5b6000918252602090912001546001600160a01b0316905081565b336001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000161461127657337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b610704611a29565b600c5460609081908190806001600160401b038111156112a0576112a0612d34565b6040519080825280602002602001820160405280156112c9578160200160208202803683370190505b509350806001600160401b038111156112e4576112e4612d34565b60405190808252806020026020018201604052801561130d578160200160208202803683370190505b509250806001600160401b0381111561132857611328612d34565b604051908082528060200260200182016040528015611351578160200160208202803683370190505b50915060005b81811015610966576000600c8281548110611374576113746133f7565b60009182526020909120600290910201805487519192506001600160a01b0316908790849081106113a7576113a76133f7565b6001600160a01b039283166020918202929092010152600182015486519116908690849081106113d9576113d96133f7565b60200260200101906001600160a01b031690816001600160a01b031681525050600a600087848151811061140f5761140f6133f7565b60200260200101516001600160a01b03166001600160a01b031681526020019081526020016000205484838151811061144a5761144a6133f7565b602090810291909101015250600101611357565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146114cb57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6114da87878787878787612a32565b50505050505050565b6000600f5460ff1660038111156114fc576114fc612ffd565b1461152157600f546040516312ba63f960e11b81526106ad9160ff1690600401613013565b600e5460005b8181101561158057600d6000600e8381548110611546576115466133f7565b60009182526020808320909101546001600160a01b0316835282019290925260400181209081556001908101805460ff1916905501611527565b5061158d600e6000612c90565b5080517f0000000000000000000000000000000000000000000000000000000000000000907f000000000000000000000000000000000000000000000000000000000000000010156116175781517f0000000000000000000000000000000000000000000000000000000000000000604051630962235760e31b81526004016106ad9291906130a8565b815160005b818110156118a257806000036116cd577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316848281518110611668576116686133f7565b6020026020010151600001516001600160a01b0316146116cd5760405163ef77b46760e01b8152600481018290526001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001660248201526044016106ad565b60006001600160a01b03168482815181106116ea576116ea6133f7565b6020026020010151600001516001600160a01b0316036117205760405163621caef560e11b8152600481018290526024016106ad565b6000600d6000868481518110611738576117386133f7565b6020026020010151600001516001600160a01b03166001600160a01b031681526020019081526020016000209050806000015460001461178e576040516308308c2360e21b8152600481018390526024016106ad565b60006117bb85847f0000000000000000000000000000000000000000000000000000000000000000610d78565b905060008684815181106117d1576117d16133f7565b6020026020010151602001519050818110156118065783818360405163c4aa8aa360e01b81526004016106ad9392919061345b565b8086101561182d57838187604051636d3ae3a760e01b81526004016106ad9392919061345b565b61183781876133e4565b9550600e87858151811061184d5761184d6133f7565b6020908102919091018101515182546001808201855560009485529290932090920180546001600160a01b0319166001600160a01b0390931692909217909155908355918201805460ff19169055500161161c565b50505050565b6001600f5460ff1660038111156118c1576118c1612ffd565b141580156118e657506002600f5460ff1660038111156118e3576118e3612ffd565b14155b1561190b576002546040516310004b0160e11b815260048101919091526024016106ad565b60006119178383612aa3565b600c549091506000908190815b818110156119bd576000600c8281548110611941576119416133f7565b6000918252602090912060029091020180549091506001600160a01b03808a169116036119b45760018101546001600160a01b03808816911603611989575050505050505050565b600190810180546001600160a01b038881166001600160a01b031983161790925516945092506119bd565b50600101611924565b50816119de5785604051632fd768d360e11b81526004016106ad9190613094565b856001600160a01b03167f25639ce407c98fba722dddf1f023c8242e3c77732cfe4660d1c04930bf4cbe728486604051611a19929190612fe3565b60405180910390a2505050505050565b600c5460005b81811015611aec576000600c8281548110611a4c57611a4c6133f7565b600091825260208083206002909202909101546001600160a01b0316808352600a9091526040909120549091506003600f5460ff166003811115611a9257611a92612ffd565b14158015611aa05750600081115b15611ad957611ad96001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001683836129da565b611ae282612ac1565b5050600101611a2f565b50611af9600c6000612cae565b600f805460ff191690556040805160008082526020820190925281611b40565b6040805180820190915260008082526020820152815260200190600190039081611b195790505b5090506107b3816114e3565b6001600160a01b0381166000908152600a602052604081205490819003611b7257919050565b611b7b82612ac1565b600c5460005b81811015611c9657600c8181548110611b9c57611b9c6133f7565b60009182526020909120600290910201546001600160a01b0390811690851603611c8e57600c611bcd6001846133e4565b81548110611bdd57611bdd6133f7565b9060005260206000209060020201600c8281548110611bfe57611bfe6133f7565b60009182526020909120825460029092020180546001600160a01b039283166001600160a01b0319918216178255600193840154939091018054939092169216919091179055600c805480611c5557611c55613471565b60008281526020902060026000199092019182020180546001600160a01b03199081168255600191909101805490911690559055611c96565b600101611b81565b506001600160a01b0383166000908152600d6020526040902060018101805460ff191690556003600f5460ff166003811115611cd457611cd4612ffd565b03611ce25760009250611d42565b611d166001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001685856129da565b6002600f5460ff166003811115611d2f57611d2f612ffd565b03611d4257600f805460ff191660011790555b5050919050565b6000600f5460ff166003811115611d6257611d62612ffd565b14158015611d8757506001600f5460ff166003811115611d8457611d84612ffd565b14155b15611dac57600f54604051630295f95f60e51b81526106ad9160ff1690600401613013565b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166386fa50636040518163ffffffff1660e01b8152600401602060405180830381865afa158015611e0a573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611e2e9190613442565b7f000000000000000000000000000000000000000000000000000000000000000014611f12577f00000000000000000000000000000000000000000000000000000000000000007f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166386fa50636040518163ffffffff1660e01b8152600401602060405180830381865afa158015611ed3573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611ef79190613442565b604051631248081b60e31b81526004016106ad9291906130a8565b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166356d399e86040518163ffffffff1660e01b8152600401602060405180830381865afa158015611f70573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611f949190613442565b7f000000000000000000000000000000000000000000000000000000000000000014612039577f00000000000000000000000000000000000000000000000000000000000000007f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166356d399e86040518163ffffffff1660e01b8152600401602060405180830381865afa158015611ed3573d6000803e3d6000fd5b6000600f5460ff16600381111561205257612052612ffd565b03612145577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316836001600160a01b0316146120cd57827f00000000000000000000000000000000000000000000000000000000000000006040516316a4ebc160e21b81526004016106ad929190612fe3565b600f805460ff1916600117905560025460055460405161ffff90911681526001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001691907fe52a01bb4ffcc3baf2c5d17111d7d5b676a7485809eb10671092a89d2bb22ff69060200160405180910390a35b6001600160a01b0383166000908152600d602052604090208054158015906121725750600181015460ff16155b156121b257805482101561219d57805460405163454cd8e160e01b81526106ad9184916004016130a8565b6001818101805460ff19169091179055612207565b6001600160a01b0384166000908152600a60205260409020541580156121de57506121db610e62565b82105b1561220757816121ec610e62565b6040516310e45ff160e21b81526004016106ad9291906130a8565b6001600160a01b0384166000908152600a602052604081205490036122d85760006122328585612aa3565b604080518082019091526001600160a01b03808816825291821660208201908152600c8054600181018255600091909152915160029092027fdf6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c7810180549385166001600160a01b031994851617905590517fdf6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c89091018054919093169116179055506122e2565b6122e284846118a8565b6001600160a01b0384166000908152600a60205260408120805484929061230a90849061340d565b90915550506001600160a01b0384166000908152600b6020526040812042905561233261096d565b9050600061233e610fce565b90507f000000000000000000000000000000000000000000000000000000000000000061236b828461340d565b11156123b05781817f000000000000000000000000000000000000000000000000000000000000000060405163513b428b60e11b81526004016106ad9392919061345b565b600c547f00000000000000000000000000000000000000000000000000000000000000001015612415576040516315b5c3d560e21b81527f000000000000000000000000000000000000000000000000000000000000000060048201526024016106ad565b7f000000000000000000000000000000000000000000000000000000000000000082036124a2576002546040516001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001691907f48996614f3380f3dcd5a2d2e6eaddfd66207ff3457fee339e09b4735bc9ec95890600090a3600f805460ff191660021790555b856001600160a01b03167fbdaa686eb6f59012d211a74523da260341c516896e9e5be954163d6ecf26ffa2856040516124dd91815260200190565b60405180910390a261251a6001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016873087612ae8565b6002600f5460ff16600381111561253357612533612ffd565b1480156125485750600f54610100900460ff16155b15612555576125556125e7565b505050505050565b6000600f5460ff16600381111561257657612576612ffd565b1461259b57600f5460405163ad88fc8f60e01b81526106ad9160ff1690600401613013565b61271061ffff821611156125cf57604051624ae8fd60e41b815261ffff8216600482015261271060248201526044016106ad565b6005805461ffff191661ffff92909216919091179055565b6002600f5460ff16600381111561260057612600612ffd565b1461262557600f546040516383696f6f60e01b81526106ad9160ff1690600401613013565b600f805460ff191660031790556002546040517f839cf22e1ba87ce2f5b9bbf46cf0175a09eed52febdfaac8852478e68203c76390600090a2600c546000816001600160401b0381111561267b5761267b612d34565b6040519080825280602002602001820160405280156126b457816020015b6126a1612ccf565b8152602001906001900390816126995790505b50905060005b8281101561274f576000600c82815481106126d7576126d76133f7565b6000918252602080832060408051608081018252600290940290910180546001600160a01b03908116858401818152600184015490921660608701529085528552600a83529320549082015284519192509084908490811061273b5761273b6133f7565b6020908102919091010152506001016126ba565b5060405163095ea7b360e01b81526001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169063095ea7b3906127de907f0000000000000000000000000000000000000000000000000000000000000000907f000000000000000000000000000000000000000000000000000000000000000090600401613487565b6020604051808303816000875af11580156127fd573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061282191906134a0565b50604051632f1fbfb360e21b81526001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169063bc7efecc90612877906000906006906002908790600401613504565b600060405180830381600087803b15801561289157600080fd5b505af1158015612555573d6000803e3d6000fd5b600f80549115156101000261ff0019909216919091179055565b6000600f5460ff1660038111156128d8576128d8612ffd565b146128fd57600f54604051639a0293fd60e01b81526106ad9160ff1690600401613013565b60405160016226579360e01b031981526001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169063ffd9a86d9061297290889088907f000000000000000000000000000000000000000000000000000000000000000090899060040161357b565b600060405180830381600087803b15801561298c57600080fd5b505af11580156129a0573d6000803e3d6000fd5b5050865160005550506020948501516001558351600655938301516007556040830151600855606090920151600955600255600355600455565b6111da83846001600160a01b031663a9059cbb8585604051602401612a00929190613487565b604051602081830303815290604052915060e01b6020820180516001600160e01b038381831617835250505050612b21565b612a3a611a29565b612a5387878760000151886020015189604001516128bf565b612a60856060015161255d565b612a69846114e3565b612a72836128a5565b80156114da576114da7f00000000000000000000000000000000000000000000000000000000000000008383611d49565b60006001600160a01b03821615612aba5781610e02565b5090919050565b6001600160a01b03166000908152600a60209081526040808320839055600b909152812055565b6040516001600160a01b0384811660248301528381166044830152606482018390526118a29186918216906323b872dd90608401612a00565b6000612b366001600160a01b03841683612b7b565b90508051600014158015612b5b575080806020019051810190612b5991906134a0565b155b156111da5782604051635274afe760e01b81526004016106ad9190613094565b6060610e028383600084600080856001600160a01b03168486604051612ba191906135b6565b60006040518083038185875af1925050503d8060008114612bde576040519150601f19603f3d011682016040523d82523d6000602084013e612be3565b606091505b5091509150612bf3868383612bfd565b9695505050505050565b606082612c1257612c0d82612c50565b610e02565b8151158015612c2957506001600160a01b0384163b155b15612c495783604051639996b31560e01b81526004016106ad9190613094565b5080610e02565b805115612c605780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b604080518082019091526000808252602082015290565b50805460008255906000526020600020908101906106bf9190612cef565b50805460008255600202906000526020600020908101906106bf9190612d08565b6040518060400160405280612ce2612c79565b8152602001600081525090565b5b80821115612d045760008155600101612cf0565b5090565b5b80821115612d045780546001600160a01b031990811682556001820180549091169055600201612d09565b634e487b7160e01b600052604160045260246000fd5b604080519081016001600160401b0381118282101715612d6c57612d6c612d34565b60405290565b604051608081016001600160401b0381118282101715612d6c57612d6c612d34565b604051601f8201601f191681016001600160401b0381118282101715612dbc57612dbc612d34565b604052919050565b80356001600160a01b0381168114612ddb57600080fd5b919050565b600082601f830112612df157600080fd5b81356001600160401b03811115612e0a57612e0a612d34565b612e1960208260051b01612d94565b8082825260208201915060208360061b860101925085831115612e3b57600080fd5b602085015b83811015612e855760408188031215612e5857600080fd5b612e60612d4a565b612e6982612dc4565b8152602082810135818301529084529290920191604001612e40565b5095945050505050565b600060208284031215612ea157600080fd5b81356001600160401b03811115612eb757600080fd5b612ec384828501612de0565b949350505050565b600060208284031215612edd57600080fd5b610e0282612dc4565b600081518084526020840193506020830160005b82811015612f215781516001600160a01b0316865260209586019590910190600101612efa565b5093949350505050565b600081518084526020840193506020830160005b82811015612f21578151865260209586019590910190600101612f3f565b606081526000612f706060830186612ee6565b8281036020840152612f828186612f2b565b83810360408501528451808252602080870193509091019060005b81811015612fbd5783511515835260209384019390920191600101612f9d565b5090979650505050505050565b600060208284031215612fdc57600080fd5b5035919050565b6001600160a01b0392831681529116602082015260400190565b634e487b7160e01b600052602160045260246000fd5b602081016004831061303557634e487b7160e01b600052602160045260246000fd5b91905290565b6000806040838503121561304e57600080fd5b8235915061305e60208401612dc4565b90509250929050565b803561ffff81168114612ddb57600080fd5b60006020828403121561308b57600080fd5b610e0282613067565b6001600160a01b0391909116815260200190565b918252602082015260400190565b805182526020810151602083015260408101516040830152606081015160608301525050565b60808101610fc882846130b6565b80151581146106bf57600080fd5b8035612ddb816130ea565b600080600080600060a0868803121561311b57600080fd5b61312486613067565b945060208601356001600160401b0381111561313f57600080fd5b61314b88828901612de0565b945050604086013561315c816130ea565b925061316a60608701612dc4565b949793965091946080013592915050565b60006040828403121561318d57600080fd5b613195612d4a565b823581526020928301359281019290925250919050565b6000608082840312156131be57600080fd5b6131c6612d72565b8235815260208084013590820152604080840135908201526060928301359281019290925250919050565b6000806000806000610120868803121561320a57600080fd5b613214878761317b565b945061322387604088016131ac565b949794965050505060c08301359260e081013592610100909101359150565b60006020828403121561325457600080fd5b8135610e02816130ea565b60008060006060848603121561327457600080fd5b505081359360208301359350604090920135919050565b80516001600160a01b03908116835260209182015116910152565b60408101610fc8828461328b565b6060815260006132c76060830186612ee6565b82810360208401526132d98186612ee6565b90508281036040840152612bf38185612f2b565b60008060008060008060008789036101c081121561330a57600080fd5b6133148a8a61317b565b97506133238a60408b016131ac565b9650608060bf198201121561333757600080fd5b50613340612d72565b60c0890135815260e0890135602082015261010089013560408201526133696101208a01613067565b606082015294506101408801356001600160401b0381111561338a57600080fd5b6133968a828b01612de0565b9450506133a661016089016130f8565b92506133b56101808901612dc4565b9699959850939692959194919350506101a09091013590565b634e487b7160e01b600052601160045260246000fd5b81810381811115610fc857610fc86133ce565b634e487b7160e01b600052603260045260246000fd5b80820180821115610fc857610fc86133ce565b60008261343d57634e487b7160e01b600052601260045260246000fd5b500490565b60006020828403121561345457600080fd5b5051919050565b9283526020830191909152604082015260600190565b634e487b7160e01b600052603160045260246000fd5b6001600160a01b03929092168252602082015260400190565b6000602082840312156134b257600080fd5b8151610e02816130ea565b600081518084526020840193506020830160005b82811015612f215781516134e687825161328b565b602090810151604088015260609096019591909101906001016134d1565b8454815260018501546020820152835460408201526001840154606082015260028401546080820152600384015460a0820152825460c0820152600183015460e0820152600283015461010082015261ffff6003840154166101208201526101606101408201526000612bf36101608301846134bd565b8451815260208086015190820152610100810161359b60408301866130b6565b6001600160a01b039390931660c082015260e0015292915050565b6000825160005b818110156135d757602081860181015185830152016135bd565b50600092019182525091905056fea164736f6c634300081a000a", "linkReferences": {}, "deployedLinkReferences": {} } diff --git a/src/web3client/abis/ServiceNodeContributionFactory.json b/src/web3client/abis/ServiceNodeContributionFactory.json index e1aac29..68ab48d 100644 --- a/src/web3client/abis/ServiceNodeContributionFactory.json +++ b/src/web3client/abis/ServiceNodeContributionFactory.json @@ -393,8 +393,8 @@ "type": "function" } ], - "bytecode": "0x6080604052348015600f57600080fd5b50615ec18061001f6000396000f3fe608060405234801561001057600080fd5b50600436106100af5760003560e01c806333623794146100b45780633f4ba83a146100dd5780635c975abb146100e7578063715018a6146100ff57806379ba5097146101075780638456cb591461010f5780638da5cb5b146101175780638faff8621461011f578063c4d66de814610132578063c70242ad14610145578063e30c397814610168578063f2fde38b14610170578063f7bc39bf14610183575b600080fd5b6000546100c7906001600160a01b031681565b6040516100d49190610809565b60405180910390f35b6100e56101af565b005b6100ef6101c1565b60405190151581526020016100d4565b6100e56101d6565b6100e56101e8565b6100e5610230565b6100c7610240565b6100c761012d36600461084a565b61025b565b6100e5610140366004610925565b610387565b6100ef610153366004610925565b60016020526000908152604090205460ff1681565b6100c76104a3565b6100e561017e366004610925565b6104ae565b6100ef610191366004610925565b6001600160a01b031660009081526001602052604090205460ff1690565b6101b761051f565b6101bf610551565b565b6000806101cc6105a8565b5460ff1692915050565b6101de61051f565b6101bf60006105cc565b33806101f26104a3565b6001600160a01b031614610224578060405163118cdaa760e01b815260040161021b9190610809565b60405180910390fd5b61022d816105cc565b50565b61023861051f565b6101bf6105f3565b60008061024b61063a565b546001600160a01b031692915050565b600061026561065e565b60008054604080516386fa506360e01b815290516001600160a01b039092169182916386fa50639160048083019260209291908290030181865afa1580156102b1573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906102d59190610947565b8989898989896040516102e7906107fc565b6102f89897969594939291906109eb565b604051809103906000f080158015610314573d6000803e3d6000fd5b506001600160a01b038116600081815260016020819052604091829020805460ff1916909117905551919350839250907feac84630ba02e5ab324a651281c90ec45563a21f07fdf52b6f601f312e2de27a90610374908935815260200190565b60405180910390a2509695505050505050565b6000610391610684565b805490915060ff600160401b82041615906001600160401b03166000811580156103b85750825b90506000826001600160401b031660011480156103d45750303b155b9050811580156103e2575080155b156104005760405163f92ee8a960e01b815260040160405180910390fd5b84546001600160401b0319166001178555831561042957845460ff60401b1916600160401b1785555b600080546001600160a01b0319166001600160a01b03881617905561044d336106a8565b6104556106b9565b831561049b57845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b505050505050565b60008061024b6106c9565b6104b661051f565b60006104c06106c9565b80546001600160a01b0319166001600160a01b03841690811782559091506104e6610240565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b33610528610240565b6001600160a01b0316146101bf573360405163118cdaa760e01b815260040161021b9190610809565b6105596106ed565b60006105636105a8565b805460ff1916815590507f5db9ee0a495bf2e6ff9c91a7834c1ba4fdd244a5e8aa4e537bd38aeae4b073aa335b60405161059d9190610809565b60405180910390a150565b7fcd5ed15c6e187e77e9aee88184c21f4f2182ab5827cb3b7e07fbedcd63f0330090565b60006105d66106c9565b80546001600160a01b031916815590506105ef82610712565b5050565b6105fb61065e565b60006106056105a8565b805460ff1916600117815590507f62e78cea01bee320cd4e420270b5ea74000d11b0c9f74754ebdbfc544b05a2586105903390565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930090565b6106666101c1565b156101bf5760405163d93c066560e01b815260040160405180910390fd5b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0090565b6106b061076e565b61022d81610793565b6106c161076e565b6101bf6107c5565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0090565b6106f56101c1565b6101bf57604051638dfc202b60e01b815260040160405180910390fd5b600061071c61063a565b80546001600160a01b038481166001600160a01b031983168117845560405193945091169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a3505050565b6107766107e2565b6101bf57604051631afcd79f60e31b815260040160405180910390fd5b61079b61076e565b6001600160a01b038116610224576000604051631e4fbdf760e01b815260040161021b9190610809565b6107cd61076e565b60006107d76105a8565b805460ff1916905550565b60006107ec610684565b54600160401b900460ff16919050565b61543d80610a7883390190565b6001600160a01b0391909116815260200190565b60006080828403121561082f57600080fd5b50919050565b8035801515811461084557600080fd5b919050565b60008060008060008086880361018081121561086557600080fd5b604081121561087357600080fd5b50869550610884886040890161081d565b94506108938860c0890161081d565b93506101408701356001600160401b038111156108af57600080fd5b8701601f810189136108c057600080fd5b80356001600160401b038111156108d657600080fd5b8960208260061b84010111156108eb57600080fd5b602091909101935091506109026101608801610835565b90509295509295509295565b80356001600160a01b038116811461084557600080fd5b60006020828403121561093757600080fd5b6109408261090e565b9392505050565b60006020828403121561095957600080fd5b5051919050565b803582526020808201359083015260408082013590830152606081013561ffff811680821461098e57600080fd5b80606085015250505050565b81835260208301925060008160005b848110156109e1576001600160a01b036109c28361090e565b16865260208281013590870152604095860195909101906001016109a9565b5093949350505050565b6001600160a01b03891681526020808201899052873560408084019190915288820135606080850191909152883560808501529188013560a084015287013560c083015286013560e08201526000610a47610100830187610960565b6101c0610180830152610a5f6101c08301858761099a565b8315156101a08401529050999850505050505050505056fe610120604052600f805460ff1916905534801561001b57600080fd5b5060405161543d38038061543d83398101604081905261003a9161196c565b866001600160a01b0381166100a45760405162461bcd60e51b815260206004820152602560248201527f5368617265643a205a65726f2d61646472657373206973206e6f74207065726d6044820152641a5d1d195960da1b60648201526084015b60405180910390fd5b86806000036100f55760405162461bcd60e51b815260206004820152601b60248201527f5368617265643a2075696e7420696e70757420697320656d7074790000000000604482015260640161009b565b6001600160a01b03891660a081905260408051630ada733d60e31b815290516356d399e8916004808201926020929091908290030181865afa15801561013f573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101639190611a30565b60c0818152505060a0516001600160a01b0316637c89d2f06040518163ffffffff1660e01b8152600401602060405180830381865afa1580156101aa573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101ce9190611a49565b6001600160a01b03166080526101008890523260e05260006101f588888888888680610204565b50505050505050505050611c7f565b61020c61026c565b845160208601516040870151610225928a928a92610398565b6060850151610233906104ad565b61023c84610537565b600f805461ff0019166101008515150217905580156102635760e051610263908383610859565b50505050505050565b600c5460005b81811015610334576000600c828154811061028f5761028f611a66565b600091825260208083206002909202909101546001600160a01b0316808352600a9091526040909120549091506003600f5460ff1660038111156102d5576102d5611751565b141580156102e35750600081115b156102ff576080516102ff906001600160a01b03168383610e71565b61032a826001600160a01b03166000908152600a60209081526040808320839055600b909152812055565b5050600101610272565b50610341600c60006116cd565b600f805460ff191690556040805160008082526020820190925281610388565b60408051808201909152600080825260208201528152602001906001900390816103615790505b50905061039481610537565b5050565b6000600f5460ff1660038111156103b1576103b1611751565b146103d657600f54604051639a0293fd60e01b815261009b9160ff1690600401611a7c565b60a05160e0516040805160016226579360e01b03198152885160048201526020808a01516024830152885160448301528801516064820152908701516084820152606087015160a48201526001600160a01b0391821660c482015260e4810186905291169063ffd9a86d9061010401600060405180830381600087803b15801561045f57600080fd5b505af1158015610473573d6000803e3d6000fd5b5050865160005550506020948501516001558351600655938301516007556040830151600855606090920151600955600255600355600455565b6000600f5460ff1660038111156104c6576104c6611751565b146104eb57600f5460405163ad88fc8f60e01b815261009b9160ff1690600401611a7c565b61271061ffff8216111561051f57604051624ae8fd60e41b815261ffff82166004820152612710602482015260440161009b565b6005805461ffff191661ffff92909216919091179055565b6000600f5460ff16600381111561055057610550611751565b1461057557600f546040516312ba63f960e11b815261009b9160ff1690600401611a7c565b600e5460005b818110156105d457600d6000600e838154811061059a5761059a611a66565b60009182526020808320909101546001600160a01b0316835282019290925260400181209081556001908101805460ff191690550161057b565b506105e1600e60006116ee565b5060c051610100518251111561061957815161010051604051630962235760e31b81526004810192909252602482015260440161009b565b815160005b8181101561085357806000036106955760e0516001600160a01b031684828151811061064c5761064c611a66565b6020026020010151600001516001600160a01b0316146106955760e05160405163ef77b46760e01b8152600481018390526001600160a01b03909116602482015260440161009b565b60006001600160a01b03168482815181106106b2576106b2611a66565b6020026020010151600001516001600160a01b0316036106e85760405163621caef560e11b81526004810182905260240161009b565b6000600d600086848151811061070057610700611a66565b6020026020010151600001516001600160a01b03166001600160a01b0316815260200190815260200160002090508060000154600014610756576040516308308c2360e21b81526004810183905260240161009b565b600061076c858461010051610ece60201b60201c565b9050600086848151811061078257610782611a66565b6020026020010151602001519050818110156107b75783818360405163c4aa8aa360e01b815260040161009b93929190611aa4565b808610156107de57838187604051636d3ae3a760e01b815260040161009b93929190611aa4565b6107e88187611ad0565b9550600e8785815181106107fe576107fe611a66565b6020908102919091018101515182546001808201855560009485529290932090920180546001600160a01b0319166001600160a01b0390931692909217909155908355918201805460ff19169055500161061e565b50505050565b6000600f5460ff16600381111561087257610872611751565b1415801561089757506001600f5460ff16600381111561089457610894611751565b14155b156108bc57600f54604051630295f95f60e51b815261009b9160ff1690600401611a7c565b60a0516001600160a01b03166386fa50636040518163ffffffff1660e01b8152600401602060405180830381865afa1580156108fc573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109209190611a30565b61010051146109b3576101005160a0516001600160a01b03166386fa50636040518163ffffffff1660e01b8152600401602060405180830381865afa15801561096d573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109919190611a30565b604051631248081b60e31b81526004810192909252602482015260440161009b565b60a0516001600160a01b03166356d399e86040518163ffffffff1660e01b8152600401602060405180830381865afa1580156109f3573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610a179190611a30565b60c05114610a625760c05160a0516001600160a01b03166356d399e86040518163ffffffff1660e01b8152600401602060405180830381865afa15801561096d573d6000803e3d6000fd5b6000600f5460ff166003811115610a7b57610a7b611751565b03610b145760e0516001600160a01b0316836001600160a01b031614610ab95760e0516040516316a4ebc160e21b815261009b918591600401611ae3565b600f805460ff1916600117905560e05160025460055460405161ffff90911681526001600160a01b03909216917fe52a01bb4ffcc3baf2c5d17111d7d5b676a7485809eb10671092a89d2bb22ff69060200160405180910390a35b6001600160a01b0383166000908152600d60205260409020805415801590610b415750600181015460ff16155b15610b8a578054821015610b7557805460405163454cd8e160e01b815261009b918491600401918252602082015260400190565b6001818101805460ff19169091179055610be6565b6001600160a01b0384166000908152600a6020526040902054158015610bb65750610bb3610f65565b82105b15610be65781610bc4610f65565b6040516310e45ff160e21b81526004810192909252602482015260440161009b565b6001600160a01b0384166000908152600a60205260408120549003610cb7576000610c11858561102b565b604080518082019091526001600160a01b03808816825291821660208201908152600c8054600181018255600091909152915160029092027fdf6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c7810180549385166001600160a01b031994851617905590517fdf6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c8909101805491909316911617905550610cc1565b610cc18484611049565b6001600160a01b0384166000908152600a602052604081208054849290610ce9908490611afd565b90915550506001600160a01b0384166000908152600b60205260408120429055610d116111ca565b90506000610d1d611236565b60c051909150610d2d8284611afd565b1115610d5457818160c05160405163513b428b60e11b815260040161009b93929190611aa4565b61010051600c541115610d8157610100516040516315b5c3d560e21b815260040161009b91815260200190565b60c0518203610dd35760e0516002546040516001600160a01b03909216917f48996614f3380f3dcd5a2d2e6eaddfd66207ff3457fee339e09b4735bc9ec95890600090a3600f805460ff191660021790555b856001600160a01b03167fbdaa686eb6f59012d211a74523da260341c516896e9e5be954163d6ecf26ffa285604051610e0e91815260200190565b60405180910390a2608051610e2e906001600160a01b03168730876112a8565b6002600f5460ff166003811115610e4757610e47611751565b148015610e5c5750600f54610100900460ff16155b15610e6957610e696112e1565b505050505050565b610ec983846001600160a01b031663a9059cbb8585604051602401610e97929190611b10565b60408051808303601f1901815291905260208101805160e09390931b6001600160e01b03938416179052915061154216565b505050565b6000828211610efa576040516376675ed960e11b8152600481018490526024810183905260440161009b565b82600003610f2b576004610f0f600186611ad0565b610f199190611b29565b610f24906001611afd565b9050610f5e565b6000610f378484611ad0565b905080610f45600187611ad0565b610f4f9190611b29565b610f5a906001611afd565b9150505b9392505050565b600e5460009081908190815b81811015610fe8576000600e8281548110610f8e57610f8e611a66565b60009182526020808320909101546001600160a01b0316808352600d909152604090912060018101549192509060ff16610fde578054610fce9087611afd565b9550610fdb600186611afd565b94505b5050600101610f71565b5061102383610ff56111ca565b60c0516110029190611ad0565b61100c9190611ad0565b600c5461101a908590611afd565b61010051610ece565b935050505090565b60006001600160a01b038216156110425781610f5e565b5090919050565b6001600f5460ff16600381111561106257611062611751565b1415801561108757506002600f5460ff16600381111561108457611084611751565b14155b156110ac576002546040516310004b0160e11b8152600481019190915260240161009b565b60006110b8838361102b565b600c549091506000908190815b8181101561115e576000600c82815481106110e2576110e2611a66565b6000918252602090912060029091020180549091506001600160a01b03808a169116036111555760018101546001600160a01b0380881691160361112a575050505050505050565b600190810180546001600160a01b038881166001600160a01b0319831617909255169450925061115e565b506001016110c5565b508161117f5785604051632fd768d360e11b815260040161009b9190611b4b565b856001600160a01b03167f25639ce407c98fba722dddf1f023c8242e3c77732cfe4660d1c04930bf4cbe7284866040516111ba929190611ae3565b60405180910390a2505050505050565b600c54600090815b81811015611231576000600c82815481106111ef576111ef611a66565b600091825260208083206002909202909101546001600160a01b0316808352600a9091526040909120549091506112269085611afd565b9350506001016111d2565b505090565b600e54600090815b81811015611231576000600e828154811061125b5761125b611a66565b60009182526020808320909101546001600160a01b0316808352600d909152604090912060018101549192509060ff1661129e57805461129b9086611afd565b94505b505060010161123e565b6040516001600160a01b0384811660248301528381166044830152606482018390526108539186918216906323b872dd90608401610e97565b6002600f5460ff1660038111156112fa576112fa611751565b1461131f57600f546040516383696f6f60e01b815261009b9160ff1690600401611a7c565b600f805460ff191660031790556002546040517f839cf22e1ba87ce2f5b9bbf46cf0175a09eed52febdfaac8852478e68203c76390600090a2600c546000816001600160401b038111156113755761137561177c565b6040519080825280602002602001820160405280156113c957816020015b60408051608081018252600091810182815260608201839052815260208101919091528152602001906001900390816113935790505b50905060005b82811015611464576000600c82815481106113ec576113ec611a66565b6000918252602080832060408051608081018252600290940290910180546001600160a01b03908116858401818152600184015490921660608701529085528552600a83529320549082015284519192509084908490811061145057611450611a66565b6020908102919091010152506001016113cf565b506080516001600160a01b031663095ea7b360a05160c0516040518363ffffffff1660e01b8152600401611499929190611b10565b6020604051808303816000875af11580156114b8573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906114dc9190611b5f565b5060a0516001600160a01b031663bc7efecc600060066002856040518563ffffffff1660e01b81526004016115149493929190611bd9565b600060405180830381600087803b15801561152e57600080fd5b505af1158015610e69573d6000803e3d6000fd5b60006115576001600160a01b0384168361159c565b9050805160001415801561157c57508080602001905181019061157a9190611b5f565b155b15610ec95782604051635274afe760e01b815260040161009b9190611b4b565b6060610f5e838360006115b0565b92915050565b6060814710156115d5573060405163cd78605960e01b815260040161009b9190611b4b565b600080856001600160a01b031684866040516115f19190611c50565b60006040518083038185875af1925050503d806000811461162e576040519150601f19603f3d011682016040523d82523d6000602084013e611633565b606091505b50909250905061164486838361164e565b9695505050505050565b6060826116635761165e826116a1565b610f5e565b815115801561167a57506001600160a01b0384163b155b1561169a5783604051639996b31560e01b815260040161009b9190611b4b565b5080610f5e565b8051156116b15780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b50565b50805460008255600202906000526020600020908101906116ca919061170c565b50805460008255906000526020600020908101906116ca919061173c565b5b808211156117385780546001600160a01b03199081168255600182018054909116905560020161170d565b5090565b5b80821115611738576000815560010161173d565b634e487b7160e01b600052602160045260246000fd5b6001600160a01b03811681146116ca57600080fd5b634e487b7160e01b600052604160045260246000fd5b604051608081016001600160401b03811182821017156117b4576117b461177c565b60405290565b604080519081016001600160401b03811182821017156117b4576117b461177c565b604051601f8201601f191681016001600160401b03811182821017156118045761180461177c565b604052919050565b60006080828403121561181e57600080fd5b611826611792565b8251815260208084015190820152604080840151908201526060928301519281019290925250919050565b60006080828403121561186357600080fd5b61186b611792565b825181526020808401519082015260408084015190820152606083015190915061ffff8116811461189b57600080fd5b606082015292915050565b600082601f8301126118b757600080fd5b81516001600160401b038111156118d0576118d061177c565b6118df60208260051b016117dc565b8082825260208201915060208360061b86010192508583111561190157600080fd5b602085015b8381101561194d576040818803121561191e57600080fd5b6119266117ba565b815161193181611767565b8152602082810151818301529084529290920191604001611906565b5095945050505050565b8051801515811461196757600080fd5b919050565b60008060008060008060008789036101c081121561198957600080fd5b885161199481611767565b60208a015190985096506040603f19820112156119b057600080fd5b506119b96117ba565b604089015181526060890151602082015294506119d98960808a0161180c565b93506119e9896101008a01611851565b6101808901519093506001600160401b03811115611a0657600080fd5b611a128a828b016118a6565b925050611a226101a08901611957565b905092959891949750929550565b600060208284031215611a4257600080fd5b5051919050565b600060208284031215611a5b57600080fd5b8151610f5e81611767565b634e487b7160e01b600052603260045260246000fd5b6020810160048310611a9e57634e487b7160e01b600052602160045260246000fd5b91905290565b9283526020830191909152604082015260600190565b634e487b7160e01b600052601160045260246000fd5b818103818111156115aa576115aa611aba565b6001600160a01b0392831681529116602082015260400190565b808201808211156115aa576115aa611aba565b6001600160a01b03929092168252602082015260400190565b600082611b4657634e487b7160e01b600052601260045260246000fd5b500490565b6001600160a01b0391909116815260200190565b600060208284031215611b7157600080fd5b610f5e82611957565b600081518084526020840193506020830160005b82811015611bcf578151805180516001600160a01b039081168952602091820151168189015290810151604088015260609096019590910190600101611b8e565b5093949350505050565b8454815260018501546020820152835460408201526001840154606082015260028401546080820152600384015460a0820152825460c0820152600183015460e0820152600283015461010082015261ffff6003840154166101208201526101606101408201526000611644610160830184611b7a565b6000825160005b81811015611c715760208186018101518583015201611c57565b506000920191825250919050565b60805160a05160c05160e051610100516135f2611e4b6000396000818161052c01528181610f36015281816115b4015281816115dd0152818161179701528181611e3001528181611e56015281816123b501526123e90152600081816104660152818161064b01528181610673015281816106d601528181610a2801528181610a5001528181610afe01528181610b2601528181610bcb01528181610bf301528181610c5b01528181610c9301528181610cbb01528181610d0d01528181610d350152818161104b01528181611073015281816111b5015281816112140152818161123c01528181611469015281816114910152818161162e015281816116a00152818161205901528181612093015281816120f50152818161244c015281816129480152612a7d01526000818161043f01528181610ef80152818161159201528181611f9601528181611fbc01528181612342015281816123750152818161241701526127b601526000818161034a01528181611dae01528181611e7701528181611f1401528181611fdd015281816127940152818161283901526129170152600081816104a001528181611ab201528181611cef015281816124f2015261276701526135f26000f3fe608060405234801561001057600080fd5b50600436106101e25760003560e01c8062e9a407146101e75780630aaffd2a146101fc5780630d616d201461020f5780630d9639ba146102175780630dcf4b8f146102375780630ebb172a1461024d57806312fa329b1461026f578063200d2ed2146102905780632816ee73146102aa5780632c6cda93146102bd5780632c7baaf3146102d0578063313f336b1461030f5780633362379414610345578063356c299d1461037957806342e94c90146103b75780634564168b146103d75780634a387cdf146103f35780634bb278f3146103fb5780634e7320ce14610403578063565a9d481461041857806356d399e81461043a578063570ca7351461046157806358b810d31461048857806360e52ecb1461049b5780636786452c146104c25780636cf72aa4146104d55780637c0b4bfb146104e857806386fa5063146105275780638c7e7a311461054e5780638dd0855714610561578063937e09b11461058157806396a608c0146105895780639ca002a614610591578063a4b6aa83146105a4578063bc063e1a146105ac578063ccec3716146105c8578063d26146a5146105db578063d826f88f146105ee578063dda0a81a146105f6578063e020e4c41461060d578063e50d50d814610620575b600080fd5b6101fa6101f5366004612e8f565b610640565b005b6101fa61020a366004612ecb565b6106c2565b6101fa6106cc565b61021f6107b7565b60405161022e93929190612f5d565b60405180910390f35b61023f61096d565b60405190815260200161022e565b6102576201518081565b6040516001600160401b03909116815260200161022e565b61028261027d366004612fca565b6109d9565b60405161022e929190612fe3565b600f5461029d9060ff1681565b60405161022e9190613013565b6101fa6102b836600461303b565b610a12565b6101fa6102cb366004613079565b610a1d565b6102d8610a93565b60405161022e919081518152602080830151908201526040808301519082015260609182015161ffff169181019190915260800190565b6006546007546008546009546103259392919084565b60408051948552602085019390935291830152606082015260800161022e565b61036c7f000000000000000000000000000000000000000000000000000000000000000081565b60405161022e9190613094565b6002546003546004546005546103939392919061ffff1684565b6040805194855260208501939093529183015261ffff16606082015260800161022e565b61023f6103c5366004612ecb565b600a6020526000908152604090205481565b6000546001546103e5919082565b60405161022e9291906130a8565b600c5461023f565b6101fa610af3565b61040b610b68565b60405161022e91906130dc565b600f5461042a90610100900460ff1681565b604051901515815260200161022e565b61023f7f000000000000000000000000000000000000000000000000000000000000000081565b61036c7f000000000000000000000000000000000000000000000000000000000000000081565b6101fa610496366004613103565b610bc0565b61036c7f000000000000000000000000000000000000000000000000000000000000000081565b6101fa6104d03660046131f1565b610c88565b6101fa6104e3366004613242565b610d02565b6105126104f6366004612ecb565b600d602052600090815260409020805460019091015460ff1682565b6040805192835290151560208301520161022e565b61023f7f000000000000000000000000000000000000000000000000000000000000000081565b61023f61055c36600461325f565b610d78565b61057461056f366004612fca565b610e09565b60405161022e91906132a6565b61023f610e62565b61023f610f62565b61023f61059f366004612fca565b610fb9565b61023f610fce565b6105b561271081565b60405161ffff909116815260200161022e565b6101fa6105d6366004612ecb565b611040565b61036c6105e9366004612fca565b6111df565b6101fa611209565b6105fe61127e565b60405161022e939291906132b4565b6101fa61061b3660046132ed565b61145e565b61023f61062e366004612ecb565b600b6020526000908152604090205481565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146106b657337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b60405180910390fd5b6106bf816114e3565b50565b6106bf33826118a8565b6001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016330361070657610704611a29565b565b336000908152600b602052604081205461072090426133e4565b90506201518081101561076a57336000908152600b602052604090819020549051630429b06960e31b815260048101919091524260248201526201518060448201526064016106ad565b600061077533611b4c565b905080156107b35760405181815233907f249a82fbe5056a1940b4e996665ba2a82c340ae9fa1e069fd1ababf5508f396e9060200160405180910390a25b5050565b600e5460609081908190806001600160401b038111156107d9576107d9612d34565b604051908082528060200260200182016040528015610802578160200160208202803683370190505b509350806001600160401b0381111561081d5761081d612d34565b604051908082528060200260200182016040528015610846578160200160208202803683370190505b509250806001600160401b0381111561086157610861612d34565b60405190808252806020026020018201604052801561088a578160200160208202803683370190505b50915060005b81811015610966576000600e82815481106108ad576108ad6133f7565b60009182526020808320909101546001600160a01b0316808352600d909152604090912087519192509082908890859081106108eb576108eb6133f7565b60200260200101906001600160a01b031690816001600160a01b0316815250508060000154868481518110610922576109226133f7565b60209081029190910101526001810154855160ff9091169086908590811061094c5761094c6133f7565b911515602092830291909101909101525050600101610890565b5050909192565b600c54600090815b818110156109d4576000600c8281548110610992576109926133f7565b600091825260208083206002909202909101546001600160a01b0316808352600a9091526040909120549091506109c9908561340d565b935050600101610975565b505090565b600c81815481106109e957600080fd5b6000918252602090912060029091020180546001909101546001600160a01b0391821692501682565b6107b3338284611d49565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610a8a57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6106bf8161255d565b610ac26040518060800160405280600081526020016000815260200160008152602001600061ffff1681525090565b5060408051608081018252600254815260035460208201526004549181019190915260055461ffff16606082015290565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610b6057337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6107046125e7565b610b936040518060800160405280600081526020016000815260200160008152602001600081525090565b50604080516080810182526006548152600754602082015260085491810191909152600954606082015290565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610c2d57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b610c35611a29565b610c3e8561255d565b610c47846114e3565b610c50836128a5565b8015610c8157610c817f00000000000000000000000000000000000000000000000000000000000000008383611d49565b5050505050565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610cf557337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b610c8185858585856128bf565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610d6f57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6106bf816128a5565b6000828211610d9e5782826040516376675ed960e11b81526004016106ad9291906130a8565b82600003610dcf576004610db36001866133e4565b610dbd9190613420565b610dc890600161340d565b9050610e02565b6000610ddb84846133e4565b905080610de96001876133e4565b610df39190613420565b610dfe90600161340d565b9150505b9392505050565b610e11612c79565b600c8281548110610e2457610e246133f7565b60009182526020918290206040805180820190915260029092020180546001600160a01b039081168352600190910154169181019190915292915050565b600e5460009081908190815b81811015610ee5576000600e8281548110610e8b57610e8b6133f7565b60009182526020808320909101546001600160a01b0316808352600d909152604090912060018101549192509060ff16610edb578054610ecb908761340d565b9550610ed860018661340d565b94505b5050600101610e6e565b50610f5a83610ef261096d565b610f1c907f00000000000000000000000000000000000000000000000000000000000000006133e4565b610f2691906133e4565b600c54610f3490859061340d565b7f0000000000000000000000000000000000000000000000000000000000000000610d78565b935050505090565b600c54600090610f725750600090565b600a6000600c600081548110610f8a57610f8a6133f7565b600091825260208083206002909202909101546001600160a01b03168352820192909252604001902054905090565b6000610fc88260006001610d78565b92915050565b600e54600090815b818110156109d4576000600e8281548110610ff357610ff36133f7565b60009182526020808320909101546001600160a01b0316808352600d909152604090912060018101549192509060ff16611036578054611033908661340d565b94505b5050600101610fd6565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146110ad57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6003600f5460ff1660038111156110c6576110c6612ffd565b141580156110ea57506000600f5460ff1660038111156110e8576110e8612ffd565b145b1561110f57600f54604051632f3e69dd60e01b81526106ad9160ff1690600401613013565b6040516370a0823160e01b815281906000906001600160a01b038316906370a0823190611140903090600401613094565b602060405180830381865afa15801561115d573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906111819190613442565b9050600081116111a6578260405163e932c57360e01b81526004016106ad9190613094565b6111da6001600160a01b0383167f0000000000000000000000000000000000000000000000000000000000000000836129da565b505050565b600e81815481106111ef57600080fd5b6000918252602090912001546001600160a01b0316905081565b336001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000161461127657337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b610704611a29565b600c5460609081908190806001600160401b038111156112a0576112a0612d34565b6040519080825280602002602001820160405280156112c9578160200160208202803683370190505b509350806001600160401b038111156112e4576112e4612d34565b60405190808252806020026020018201604052801561130d578160200160208202803683370190505b509250806001600160401b0381111561132857611328612d34565b604051908082528060200260200182016040528015611351578160200160208202803683370190505b50915060005b81811015610966576000600c8281548110611374576113746133f7565b60009182526020909120600290910201805487519192506001600160a01b0316908790849081106113a7576113a76133f7565b6001600160a01b039283166020918202929092010152600182015486519116908690849081106113d9576113d96133f7565b60200260200101906001600160a01b031690816001600160a01b031681525050600a600087848151811061140f5761140f6133f7565b60200260200101516001600160a01b03166001600160a01b031681526020019081526020016000205484838151811061144a5761144a6133f7565b602090810291909101015250600101611357565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146114cb57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6114da87878787878787612a32565b50505050505050565b6000600f5460ff1660038111156114fc576114fc612ffd565b1461152157600f546040516312ba63f960e11b81526106ad9160ff1690600401613013565b600e5460005b8181101561158057600d6000600e8381548110611546576115466133f7565b60009182526020808320909101546001600160a01b0316835282019290925260400181209081556001908101805460ff1916905501611527565b5061158d600e6000612c90565b5080517f0000000000000000000000000000000000000000000000000000000000000000907f000000000000000000000000000000000000000000000000000000000000000010156116175781517f0000000000000000000000000000000000000000000000000000000000000000604051630962235760e31b81526004016106ad9291906130a8565b815160005b818110156118a257806000036116cd577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316848281518110611668576116686133f7565b6020026020010151600001516001600160a01b0316146116cd5760405163ef77b46760e01b8152600481018290526001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001660248201526044016106ad565b60006001600160a01b03168482815181106116ea576116ea6133f7565b6020026020010151600001516001600160a01b0316036117205760405163621caef560e11b8152600481018290526024016106ad565b6000600d6000868481518110611738576117386133f7565b6020026020010151600001516001600160a01b03166001600160a01b031681526020019081526020016000209050806000015460001461178e576040516308308c2360e21b8152600481018390526024016106ad565b60006117bb85847f0000000000000000000000000000000000000000000000000000000000000000610d78565b905060008684815181106117d1576117d16133f7565b6020026020010151602001519050818110156118065783818360405163c4aa8aa360e01b81526004016106ad9392919061345b565b8086101561182d57838187604051636d3ae3a760e01b81526004016106ad9392919061345b565b61183781876133e4565b9550600e87858151811061184d5761184d6133f7565b6020908102919091018101515182546001808201855560009485529290932090920180546001600160a01b0319166001600160a01b0390931692909217909155908355918201805460ff19169055500161161c565b50505050565b6001600f5460ff1660038111156118c1576118c1612ffd565b141580156118e657506002600f5460ff1660038111156118e3576118e3612ffd565b14155b1561190b576002546040516310004b0160e11b815260048101919091526024016106ad565b60006119178383612aa3565b600c549091506000908190815b818110156119bd576000600c8281548110611941576119416133f7565b6000918252602090912060029091020180549091506001600160a01b03808a169116036119b45760018101546001600160a01b03808816911603611989575050505050505050565b600190810180546001600160a01b038881166001600160a01b031983161790925516945092506119bd565b50600101611924565b50816119de5785604051632fd768d360e11b81526004016106ad9190613094565b856001600160a01b03167f25639ce407c98fba722dddf1f023c8242e3c77732cfe4660d1c04930bf4cbe728486604051611a19929190612fe3565b60405180910390a2505050505050565b600c5460005b81811015611aec576000600c8281548110611a4c57611a4c6133f7565b600091825260208083206002909202909101546001600160a01b0316808352600a9091526040909120549091506003600f5460ff166003811115611a9257611a92612ffd565b14158015611aa05750600081115b15611ad957611ad96001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001683836129da565b611ae282612ac1565b5050600101611a2f565b50611af9600c6000612cae565b600f805460ff191690556040805160008082526020820190925281611b40565b6040805180820190915260008082526020820152815260200190600190039081611b195790505b5090506107b3816114e3565b6001600160a01b0381166000908152600a602052604081205490819003611b7257919050565b611b7b82612ac1565b600c5460005b81811015611c9657600c8181548110611b9c57611b9c6133f7565b60009182526020909120600290910201546001600160a01b0390811690851603611c8e57600c611bcd6001846133e4565b81548110611bdd57611bdd6133f7565b9060005260206000209060020201600c8281548110611bfe57611bfe6133f7565b60009182526020909120825460029092020180546001600160a01b039283166001600160a01b0319918216178255600193840154939091018054939092169216919091179055600c805480611c5557611c55613471565b60008281526020902060026000199092019182020180546001600160a01b03199081168255600191909101805490911690559055611c96565b600101611b81565b506001600160a01b0383166000908152600d6020526040902060018101805460ff191690556003600f5460ff166003811115611cd457611cd4612ffd565b03611ce25760009250611d42565b611d166001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001685856129da565b6002600f5460ff166003811115611d2f57611d2f612ffd565b03611d4257600f805460ff191660011790555b5050919050565b6000600f5460ff166003811115611d6257611d62612ffd565b14158015611d8757506001600f5460ff166003811115611d8457611d84612ffd565b14155b15611dac57600f54604051630295f95f60e51b81526106ad9160ff1690600401613013565b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166386fa50636040518163ffffffff1660e01b8152600401602060405180830381865afa158015611e0a573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611e2e9190613442565b7f000000000000000000000000000000000000000000000000000000000000000014611f12577f00000000000000000000000000000000000000000000000000000000000000007f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166386fa50636040518163ffffffff1660e01b8152600401602060405180830381865afa158015611ed3573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611ef79190613442565b604051631248081b60e31b81526004016106ad9291906130a8565b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166356d399e86040518163ffffffff1660e01b8152600401602060405180830381865afa158015611f70573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611f949190613442565b7f000000000000000000000000000000000000000000000000000000000000000014612039577f00000000000000000000000000000000000000000000000000000000000000007f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166356d399e86040518163ffffffff1660e01b8152600401602060405180830381865afa158015611ed3573d6000803e3d6000fd5b6000600f5460ff16600381111561205257612052612ffd565b03612145577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316836001600160a01b0316146120cd57827f00000000000000000000000000000000000000000000000000000000000000006040516316a4ebc160e21b81526004016106ad929190612fe3565b600f805460ff1916600117905560025460055460405161ffff90911681526001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001691907fe52a01bb4ffcc3baf2c5d17111d7d5b676a7485809eb10671092a89d2bb22ff69060200160405180910390a35b6001600160a01b0383166000908152600d602052604090208054158015906121725750600181015460ff16155b156121b257805482101561219d57805460405163454cd8e160e01b81526106ad9184916004016130a8565b6001818101805460ff19169091179055612207565b6001600160a01b0384166000908152600a60205260409020541580156121de57506121db610e62565b82105b1561220757816121ec610e62565b6040516310e45ff160e21b81526004016106ad9291906130a8565b6001600160a01b0384166000908152600a602052604081205490036122d85760006122328585612aa3565b604080518082019091526001600160a01b03808816825291821660208201908152600c8054600181018255600091909152915160029092027fdf6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c7810180549385166001600160a01b031994851617905590517fdf6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c89091018054919093169116179055506122e2565b6122e284846118a8565b6001600160a01b0384166000908152600a60205260408120805484929061230a90849061340d565b90915550506001600160a01b0384166000908152600b6020526040812042905561233261096d565b9050600061233e610fce565b90507f000000000000000000000000000000000000000000000000000000000000000061236b828461340d565b11156123b05781817f000000000000000000000000000000000000000000000000000000000000000060405163513b428b60e11b81526004016106ad9392919061345b565b600c547f00000000000000000000000000000000000000000000000000000000000000001015612415576040516315b5c3d560e21b81527f000000000000000000000000000000000000000000000000000000000000000060048201526024016106ad565b7f000000000000000000000000000000000000000000000000000000000000000082036124a2576002546040516001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001691907f48996614f3380f3dcd5a2d2e6eaddfd66207ff3457fee339e09b4735bc9ec95890600090a3600f805460ff191660021790555b856001600160a01b03167fbdaa686eb6f59012d211a74523da260341c516896e9e5be954163d6ecf26ffa2856040516124dd91815260200190565b60405180910390a261251a6001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016873087612ae8565b6002600f5460ff16600381111561253357612533612ffd565b1480156125485750600f54610100900460ff16155b15612555576125556125e7565b505050505050565b6000600f5460ff16600381111561257657612576612ffd565b1461259b57600f5460405163ad88fc8f60e01b81526106ad9160ff1690600401613013565b61271061ffff821611156125cf57604051624ae8fd60e41b815261ffff8216600482015261271060248201526044016106ad565b6005805461ffff191661ffff92909216919091179055565b6002600f5460ff16600381111561260057612600612ffd565b1461262557600f546040516383696f6f60e01b81526106ad9160ff1690600401613013565b600f805460ff191660031790556002546040517f839cf22e1ba87ce2f5b9bbf46cf0175a09eed52febdfaac8852478e68203c76390600090a2600c546000816001600160401b0381111561267b5761267b612d34565b6040519080825280602002602001820160405280156126b457816020015b6126a1612ccf565b8152602001906001900390816126995790505b50905060005b8281101561274f576000600c82815481106126d7576126d76133f7565b6000918252602080832060408051608081018252600290940290910180546001600160a01b03908116858401818152600184015490921660608701529085528552600a83529320549082015284519192509084908490811061273b5761273b6133f7565b6020908102919091010152506001016126ba565b5060405163095ea7b360e01b81526001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169063095ea7b3906127de907f0000000000000000000000000000000000000000000000000000000000000000907f000000000000000000000000000000000000000000000000000000000000000090600401613487565b6020604051808303816000875af11580156127fd573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061282191906134a0565b50604051632f1fbfb360e21b81526001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169063bc7efecc90612877906000906006906002908790600401613504565b600060405180830381600087803b15801561289157600080fd5b505af1158015612555573d6000803e3d6000fd5b600f80549115156101000261ff0019909216919091179055565b6000600f5460ff1660038111156128d8576128d8612ffd565b146128fd57600f54604051639a0293fd60e01b81526106ad9160ff1690600401613013565b60405160016226579360e01b031981526001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169063ffd9a86d9061297290889088907f000000000000000000000000000000000000000000000000000000000000000090899060040161357b565b600060405180830381600087803b15801561298c57600080fd5b505af11580156129a0573d6000803e3d6000fd5b5050865160005550506020948501516001558351600655938301516007556040830151600855606090920151600955600255600355600455565b6111da83846001600160a01b031663a9059cbb8585604051602401612a00929190613487565b604051602081830303815290604052915060e01b6020820180516001600160e01b038381831617835250505050612b21565b612a3a611a29565b612a5387878760000151886020015189604001516128bf565b612a60856060015161255d565b612a69846114e3565b612a72836128a5565b80156114da576114da7f00000000000000000000000000000000000000000000000000000000000000008383611d49565b60006001600160a01b03821615612aba5781610e02565b5090919050565b6001600160a01b03166000908152600a60209081526040808320839055600b909152812055565b6040516001600160a01b0384811660248301528381166044830152606482018390526118a29186918216906323b872dd90608401612a00565b6000612b366001600160a01b03841683612b7b565b90508051600014158015612b5b575080806020019051810190612b5991906134a0565b155b156111da5782604051635274afe760e01b81526004016106ad9190613094565b6060610e028383600084600080856001600160a01b03168486604051612ba191906135b6565b60006040518083038185875af1925050503d8060008114612bde576040519150601f19603f3d011682016040523d82523d6000602084013e612be3565b606091505b5091509150612bf3868383612bfd565b9695505050505050565b606082612c1257612c0d82612c50565b610e02565b8151158015612c2957506001600160a01b0384163b155b15612c495783604051639996b31560e01b81526004016106ad9190613094565b5080610e02565b805115612c605780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b604080518082019091526000808252602082015290565b50805460008255906000526020600020908101906106bf9190612cef565b50805460008255600202906000526020600020908101906106bf9190612d08565b6040518060400160405280612ce2612c79565b8152602001600081525090565b5b80821115612d045760008155600101612cf0565b5090565b5b80821115612d045780546001600160a01b031990811682556001820180549091169055600201612d09565b634e487b7160e01b600052604160045260246000fd5b604080519081016001600160401b0381118282101715612d6c57612d6c612d34565b60405290565b604051608081016001600160401b0381118282101715612d6c57612d6c612d34565b604051601f8201601f191681016001600160401b0381118282101715612dbc57612dbc612d34565b604052919050565b80356001600160a01b0381168114612ddb57600080fd5b919050565b600082601f830112612df157600080fd5b81356001600160401b03811115612e0a57612e0a612d34565b612e1960208260051b01612d94565b8082825260208201915060208360061b860101925085831115612e3b57600080fd5b602085015b83811015612e855760408188031215612e5857600080fd5b612e60612d4a565b612e6982612dc4565b8152602082810135818301529084529290920191604001612e40565b5095945050505050565b600060208284031215612ea157600080fd5b81356001600160401b03811115612eb757600080fd5b612ec384828501612de0565b949350505050565b600060208284031215612edd57600080fd5b610e0282612dc4565b600081518084526020840193506020830160005b82811015612f215781516001600160a01b0316865260209586019590910190600101612efa565b5093949350505050565b600081518084526020840193506020830160005b82811015612f21578151865260209586019590910190600101612f3f565b606081526000612f706060830186612ee6565b8281036020840152612f828186612f2b565b83810360408501528451808252602080870193509091019060005b81811015612fbd5783511515835260209384019390920191600101612f9d565b5090979650505050505050565b600060208284031215612fdc57600080fd5b5035919050565b6001600160a01b0392831681529116602082015260400190565b634e487b7160e01b600052602160045260246000fd5b602081016004831061303557634e487b7160e01b600052602160045260246000fd5b91905290565b6000806040838503121561304e57600080fd5b8235915061305e60208401612dc4565b90509250929050565b803561ffff81168114612ddb57600080fd5b60006020828403121561308b57600080fd5b610e0282613067565b6001600160a01b0391909116815260200190565b918252602082015260400190565b805182526020810151602083015260408101516040830152606081015160608301525050565b60808101610fc882846130b6565b80151581146106bf57600080fd5b8035612ddb816130ea565b600080600080600060a0868803121561311b57600080fd5b61312486613067565b945060208601356001600160401b0381111561313f57600080fd5b61314b88828901612de0565b945050604086013561315c816130ea565b925061316a60608701612dc4565b949793965091946080013592915050565b60006040828403121561318d57600080fd5b613195612d4a565b823581526020928301359281019290925250919050565b6000608082840312156131be57600080fd5b6131c6612d72565b8235815260208084013590820152604080840135908201526060928301359281019290925250919050565b6000806000806000610120868803121561320a57600080fd5b613214878761317b565b945061322387604088016131ac565b949794965050505060c08301359260e081013592610100909101359150565b60006020828403121561325457600080fd5b8135610e02816130ea565b60008060006060848603121561327457600080fd5b505081359360208301359350604090920135919050565b80516001600160a01b03908116835260209182015116910152565b60408101610fc8828461328b565b6060815260006132c76060830186612ee6565b82810360208401526132d98186612ee6565b90508281036040840152612bf38185612f2b565b60008060008060008060008789036101c081121561330a57600080fd5b6133148a8a61317b565b97506133238a60408b016131ac565b9650608060bf198201121561333757600080fd5b50613340612d72565b60c0890135815260e0890135602082015261010089013560408201526133696101208a01613067565b606082015294506101408801356001600160401b0381111561338a57600080fd5b6133968a828b01612de0565b9450506133a661016089016130f8565b92506133b56101808901612dc4565b9699959850939692959194919350506101a09091013590565b634e487b7160e01b600052601160045260246000fd5b81810381811115610fc857610fc86133ce565b634e487b7160e01b600052603260045260246000fd5b80820180821115610fc857610fc86133ce565b60008261343d57634e487b7160e01b600052601260045260246000fd5b500490565b60006020828403121561345457600080fd5b5051919050565b9283526020830191909152604082015260600190565b634e487b7160e01b600052603160045260246000fd5b6001600160a01b03929092168252602082015260400190565b6000602082840312156134b257600080fd5b8151610e02816130ea565b600081518084526020840193506020830160005b82811015612f215781516134e687825161328b565b602090810151604088015260609096019591909101906001016134d1565b8454815260018501546020820152835460408201526001840154606082015260028401546080820152600384015460a0820152825460c0820152600183015460e0820152600283015461010082015261ffff6003840154166101208201526101606101408201526000612bf36101608301846134bd565b8451815260208086015190820152610100810161359b60408301866130b6565b6001600160a01b039390931660c082015260e0015292915050565b6000825160005b818110156135d757602081860181015185830152016135bd565b50600092019182525091905056fea164736f6c634300081a000aa164736f6c634300081a000a", - "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100af5760003560e01c806333623794146100b45780633f4ba83a146100dd5780635c975abb146100e7578063715018a6146100ff57806379ba5097146101075780638456cb591461010f5780638da5cb5b146101175780638faff8621461011f578063c4d66de814610132578063c70242ad14610145578063e30c397814610168578063f2fde38b14610170578063f7bc39bf14610183575b600080fd5b6000546100c7906001600160a01b031681565b6040516100d49190610809565b60405180910390f35b6100e56101af565b005b6100ef6101c1565b60405190151581526020016100d4565b6100e56101d6565b6100e56101e8565b6100e5610230565b6100c7610240565b6100c761012d36600461084a565b61025b565b6100e5610140366004610925565b610387565b6100ef610153366004610925565b60016020526000908152604090205460ff1681565b6100c76104a3565b6100e561017e366004610925565b6104ae565b6100ef610191366004610925565b6001600160a01b031660009081526001602052604090205460ff1690565b6101b761051f565b6101bf610551565b565b6000806101cc6105a8565b5460ff1692915050565b6101de61051f565b6101bf60006105cc565b33806101f26104a3565b6001600160a01b031614610224578060405163118cdaa760e01b815260040161021b9190610809565b60405180910390fd5b61022d816105cc565b50565b61023861051f565b6101bf6105f3565b60008061024b61063a565b546001600160a01b031692915050565b600061026561065e565b60008054604080516386fa506360e01b815290516001600160a01b039092169182916386fa50639160048083019260209291908290030181865afa1580156102b1573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906102d59190610947565b8989898989896040516102e7906107fc565b6102f89897969594939291906109eb565b604051809103906000f080158015610314573d6000803e3d6000fd5b506001600160a01b038116600081815260016020819052604091829020805460ff1916909117905551919350839250907feac84630ba02e5ab324a651281c90ec45563a21f07fdf52b6f601f312e2de27a90610374908935815260200190565b60405180910390a2509695505050505050565b6000610391610684565b805490915060ff600160401b82041615906001600160401b03166000811580156103b85750825b90506000826001600160401b031660011480156103d45750303b155b9050811580156103e2575080155b156104005760405163f92ee8a960e01b815260040160405180910390fd5b84546001600160401b0319166001178555831561042957845460ff60401b1916600160401b1785555b600080546001600160a01b0319166001600160a01b03881617905561044d336106a8565b6104556106b9565b831561049b57845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b505050505050565b60008061024b6106c9565b6104b661051f565b60006104c06106c9565b80546001600160a01b0319166001600160a01b03841690811782559091506104e6610240565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b33610528610240565b6001600160a01b0316146101bf573360405163118cdaa760e01b815260040161021b9190610809565b6105596106ed565b60006105636105a8565b805460ff1916815590507f5db9ee0a495bf2e6ff9c91a7834c1ba4fdd244a5e8aa4e537bd38aeae4b073aa335b60405161059d9190610809565b60405180910390a150565b7fcd5ed15c6e187e77e9aee88184c21f4f2182ab5827cb3b7e07fbedcd63f0330090565b60006105d66106c9565b80546001600160a01b031916815590506105ef82610712565b5050565b6105fb61065e565b60006106056105a8565b805460ff1916600117815590507f62e78cea01bee320cd4e420270b5ea74000d11b0c9f74754ebdbfc544b05a2586105903390565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930090565b6106666101c1565b156101bf5760405163d93c066560e01b815260040160405180910390fd5b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0090565b6106b061076e565b61022d81610793565b6106c161076e565b6101bf6107c5565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0090565b6106f56101c1565b6101bf57604051638dfc202b60e01b815260040160405180910390fd5b600061071c61063a565b80546001600160a01b038481166001600160a01b031983168117845560405193945091169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a3505050565b6107766107e2565b6101bf57604051631afcd79f60e31b815260040160405180910390fd5b61079b61076e565b6001600160a01b038116610224576000604051631e4fbdf760e01b815260040161021b9190610809565b6107cd61076e565b60006107d76105a8565b805460ff1916905550565b60006107ec610684565b54600160401b900460ff16919050565b61543d80610a7883390190565b6001600160a01b0391909116815260200190565b60006080828403121561082f57600080fd5b50919050565b8035801515811461084557600080fd5b919050565b60008060008060008086880361018081121561086557600080fd5b604081121561087357600080fd5b50869550610884886040890161081d565b94506108938860c0890161081d565b93506101408701356001600160401b038111156108af57600080fd5b8701601f810189136108c057600080fd5b80356001600160401b038111156108d657600080fd5b8960208260061b84010111156108eb57600080fd5b602091909101935091506109026101608801610835565b90509295509295509295565b80356001600160a01b038116811461084557600080fd5b60006020828403121561093757600080fd5b6109408261090e565b9392505050565b60006020828403121561095957600080fd5b5051919050565b803582526020808201359083015260408082013590830152606081013561ffff811680821461098e57600080fd5b80606085015250505050565b81835260208301925060008160005b848110156109e1576001600160a01b036109c28361090e565b16865260208281013590870152604095860195909101906001016109a9565b5093949350505050565b6001600160a01b03891681526020808201899052873560408084019190915288820135606080850191909152883560808501529188013560a084015287013560c083015286013560e08201526000610a47610100830187610960565b6101c0610180830152610a5f6101c08301858761099a565b8315156101a08401529050999850505050505050505056fe610120604052600f805460ff1916905534801561001b57600080fd5b5060405161543d38038061543d83398101604081905261003a9161196c565b866001600160a01b0381166100a45760405162461bcd60e51b815260206004820152602560248201527f5368617265643a205a65726f2d61646472657373206973206e6f74207065726d6044820152641a5d1d195960da1b60648201526084015b60405180910390fd5b86806000036100f55760405162461bcd60e51b815260206004820152601b60248201527f5368617265643a2075696e7420696e70757420697320656d7074790000000000604482015260640161009b565b6001600160a01b03891660a081905260408051630ada733d60e31b815290516356d399e8916004808201926020929091908290030181865afa15801561013f573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101639190611a30565b60c0818152505060a0516001600160a01b0316637c89d2f06040518163ffffffff1660e01b8152600401602060405180830381865afa1580156101aa573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101ce9190611a49565b6001600160a01b03166080526101008890523260e05260006101f588888888888680610204565b50505050505050505050611c7f565b61020c61026c565b845160208601516040870151610225928a928a92610398565b6060850151610233906104ad565b61023c84610537565b600f805461ff0019166101008515150217905580156102635760e051610263908383610859565b50505050505050565b600c5460005b81811015610334576000600c828154811061028f5761028f611a66565b600091825260208083206002909202909101546001600160a01b0316808352600a9091526040909120549091506003600f5460ff1660038111156102d5576102d5611751565b141580156102e35750600081115b156102ff576080516102ff906001600160a01b03168383610e71565b61032a826001600160a01b03166000908152600a60209081526040808320839055600b909152812055565b5050600101610272565b50610341600c60006116cd565b600f805460ff191690556040805160008082526020820190925281610388565b60408051808201909152600080825260208201528152602001906001900390816103615790505b50905061039481610537565b5050565b6000600f5460ff1660038111156103b1576103b1611751565b146103d657600f54604051639a0293fd60e01b815261009b9160ff1690600401611a7c565b60a05160e0516040805160016226579360e01b03198152885160048201526020808a01516024830152885160448301528801516064820152908701516084820152606087015160a48201526001600160a01b0391821660c482015260e4810186905291169063ffd9a86d9061010401600060405180830381600087803b15801561045f57600080fd5b505af1158015610473573d6000803e3d6000fd5b5050865160005550506020948501516001558351600655938301516007556040830151600855606090920151600955600255600355600455565b6000600f5460ff1660038111156104c6576104c6611751565b146104eb57600f5460405163ad88fc8f60e01b815261009b9160ff1690600401611a7c565b61271061ffff8216111561051f57604051624ae8fd60e41b815261ffff82166004820152612710602482015260440161009b565b6005805461ffff191661ffff92909216919091179055565b6000600f5460ff16600381111561055057610550611751565b1461057557600f546040516312ba63f960e11b815261009b9160ff1690600401611a7c565b600e5460005b818110156105d457600d6000600e838154811061059a5761059a611a66565b60009182526020808320909101546001600160a01b0316835282019290925260400181209081556001908101805460ff191690550161057b565b506105e1600e60006116ee565b5060c051610100518251111561061957815161010051604051630962235760e31b81526004810192909252602482015260440161009b565b815160005b8181101561085357806000036106955760e0516001600160a01b031684828151811061064c5761064c611a66565b6020026020010151600001516001600160a01b0316146106955760e05160405163ef77b46760e01b8152600481018390526001600160a01b03909116602482015260440161009b565b60006001600160a01b03168482815181106106b2576106b2611a66565b6020026020010151600001516001600160a01b0316036106e85760405163621caef560e11b81526004810182905260240161009b565b6000600d600086848151811061070057610700611a66565b6020026020010151600001516001600160a01b03166001600160a01b0316815260200190815260200160002090508060000154600014610756576040516308308c2360e21b81526004810183905260240161009b565b600061076c858461010051610ece60201b60201c565b9050600086848151811061078257610782611a66565b6020026020010151602001519050818110156107b75783818360405163c4aa8aa360e01b815260040161009b93929190611aa4565b808610156107de57838187604051636d3ae3a760e01b815260040161009b93929190611aa4565b6107e88187611ad0565b9550600e8785815181106107fe576107fe611a66565b6020908102919091018101515182546001808201855560009485529290932090920180546001600160a01b0319166001600160a01b0390931692909217909155908355918201805460ff19169055500161061e565b50505050565b6000600f5460ff16600381111561087257610872611751565b1415801561089757506001600f5460ff16600381111561089457610894611751565b14155b156108bc57600f54604051630295f95f60e51b815261009b9160ff1690600401611a7c565b60a0516001600160a01b03166386fa50636040518163ffffffff1660e01b8152600401602060405180830381865afa1580156108fc573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109209190611a30565b61010051146109b3576101005160a0516001600160a01b03166386fa50636040518163ffffffff1660e01b8152600401602060405180830381865afa15801561096d573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109919190611a30565b604051631248081b60e31b81526004810192909252602482015260440161009b565b60a0516001600160a01b03166356d399e86040518163ffffffff1660e01b8152600401602060405180830381865afa1580156109f3573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610a179190611a30565b60c05114610a625760c05160a0516001600160a01b03166356d399e86040518163ffffffff1660e01b8152600401602060405180830381865afa15801561096d573d6000803e3d6000fd5b6000600f5460ff166003811115610a7b57610a7b611751565b03610b145760e0516001600160a01b0316836001600160a01b031614610ab95760e0516040516316a4ebc160e21b815261009b918591600401611ae3565b600f805460ff1916600117905560e05160025460055460405161ffff90911681526001600160a01b03909216917fe52a01bb4ffcc3baf2c5d17111d7d5b676a7485809eb10671092a89d2bb22ff69060200160405180910390a35b6001600160a01b0383166000908152600d60205260409020805415801590610b415750600181015460ff16155b15610b8a578054821015610b7557805460405163454cd8e160e01b815261009b918491600401918252602082015260400190565b6001818101805460ff19169091179055610be6565b6001600160a01b0384166000908152600a6020526040902054158015610bb65750610bb3610f65565b82105b15610be65781610bc4610f65565b6040516310e45ff160e21b81526004810192909252602482015260440161009b565b6001600160a01b0384166000908152600a60205260408120549003610cb7576000610c11858561102b565b604080518082019091526001600160a01b03808816825291821660208201908152600c8054600181018255600091909152915160029092027fdf6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c7810180549385166001600160a01b031994851617905590517fdf6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c8909101805491909316911617905550610cc1565b610cc18484611049565b6001600160a01b0384166000908152600a602052604081208054849290610ce9908490611afd565b90915550506001600160a01b0384166000908152600b60205260408120429055610d116111ca565b90506000610d1d611236565b60c051909150610d2d8284611afd565b1115610d5457818160c05160405163513b428b60e11b815260040161009b93929190611aa4565b61010051600c541115610d8157610100516040516315b5c3d560e21b815260040161009b91815260200190565b60c0518203610dd35760e0516002546040516001600160a01b03909216917f48996614f3380f3dcd5a2d2e6eaddfd66207ff3457fee339e09b4735bc9ec95890600090a3600f805460ff191660021790555b856001600160a01b03167fbdaa686eb6f59012d211a74523da260341c516896e9e5be954163d6ecf26ffa285604051610e0e91815260200190565b60405180910390a2608051610e2e906001600160a01b03168730876112a8565b6002600f5460ff166003811115610e4757610e47611751565b148015610e5c5750600f54610100900460ff16155b15610e6957610e696112e1565b505050505050565b610ec983846001600160a01b031663a9059cbb8585604051602401610e97929190611b10565b60408051808303601f1901815291905260208101805160e09390931b6001600160e01b03938416179052915061154216565b505050565b6000828211610efa576040516376675ed960e11b8152600481018490526024810183905260440161009b565b82600003610f2b576004610f0f600186611ad0565b610f199190611b29565b610f24906001611afd565b9050610f5e565b6000610f378484611ad0565b905080610f45600187611ad0565b610f4f9190611b29565b610f5a906001611afd565b9150505b9392505050565b600e5460009081908190815b81811015610fe8576000600e8281548110610f8e57610f8e611a66565b60009182526020808320909101546001600160a01b0316808352600d909152604090912060018101549192509060ff16610fde578054610fce9087611afd565b9550610fdb600186611afd565b94505b5050600101610f71565b5061102383610ff56111ca565b60c0516110029190611ad0565b61100c9190611ad0565b600c5461101a908590611afd565b61010051610ece565b935050505090565b60006001600160a01b038216156110425781610f5e565b5090919050565b6001600f5460ff16600381111561106257611062611751565b1415801561108757506002600f5460ff16600381111561108457611084611751565b14155b156110ac576002546040516310004b0160e11b8152600481019190915260240161009b565b60006110b8838361102b565b600c549091506000908190815b8181101561115e576000600c82815481106110e2576110e2611a66565b6000918252602090912060029091020180549091506001600160a01b03808a169116036111555760018101546001600160a01b0380881691160361112a575050505050505050565b600190810180546001600160a01b038881166001600160a01b0319831617909255169450925061115e565b506001016110c5565b508161117f5785604051632fd768d360e11b815260040161009b9190611b4b565b856001600160a01b03167f25639ce407c98fba722dddf1f023c8242e3c77732cfe4660d1c04930bf4cbe7284866040516111ba929190611ae3565b60405180910390a2505050505050565b600c54600090815b81811015611231576000600c82815481106111ef576111ef611a66565b600091825260208083206002909202909101546001600160a01b0316808352600a9091526040909120549091506112269085611afd565b9350506001016111d2565b505090565b600e54600090815b81811015611231576000600e828154811061125b5761125b611a66565b60009182526020808320909101546001600160a01b0316808352600d909152604090912060018101549192509060ff1661129e57805461129b9086611afd565b94505b505060010161123e565b6040516001600160a01b0384811660248301528381166044830152606482018390526108539186918216906323b872dd90608401610e97565b6002600f5460ff1660038111156112fa576112fa611751565b1461131f57600f546040516383696f6f60e01b815261009b9160ff1690600401611a7c565b600f805460ff191660031790556002546040517f839cf22e1ba87ce2f5b9bbf46cf0175a09eed52febdfaac8852478e68203c76390600090a2600c546000816001600160401b038111156113755761137561177c565b6040519080825280602002602001820160405280156113c957816020015b60408051608081018252600091810182815260608201839052815260208101919091528152602001906001900390816113935790505b50905060005b82811015611464576000600c82815481106113ec576113ec611a66565b6000918252602080832060408051608081018252600290940290910180546001600160a01b03908116858401818152600184015490921660608701529085528552600a83529320549082015284519192509084908490811061145057611450611a66565b6020908102919091010152506001016113cf565b506080516001600160a01b031663095ea7b360a05160c0516040518363ffffffff1660e01b8152600401611499929190611b10565b6020604051808303816000875af11580156114b8573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906114dc9190611b5f565b5060a0516001600160a01b031663bc7efecc600060066002856040518563ffffffff1660e01b81526004016115149493929190611bd9565b600060405180830381600087803b15801561152e57600080fd5b505af1158015610e69573d6000803e3d6000fd5b60006115576001600160a01b0384168361159c565b9050805160001415801561157c57508080602001905181019061157a9190611b5f565b155b15610ec95782604051635274afe760e01b815260040161009b9190611b4b565b6060610f5e838360006115b0565b92915050565b6060814710156115d5573060405163cd78605960e01b815260040161009b9190611b4b565b600080856001600160a01b031684866040516115f19190611c50565b60006040518083038185875af1925050503d806000811461162e576040519150601f19603f3d011682016040523d82523d6000602084013e611633565b606091505b50909250905061164486838361164e565b9695505050505050565b6060826116635761165e826116a1565b610f5e565b815115801561167a57506001600160a01b0384163b155b1561169a5783604051639996b31560e01b815260040161009b9190611b4b565b5080610f5e565b8051156116b15780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b50565b50805460008255600202906000526020600020908101906116ca919061170c565b50805460008255906000526020600020908101906116ca919061173c565b5b808211156117385780546001600160a01b03199081168255600182018054909116905560020161170d565b5090565b5b80821115611738576000815560010161173d565b634e487b7160e01b600052602160045260246000fd5b6001600160a01b03811681146116ca57600080fd5b634e487b7160e01b600052604160045260246000fd5b604051608081016001600160401b03811182821017156117b4576117b461177c565b60405290565b604080519081016001600160401b03811182821017156117b4576117b461177c565b604051601f8201601f191681016001600160401b03811182821017156118045761180461177c565b604052919050565b60006080828403121561181e57600080fd5b611826611792565b8251815260208084015190820152604080840151908201526060928301519281019290925250919050565b60006080828403121561186357600080fd5b61186b611792565b825181526020808401519082015260408084015190820152606083015190915061ffff8116811461189b57600080fd5b606082015292915050565b600082601f8301126118b757600080fd5b81516001600160401b038111156118d0576118d061177c565b6118df60208260051b016117dc565b8082825260208201915060208360061b86010192508583111561190157600080fd5b602085015b8381101561194d576040818803121561191e57600080fd5b6119266117ba565b815161193181611767565b8152602082810151818301529084529290920191604001611906565b5095945050505050565b8051801515811461196757600080fd5b919050565b60008060008060008060008789036101c081121561198957600080fd5b885161199481611767565b60208a015190985096506040603f19820112156119b057600080fd5b506119b96117ba565b604089015181526060890151602082015294506119d98960808a0161180c565b93506119e9896101008a01611851565b6101808901519093506001600160401b03811115611a0657600080fd5b611a128a828b016118a6565b925050611a226101a08901611957565b905092959891949750929550565b600060208284031215611a4257600080fd5b5051919050565b600060208284031215611a5b57600080fd5b8151610f5e81611767565b634e487b7160e01b600052603260045260246000fd5b6020810160048310611a9e57634e487b7160e01b600052602160045260246000fd5b91905290565b9283526020830191909152604082015260600190565b634e487b7160e01b600052601160045260246000fd5b818103818111156115aa576115aa611aba565b6001600160a01b0392831681529116602082015260400190565b808201808211156115aa576115aa611aba565b6001600160a01b03929092168252602082015260400190565b600082611b4657634e487b7160e01b600052601260045260246000fd5b500490565b6001600160a01b0391909116815260200190565b600060208284031215611b7157600080fd5b610f5e82611957565b600081518084526020840193506020830160005b82811015611bcf578151805180516001600160a01b039081168952602091820151168189015290810151604088015260609096019590910190600101611b8e565b5093949350505050565b8454815260018501546020820152835460408201526001840154606082015260028401546080820152600384015460a0820152825460c0820152600183015460e0820152600283015461010082015261ffff6003840154166101208201526101606101408201526000611644610160830184611b7a565b6000825160005b81811015611c715760208186018101518583015201611c57565b506000920191825250919050565b60805160a05160c05160e051610100516135f2611e4b6000396000818161052c01528181610f36015281816115b4015281816115dd0152818161179701528181611e3001528181611e56015281816123b501526123e90152600081816104660152818161064b01528181610673015281816106d601528181610a2801528181610a5001528181610afe01528181610b2601528181610bcb01528181610bf301528181610c5b01528181610c9301528181610cbb01528181610d0d01528181610d350152818161104b01528181611073015281816111b5015281816112140152818161123c01528181611469015281816114910152818161162e015281816116a00152818161205901528181612093015281816120f50152818161244c015281816129480152612a7d01526000818161043f01528181610ef80152818161159201528181611f9601528181611fbc01528181612342015281816123750152818161241701526127b601526000818161034a01528181611dae01528181611e7701528181611f1401528181611fdd015281816127940152818161283901526129170152600081816104a001528181611ab201528181611cef015281816124f2015261276701526135f26000f3fe608060405234801561001057600080fd5b50600436106101e25760003560e01c8062e9a407146101e75780630aaffd2a146101fc5780630d616d201461020f5780630d9639ba146102175780630dcf4b8f146102375780630ebb172a1461024d57806312fa329b1461026f578063200d2ed2146102905780632816ee73146102aa5780632c6cda93146102bd5780632c7baaf3146102d0578063313f336b1461030f5780633362379414610345578063356c299d1461037957806342e94c90146103b75780634564168b146103d75780634a387cdf146103f35780634bb278f3146103fb5780634e7320ce14610403578063565a9d481461041857806356d399e81461043a578063570ca7351461046157806358b810d31461048857806360e52ecb1461049b5780636786452c146104c25780636cf72aa4146104d55780637c0b4bfb146104e857806386fa5063146105275780638c7e7a311461054e5780638dd0855714610561578063937e09b11461058157806396a608c0146105895780639ca002a614610591578063a4b6aa83146105a4578063bc063e1a146105ac578063ccec3716146105c8578063d26146a5146105db578063d826f88f146105ee578063dda0a81a146105f6578063e020e4c41461060d578063e50d50d814610620575b600080fd5b6101fa6101f5366004612e8f565b610640565b005b6101fa61020a366004612ecb565b6106c2565b6101fa6106cc565b61021f6107b7565b60405161022e93929190612f5d565b60405180910390f35b61023f61096d565b60405190815260200161022e565b6102576201518081565b6040516001600160401b03909116815260200161022e565b61028261027d366004612fca565b6109d9565b60405161022e929190612fe3565b600f5461029d9060ff1681565b60405161022e9190613013565b6101fa6102b836600461303b565b610a12565b6101fa6102cb366004613079565b610a1d565b6102d8610a93565b60405161022e919081518152602080830151908201526040808301519082015260609182015161ffff169181019190915260800190565b6006546007546008546009546103259392919084565b60408051948552602085019390935291830152606082015260800161022e565b61036c7f000000000000000000000000000000000000000000000000000000000000000081565b60405161022e9190613094565b6002546003546004546005546103939392919061ffff1684565b6040805194855260208501939093529183015261ffff16606082015260800161022e565b61023f6103c5366004612ecb565b600a6020526000908152604090205481565b6000546001546103e5919082565b60405161022e9291906130a8565b600c5461023f565b6101fa610af3565b61040b610b68565b60405161022e91906130dc565b600f5461042a90610100900460ff1681565b604051901515815260200161022e565b61023f7f000000000000000000000000000000000000000000000000000000000000000081565b61036c7f000000000000000000000000000000000000000000000000000000000000000081565b6101fa610496366004613103565b610bc0565b61036c7f000000000000000000000000000000000000000000000000000000000000000081565b6101fa6104d03660046131f1565b610c88565b6101fa6104e3366004613242565b610d02565b6105126104f6366004612ecb565b600d602052600090815260409020805460019091015460ff1682565b6040805192835290151560208301520161022e565b61023f7f000000000000000000000000000000000000000000000000000000000000000081565b61023f61055c36600461325f565b610d78565b61057461056f366004612fca565b610e09565b60405161022e91906132a6565b61023f610e62565b61023f610f62565b61023f61059f366004612fca565b610fb9565b61023f610fce565b6105b561271081565b60405161ffff909116815260200161022e565b6101fa6105d6366004612ecb565b611040565b61036c6105e9366004612fca565b6111df565b6101fa611209565b6105fe61127e565b60405161022e939291906132b4565b6101fa61061b3660046132ed565b61145e565b61023f61062e366004612ecb565b600b6020526000908152604090205481565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146106b657337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b60405180910390fd5b6106bf816114e3565b50565b6106bf33826118a8565b6001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016330361070657610704611a29565b565b336000908152600b602052604081205461072090426133e4565b90506201518081101561076a57336000908152600b602052604090819020549051630429b06960e31b815260048101919091524260248201526201518060448201526064016106ad565b600061077533611b4c565b905080156107b35760405181815233907f249a82fbe5056a1940b4e996665ba2a82c340ae9fa1e069fd1ababf5508f396e9060200160405180910390a25b5050565b600e5460609081908190806001600160401b038111156107d9576107d9612d34565b604051908082528060200260200182016040528015610802578160200160208202803683370190505b509350806001600160401b0381111561081d5761081d612d34565b604051908082528060200260200182016040528015610846578160200160208202803683370190505b509250806001600160401b0381111561086157610861612d34565b60405190808252806020026020018201604052801561088a578160200160208202803683370190505b50915060005b81811015610966576000600e82815481106108ad576108ad6133f7565b60009182526020808320909101546001600160a01b0316808352600d909152604090912087519192509082908890859081106108eb576108eb6133f7565b60200260200101906001600160a01b031690816001600160a01b0316815250508060000154868481518110610922576109226133f7565b60209081029190910101526001810154855160ff9091169086908590811061094c5761094c6133f7565b911515602092830291909101909101525050600101610890565b5050909192565b600c54600090815b818110156109d4576000600c8281548110610992576109926133f7565b600091825260208083206002909202909101546001600160a01b0316808352600a9091526040909120549091506109c9908561340d565b935050600101610975565b505090565b600c81815481106109e957600080fd5b6000918252602090912060029091020180546001909101546001600160a01b0391821692501682565b6107b3338284611d49565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610a8a57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6106bf8161255d565b610ac26040518060800160405280600081526020016000815260200160008152602001600061ffff1681525090565b5060408051608081018252600254815260035460208201526004549181019190915260055461ffff16606082015290565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610b6057337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6107046125e7565b610b936040518060800160405280600081526020016000815260200160008152602001600081525090565b50604080516080810182526006548152600754602082015260085491810191909152600954606082015290565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610c2d57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b610c35611a29565b610c3e8561255d565b610c47846114e3565b610c50836128a5565b8015610c8157610c817f00000000000000000000000000000000000000000000000000000000000000008383611d49565b5050505050565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610cf557337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b610c8185858585856128bf565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610d6f57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6106bf816128a5565b6000828211610d9e5782826040516376675ed960e11b81526004016106ad9291906130a8565b82600003610dcf576004610db36001866133e4565b610dbd9190613420565b610dc890600161340d565b9050610e02565b6000610ddb84846133e4565b905080610de96001876133e4565b610df39190613420565b610dfe90600161340d565b9150505b9392505050565b610e11612c79565b600c8281548110610e2457610e246133f7565b60009182526020918290206040805180820190915260029092020180546001600160a01b039081168352600190910154169181019190915292915050565b600e5460009081908190815b81811015610ee5576000600e8281548110610e8b57610e8b6133f7565b60009182526020808320909101546001600160a01b0316808352600d909152604090912060018101549192509060ff16610edb578054610ecb908761340d565b9550610ed860018661340d565b94505b5050600101610e6e565b50610f5a83610ef261096d565b610f1c907f00000000000000000000000000000000000000000000000000000000000000006133e4565b610f2691906133e4565b600c54610f3490859061340d565b7f0000000000000000000000000000000000000000000000000000000000000000610d78565b935050505090565b600c54600090610f725750600090565b600a6000600c600081548110610f8a57610f8a6133f7565b600091825260208083206002909202909101546001600160a01b03168352820192909252604001902054905090565b6000610fc88260006001610d78565b92915050565b600e54600090815b818110156109d4576000600e8281548110610ff357610ff36133f7565b60009182526020808320909101546001600160a01b0316808352600d909152604090912060018101549192509060ff16611036578054611033908661340d565b94505b5050600101610fd6565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146110ad57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6003600f5460ff1660038111156110c6576110c6612ffd565b141580156110ea57506000600f5460ff1660038111156110e8576110e8612ffd565b145b1561110f57600f54604051632f3e69dd60e01b81526106ad9160ff1690600401613013565b6040516370a0823160e01b815281906000906001600160a01b038316906370a0823190611140903090600401613094565b602060405180830381865afa15801561115d573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906111819190613442565b9050600081116111a6578260405163e932c57360e01b81526004016106ad9190613094565b6111da6001600160a01b0383167f0000000000000000000000000000000000000000000000000000000000000000836129da565b505050565b600e81815481106111ef57600080fd5b6000918252602090912001546001600160a01b0316905081565b336001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000161461127657337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b610704611a29565b600c5460609081908190806001600160401b038111156112a0576112a0612d34565b6040519080825280602002602001820160405280156112c9578160200160208202803683370190505b509350806001600160401b038111156112e4576112e4612d34565b60405190808252806020026020018201604052801561130d578160200160208202803683370190505b509250806001600160401b0381111561132857611328612d34565b604051908082528060200260200182016040528015611351578160200160208202803683370190505b50915060005b81811015610966576000600c8281548110611374576113746133f7565b60009182526020909120600290910201805487519192506001600160a01b0316908790849081106113a7576113a76133f7565b6001600160a01b039283166020918202929092010152600182015486519116908690849081106113d9576113d96133f7565b60200260200101906001600160a01b031690816001600160a01b031681525050600a600087848151811061140f5761140f6133f7565b60200260200101516001600160a01b03166001600160a01b031681526020019081526020016000205484838151811061144a5761144a6133f7565b602090810291909101015250600101611357565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146114cb57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6114da87878787878787612a32565b50505050505050565b6000600f5460ff1660038111156114fc576114fc612ffd565b1461152157600f546040516312ba63f960e11b81526106ad9160ff1690600401613013565b600e5460005b8181101561158057600d6000600e8381548110611546576115466133f7565b60009182526020808320909101546001600160a01b0316835282019290925260400181209081556001908101805460ff1916905501611527565b5061158d600e6000612c90565b5080517f0000000000000000000000000000000000000000000000000000000000000000907f000000000000000000000000000000000000000000000000000000000000000010156116175781517f0000000000000000000000000000000000000000000000000000000000000000604051630962235760e31b81526004016106ad9291906130a8565b815160005b818110156118a257806000036116cd577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316848281518110611668576116686133f7565b6020026020010151600001516001600160a01b0316146116cd5760405163ef77b46760e01b8152600481018290526001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001660248201526044016106ad565b60006001600160a01b03168482815181106116ea576116ea6133f7565b6020026020010151600001516001600160a01b0316036117205760405163621caef560e11b8152600481018290526024016106ad565b6000600d6000868481518110611738576117386133f7565b6020026020010151600001516001600160a01b03166001600160a01b031681526020019081526020016000209050806000015460001461178e576040516308308c2360e21b8152600481018390526024016106ad565b60006117bb85847f0000000000000000000000000000000000000000000000000000000000000000610d78565b905060008684815181106117d1576117d16133f7565b6020026020010151602001519050818110156118065783818360405163c4aa8aa360e01b81526004016106ad9392919061345b565b8086101561182d57838187604051636d3ae3a760e01b81526004016106ad9392919061345b565b61183781876133e4565b9550600e87858151811061184d5761184d6133f7565b6020908102919091018101515182546001808201855560009485529290932090920180546001600160a01b0319166001600160a01b0390931692909217909155908355918201805460ff19169055500161161c565b50505050565b6001600f5460ff1660038111156118c1576118c1612ffd565b141580156118e657506002600f5460ff1660038111156118e3576118e3612ffd565b14155b1561190b576002546040516310004b0160e11b815260048101919091526024016106ad565b60006119178383612aa3565b600c549091506000908190815b818110156119bd576000600c8281548110611941576119416133f7565b6000918252602090912060029091020180549091506001600160a01b03808a169116036119b45760018101546001600160a01b03808816911603611989575050505050505050565b600190810180546001600160a01b038881166001600160a01b031983161790925516945092506119bd565b50600101611924565b50816119de5785604051632fd768d360e11b81526004016106ad9190613094565b856001600160a01b03167f25639ce407c98fba722dddf1f023c8242e3c77732cfe4660d1c04930bf4cbe728486604051611a19929190612fe3565b60405180910390a2505050505050565b600c5460005b81811015611aec576000600c8281548110611a4c57611a4c6133f7565b600091825260208083206002909202909101546001600160a01b0316808352600a9091526040909120549091506003600f5460ff166003811115611a9257611a92612ffd565b14158015611aa05750600081115b15611ad957611ad96001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001683836129da565b611ae282612ac1565b5050600101611a2f565b50611af9600c6000612cae565b600f805460ff191690556040805160008082526020820190925281611b40565b6040805180820190915260008082526020820152815260200190600190039081611b195790505b5090506107b3816114e3565b6001600160a01b0381166000908152600a602052604081205490819003611b7257919050565b611b7b82612ac1565b600c5460005b81811015611c9657600c8181548110611b9c57611b9c6133f7565b60009182526020909120600290910201546001600160a01b0390811690851603611c8e57600c611bcd6001846133e4565b81548110611bdd57611bdd6133f7565b9060005260206000209060020201600c8281548110611bfe57611bfe6133f7565b60009182526020909120825460029092020180546001600160a01b039283166001600160a01b0319918216178255600193840154939091018054939092169216919091179055600c805480611c5557611c55613471565b60008281526020902060026000199092019182020180546001600160a01b03199081168255600191909101805490911690559055611c96565b600101611b81565b506001600160a01b0383166000908152600d6020526040902060018101805460ff191690556003600f5460ff166003811115611cd457611cd4612ffd565b03611ce25760009250611d42565b611d166001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001685856129da565b6002600f5460ff166003811115611d2f57611d2f612ffd565b03611d4257600f805460ff191660011790555b5050919050565b6000600f5460ff166003811115611d6257611d62612ffd565b14158015611d8757506001600f5460ff166003811115611d8457611d84612ffd565b14155b15611dac57600f54604051630295f95f60e51b81526106ad9160ff1690600401613013565b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166386fa50636040518163ffffffff1660e01b8152600401602060405180830381865afa158015611e0a573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611e2e9190613442565b7f000000000000000000000000000000000000000000000000000000000000000014611f12577f00000000000000000000000000000000000000000000000000000000000000007f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166386fa50636040518163ffffffff1660e01b8152600401602060405180830381865afa158015611ed3573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611ef79190613442565b604051631248081b60e31b81526004016106ad9291906130a8565b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166356d399e86040518163ffffffff1660e01b8152600401602060405180830381865afa158015611f70573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611f949190613442565b7f000000000000000000000000000000000000000000000000000000000000000014612039577f00000000000000000000000000000000000000000000000000000000000000007f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166356d399e86040518163ffffffff1660e01b8152600401602060405180830381865afa158015611ed3573d6000803e3d6000fd5b6000600f5460ff16600381111561205257612052612ffd565b03612145577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316836001600160a01b0316146120cd57827f00000000000000000000000000000000000000000000000000000000000000006040516316a4ebc160e21b81526004016106ad929190612fe3565b600f805460ff1916600117905560025460055460405161ffff90911681526001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001691907fe52a01bb4ffcc3baf2c5d17111d7d5b676a7485809eb10671092a89d2bb22ff69060200160405180910390a35b6001600160a01b0383166000908152600d602052604090208054158015906121725750600181015460ff16155b156121b257805482101561219d57805460405163454cd8e160e01b81526106ad9184916004016130a8565b6001818101805460ff19169091179055612207565b6001600160a01b0384166000908152600a60205260409020541580156121de57506121db610e62565b82105b1561220757816121ec610e62565b6040516310e45ff160e21b81526004016106ad9291906130a8565b6001600160a01b0384166000908152600a602052604081205490036122d85760006122328585612aa3565b604080518082019091526001600160a01b03808816825291821660208201908152600c8054600181018255600091909152915160029092027fdf6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c7810180549385166001600160a01b031994851617905590517fdf6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c89091018054919093169116179055506122e2565b6122e284846118a8565b6001600160a01b0384166000908152600a60205260408120805484929061230a90849061340d565b90915550506001600160a01b0384166000908152600b6020526040812042905561233261096d565b9050600061233e610fce565b90507f000000000000000000000000000000000000000000000000000000000000000061236b828461340d565b11156123b05781817f000000000000000000000000000000000000000000000000000000000000000060405163513b428b60e11b81526004016106ad9392919061345b565b600c547f00000000000000000000000000000000000000000000000000000000000000001015612415576040516315b5c3d560e21b81527f000000000000000000000000000000000000000000000000000000000000000060048201526024016106ad565b7f000000000000000000000000000000000000000000000000000000000000000082036124a2576002546040516001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001691907f48996614f3380f3dcd5a2d2e6eaddfd66207ff3457fee339e09b4735bc9ec95890600090a3600f805460ff191660021790555b856001600160a01b03167fbdaa686eb6f59012d211a74523da260341c516896e9e5be954163d6ecf26ffa2856040516124dd91815260200190565b60405180910390a261251a6001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016873087612ae8565b6002600f5460ff16600381111561253357612533612ffd565b1480156125485750600f54610100900460ff16155b15612555576125556125e7565b505050505050565b6000600f5460ff16600381111561257657612576612ffd565b1461259b57600f5460405163ad88fc8f60e01b81526106ad9160ff1690600401613013565b61271061ffff821611156125cf57604051624ae8fd60e41b815261ffff8216600482015261271060248201526044016106ad565b6005805461ffff191661ffff92909216919091179055565b6002600f5460ff16600381111561260057612600612ffd565b1461262557600f546040516383696f6f60e01b81526106ad9160ff1690600401613013565b600f805460ff191660031790556002546040517f839cf22e1ba87ce2f5b9bbf46cf0175a09eed52febdfaac8852478e68203c76390600090a2600c546000816001600160401b0381111561267b5761267b612d34565b6040519080825280602002602001820160405280156126b457816020015b6126a1612ccf565b8152602001906001900390816126995790505b50905060005b8281101561274f576000600c82815481106126d7576126d76133f7565b6000918252602080832060408051608081018252600290940290910180546001600160a01b03908116858401818152600184015490921660608701529085528552600a83529320549082015284519192509084908490811061273b5761273b6133f7565b6020908102919091010152506001016126ba565b5060405163095ea7b360e01b81526001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169063095ea7b3906127de907f0000000000000000000000000000000000000000000000000000000000000000907f000000000000000000000000000000000000000000000000000000000000000090600401613487565b6020604051808303816000875af11580156127fd573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061282191906134a0565b50604051632f1fbfb360e21b81526001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169063bc7efecc90612877906000906006906002908790600401613504565b600060405180830381600087803b15801561289157600080fd5b505af1158015612555573d6000803e3d6000fd5b600f80549115156101000261ff0019909216919091179055565b6000600f5460ff1660038111156128d8576128d8612ffd565b146128fd57600f54604051639a0293fd60e01b81526106ad9160ff1690600401613013565b60405160016226579360e01b031981526001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169063ffd9a86d9061297290889088907f000000000000000000000000000000000000000000000000000000000000000090899060040161357b565b600060405180830381600087803b15801561298c57600080fd5b505af11580156129a0573d6000803e3d6000fd5b5050865160005550506020948501516001558351600655938301516007556040830151600855606090920151600955600255600355600455565b6111da83846001600160a01b031663a9059cbb8585604051602401612a00929190613487565b604051602081830303815290604052915060e01b6020820180516001600160e01b038381831617835250505050612b21565b612a3a611a29565b612a5387878760000151886020015189604001516128bf565b612a60856060015161255d565b612a69846114e3565b612a72836128a5565b80156114da576114da7f00000000000000000000000000000000000000000000000000000000000000008383611d49565b60006001600160a01b03821615612aba5781610e02565b5090919050565b6001600160a01b03166000908152600a60209081526040808320839055600b909152812055565b6040516001600160a01b0384811660248301528381166044830152606482018390526118a29186918216906323b872dd90608401612a00565b6000612b366001600160a01b03841683612b7b565b90508051600014158015612b5b575080806020019051810190612b5991906134a0565b155b156111da5782604051635274afe760e01b81526004016106ad9190613094565b6060610e028383600084600080856001600160a01b03168486604051612ba191906135b6565b60006040518083038185875af1925050503d8060008114612bde576040519150601f19603f3d011682016040523d82523d6000602084013e612be3565b606091505b5091509150612bf3868383612bfd565b9695505050505050565b606082612c1257612c0d82612c50565b610e02565b8151158015612c2957506001600160a01b0384163b155b15612c495783604051639996b31560e01b81526004016106ad9190613094565b5080610e02565b805115612c605780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b604080518082019091526000808252602082015290565b50805460008255906000526020600020908101906106bf9190612cef565b50805460008255600202906000526020600020908101906106bf9190612d08565b6040518060400160405280612ce2612c79565b8152602001600081525090565b5b80821115612d045760008155600101612cf0565b5090565b5b80821115612d045780546001600160a01b031990811682556001820180549091169055600201612d09565b634e487b7160e01b600052604160045260246000fd5b604080519081016001600160401b0381118282101715612d6c57612d6c612d34565b60405290565b604051608081016001600160401b0381118282101715612d6c57612d6c612d34565b604051601f8201601f191681016001600160401b0381118282101715612dbc57612dbc612d34565b604052919050565b80356001600160a01b0381168114612ddb57600080fd5b919050565b600082601f830112612df157600080fd5b81356001600160401b03811115612e0a57612e0a612d34565b612e1960208260051b01612d94565b8082825260208201915060208360061b860101925085831115612e3b57600080fd5b602085015b83811015612e855760408188031215612e5857600080fd5b612e60612d4a565b612e6982612dc4565b8152602082810135818301529084529290920191604001612e40565b5095945050505050565b600060208284031215612ea157600080fd5b81356001600160401b03811115612eb757600080fd5b612ec384828501612de0565b949350505050565b600060208284031215612edd57600080fd5b610e0282612dc4565b600081518084526020840193506020830160005b82811015612f215781516001600160a01b0316865260209586019590910190600101612efa565b5093949350505050565b600081518084526020840193506020830160005b82811015612f21578151865260209586019590910190600101612f3f565b606081526000612f706060830186612ee6565b8281036020840152612f828186612f2b565b83810360408501528451808252602080870193509091019060005b81811015612fbd5783511515835260209384019390920191600101612f9d565b5090979650505050505050565b600060208284031215612fdc57600080fd5b5035919050565b6001600160a01b0392831681529116602082015260400190565b634e487b7160e01b600052602160045260246000fd5b602081016004831061303557634e487b7160e01b600052602160045260246000fd5b91905290565b6000806040838503121561304e57600080fd5b8235915061305e60208401612dc4565b90509250929050565b803561ffff81168114612ddb57600080fd5b60006020828403121561308b57600080fd5b610e0282613067565b6001600160a01b0391909116815260200190565b918252602082015260400190565b805182526020810151602083015260408101516040830152606081015160608301525050565b60808101610fc882846130b6565b80151581146106bf57600080fd5b8035612ddb816130ea565b600080600080600060a0868803121561311b57600080fd5b61312486613067565b945060208601356001600160401b0381111561313f57600080fd5b61314b88828901612de0565b945050604086013561315c816130ea565b925061316a60608701612dc4565b949793965091946080013592915050565b60006040828403121561318d57600080fd5b613195612d4a565b823581526020928301359281019290925250919050565b6000608082840312156131be57600080fd5b6131c6612d72565b8235815260208084013590820152604080840135908201526060928301359281019290925250919050565b6000806000806000610120868803121561320a57600080fd5b613214878761317b565b945061322387604088016131ac565b949794965050505060c08301359260e081013592610100909101359150565b60006020828403121561325457600080fd5b8135610e02816130ea565b60008060006060848603121561327457600080fd5b505081359360208301359350604090920135919050565b80516001600160a01b03908116835260209182015116910152565b60408101610fc8828461328b565b6060815260006132c76060830186612ee6565b82810360208401526132d98186612ee6565b90508281036040840152612bf38185612f2b565b60008060008060008060008789036101c081121561330a57600080fd5b6133148a8a61317b565b97506133238a60408b016131ac565b9650608060bf198201121561333757600080fd5b50613340612d72565b60c0890135815260e0890135602082015261010089013560408201526133696101208a01613067565b606082015294506101408801356001600160401b0381111561338a57600080fd5b6133968a828b01612de0565b9450506133a661016089016130f8565b92506133b56101808901612dc4565b9699959850939692959194919350506101a09091013590565b634e487b7160e01b600052601160045260246000fd5b81810381811115610fc857610fc86133ce565b634e487b7160e01b600052603260045260246000fd5b80820180821115610fc857610fc86133ce565b60008261343d57634e487b7160e01b600052601260045260246000fd5b500490565b60006020828403121561345457600080fd5b5051919050565b9283526020830191909152604082015260600190565b634e487b7160e01b600052603160045260246000fd5b6001600160a01b03929092168252602082015260400190565b6000602082840312156134b257600080fd5b8151610e02816130ea565b600081518084526020840193506020830160005b82811015612f215781516134e687825161328b565b602090810151604088015260609096019591909101906001016134d1565b8454815260018501546020820152835460408201526001840154606082015260028401546080820152600384015460a0820152825460c0820152600183015460e0820152600283015461010082015261ffff6003840154166101208201526101606101408201526000612bf36101608301846134bd565b8451815260208086015190820152610100810161359b60408301866130b6565b6001600160a01b039390931660c082015260e0015292915050565b6000825160005b818110156135d757602081860181015185830152016135bd565b50600092019182525091905056fea164736f6c634300081a000aa164736f6c634300081a000a", + "bytecode": "0x6080604052348015600f57600080fd5b50615ec18061001f6000396000f3fe608060405234801561001057600080fd5b50600436106100af5760003560e01c806333623794146100b45780633f4ba83a146100dd5780635c975abb146100e7578063715018a6146100ff57806379ba5097146101075780638456cb591461010f5780638da5cb5b146101175780638faff8621461011f578063c4d66de814610132578063c70242ad14610145578063e30c397814610168578063f2fde38b14610170578063f7bc39bf14610183575b600080fd5b6000546100c7906001600160a01b031681565b6040516100d49190610809565b60405180910390f35b6100e56101af565b005b6100ef6101c1565b60405190151581526020016100d4565b6100e56101d6565b6100e56101e8565b6100e5610230565b6100c7610240565b6100c761012d36600461084a565b61025b565b6100e5610140366004610925565b610387565b6100ef610153366004610925565b60016020526000908152604090205460ff1681565b6100c76104a3565b6100e561017e366004610925565b6104ae565b6100ef610191366004610925565b6001600160a01b031660009081526001602052604090205460ff1690565b6101b761051f565b6101bf610551565b565b6000806101cc6105a8565b5460ff1692915050565b6101de61051f565b6101bf60006105cc565b33806101f26104a3565b6001600160a01b031614610224578060405163118cdaa760e01b815260040161021b9190610809565b60405180910390fd5b61022d816105cc565b50565b61023861051f565b6101bf6105f3565b60008061024b61063a565b546001600160a01b031692915050565b600061026561065e565b60008054604080516386fa506360e01b815290516001600160a01b039092169182916386fa50639160048083019260209291908290030181865afa1580156102b1573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906102d59190610947565b8989898989896040516102e7906107fc565b6102f89897969594939291906109eb565b604051809103906000f080158015610314573d6000803e3d6000fd5b506001600160a01b038116600081815260016020819052604091829020805460ff1916909117905551919350839250907feac84630ba02e5ab324a651281c90ec45563a21f07fdf52b6f601f312e2de27a90610374908935815260200190565b60405180910390a2509695505050505050565b6000610391610684565b805490915060ff600160401b82041615906001600160401b03166000811580156103b85750825b90506000826001600160401b031660011480156103d45750303b155b9050811580156103e2575080155b156104005760405163f92ee8a960e01b815260040160405180910390fd5b84546001600160401b0319166001178555831561042957845460ff60401b1916600160401b1785555b600080546001600160a01b0319166001600160a01b03881617905561044d336106a8565b6104556106b9565b831561049b57845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b505050505050565b60008061024b6106c9565b6104b661051f565b60006104c06106c9565b80546001600160a01b0319166001600160a01b03841690811782559091506104e6610240565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b33610528610240565b6001600160a01b0316146101bf573360405163118cdaa760e01b815260040161021b9190610809565b6105596106ed565b60006105636105a8565b805460ff1916815590507f5db9ee0a495bf2e6ff9c91a7834c1ba4fdd244a5e8aa4e537bd38aeae4b073aa335b60405161059d9190610809565b60405180910390a150565b7fcd5ed15c6e187e77e9aee88184c21f4f2182ab5827cb3b7e07fbedcd63f0330090565b60006105d66106c9565b80546001600160a01b031916815590506105ef82610712565b5050565b6105fb61065e565b60006106056105a8565b805460ff1916600117815590507f62e78cea01bee320cd4e420270b5ea74000d11b0c9f74754ebdbfc544b05a2586105903390565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930090565b6106666101c1565b156101bf5760405163d93c066560e01b815260040160405180910390fd5b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0090565b6106b061076e565b61022d81610793565b6106c161076e565b6101bf6107c5565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0090565b6106f56101c1565b6101bf57604051638dfc202b60e01b815260040160405180910390fd5b600061071c61063a565b80546001600160a01b038481166001600160a01b031983168117845560405193945091169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a3505050565b6107766107e2565b6101bf57604051631afcd79f60e31b815260040160405180910390fd5b61079b61076e565b6001600160a01b038116610224576000604051631e4fbdf760e01b815260040161021b9190610809565b6107cd61076e565b60006107d76105a8565b805460ff1916905550565b60006107ec610684565b54600160401b900460ff16919050565b61543d80610a7883390190565b6001600160a01b0391909116815260200190565b60006080828403121561082f57600080fd5b50919050565b8035801515811461084557600080fd5b919050565b60008060008060008086880361018081121561086557600080fd5b604081121561087357600080fd5b50869550610884886040890161081d565b94506108938860c0890161081d565b93506101408701356001600160401b038111156108af57600080fd5b8701601f810189136108c057600080fd5b80356001600160401b038111156108d657600080fd5b8960208260061b84010111156108eb57600080fd5b602091909101935091506109026101608801610835565b90509295509295509295565b80356001600160a01b038116811461084557600080fd5b60006020828403121561093757600080fd5b6109408261090e565b9392505050565b60006020828403121561095957600080fd5b5051919050565b803582526020808201359083015260408082013590830152606081013561ffff811680821461098e57600080fd5b80606085015250505050565b81835260208301925060008160005b848110156109e1576001600160a01b036109c28361090e565b16865260208281013590870152604095860195909101906001016109a9565b5093949350505050565b6001600160a01b03891681526020808201899052873560408084019190915288820135606080850191909152883560808501529188013560a084015287013560c083015286013560e08201526000610a47610100830187610960565b6101c0610180830152610a5f6101c08301858761099a565b8315156101a08401529050999850505050505050505056fe610120604052600f805460ff1916905534801561001b57600080fd5b5060405161543d38038061543d83398101604081905261003a9161196c565b866001600160a01b0381166100a45760405162461bcd60e51b815260206004820152602560248201527f5368617265643a205a65726f2d61646472657373206973206e6f74207065726d6044820152641a5d1d195960da1b60648201526084015b60405180910390fd5b86806000036100f55760405162461bcd60e51b815260206004820152601b60248201527f5368617265643a2075696e7420696e70757420697320656d7074790000000000604482015260640161009b565b6001600160a01b03891660a081905260408051630ada733d60e31b815290516356d399e8916004808201926020929091908290030181865afa15801561013f573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101639190611a30565b60c0818152505060a0516001600160a01b0316637c89d2f06040518163ffffffff1660e01b8152600401602060405180830381865afa1580156101aa573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101ce9190611a49565b6001600160a01b03166080526101008890523260e05260006101f588888888888680610204565b50505050505050505050611c7f565b61020c61026c565b845160208601516040870151610225928a928a92610398565b6060850151610233906104ad565b61023c84610537565b600f805461ff0019166101008515150217905580156102635760e051610263908383610859565b50505050505050565b600c5460005b81811015610334576000600c828154811061028f5761028f611a66565b600091825260208083206002909202909101546001600160a01b0316808352600a9091526040909120549091506003600f5460ff1660038111156102d5576102d5611751565b141580156102e35750600081115b156102ff576080516102ff906001600160a01b03168383610e71565b61032a826001600160a01b03166000908152600a60209081526040808320839055600b909152812055565b5050600101610272565b50610341600c60006116cd565b600f805460ff191690556040805160008082526020820190925281610388565b60408051808201909152600080825260208201528152602001906001900390816103615790505b50905061039481610537565b5050565b6000600f5460ff1660038111156103b1576103b1611751565b146103d657600f54604051639a0293fd60e01b815261009b9160ff1690600401611a7c565b60a05160e0516040805160016226579360e01b03198152885160048201526020808a01516024830152885160448301528801516064820152908701516084820152606087015160a48201526001600160a01b0391821660c482015260e4810186905291169063ffd9a86d9061010401600060405180830381600087803b15801561045f57600080fd5b505af1158015610473573d6000803e3d6000fd5b5050865160005550506020948501516001558351600655938301516007556040830151600855606090920151600955600255600355600455565b6000600f5460ff1660038111156104c6576104c6611751565b146104eb57600f5460405163ad88fc8f60e01b815261009b9160ff1690600401611a7c565b61271061ffff8216111561051f57604051624ae8fd60e41b815261ffff82166004820152612710602482015260440161009b565b6005805461ffff191661ffff92909216919091179055565b6000600f5460ff16600381111561055057610550611751565b1461057557600f546040516312ba63f960e11b815261009b9160ff1690600401611a7c565b600e5460005b818110156105d457600d6000600e838154811061059a5761059a611a66565b60009182526020808320909101546001600160a01b0316835282019290925260400181209081556001908101805460ff191690550161057b565b506105e1600e60006116ee565b5060c051610100518251111561061957815161010051604051630962235760e31b81526004810192909252602482015260440161009b565b815160005b8181101561085357806000036106955760e0516001600160a01b031684828151811061064c5761064c611a66565b6020026020010151600001516001600160a01b0316146106955760e05160405163ef77b46760e01b8152600481018390526001600160a01b03909116602482015260440161009b565b60006001600160a01b03168482815181106106b2576106b2611a66565b6020026020010151600001516001600160a01b0316036106e85760405163621caef560e11b81526004810182905260240161009b565b6000600d600086848151811061070057610700611a66565b6020026020010151600001516001600160a01b03166001600160a01b0316815260200190815260200160002090508060000154600014610756576040516308308c2360e21b81526004810183905260240161009b565b600061076c858461010051610ece60201b60201c565b9050600086848151811061078257610782611a66565b6020026020010151602001519050818110156107b75783818360405163c4aa8aa360e01b815260040161009b93929190611aa4565b808610156107de57838187604051636d3ae3a760e01b815260040161009b93929190611aa4565b6107e88187611ad0565b9550600e8785815181106107fe576107fe611a66565b6020908102919091018101515182546001808201855560009485529290932090920180546001600160a01b0319166001600160a01b0390931692909217909155908355918201805460ff19169055500161061e565b50505050565b6000600f5460ff16600381111561087257610872611751565b1415801561089757506001600f5460ff16600381111561089457610894611751565b14155b156108bc57600f54604051630295f95f60e51b815261009b9160ff1690600401611a7c565b60a0516001600160a01b03166386fa50636040518163ffffffff1660e01b8152600401602060405180830381865afa1580156108fc573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109209190611a30565b61010051146109b3576101005160a0516001600160a01b03166386fa50636040518163ffffffff1660e01b8152600401602060405180830381865afa15801561096d573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109919190611a30565b604051631248081b60e31b81526004810192909252602482015260440161009b565b60a0516001600160a01b03166356d399e86040518163ffffffff1660e01b8152600401602060405180830381865afa1580156109f3573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610a179190611a30565b60c05114610a625760c05160a0516001600160a01b03166356d399e86040518163ffffffff1660e01b8152600401602060405180830381865afa15801561096d573d6000803e3d6000fd5b6000600f5460ff166003811115610a7b57610a7b611751565b03610b145760e0516001600160a01b0316836001600160a01b031614610ab95760e0516040516316a4ebc160e21b815261009b918591600401611ae3565b600f805460ff1916600117905560e05160025460055460405161ffff90911681526001600160a01b03909216917fe52a01bb4ffcc3baf2c5d17111d7d5b676a7485809eb10671092a89d2bb22ff69060200160405180910390a35b6001600160a01b0383166000908152600d60205260409020805415801590610b415750600181015460ff16155b15610b8a578054821015610b7557805460405163454cd8e160e01b815261009b918491600401918252602082015260400190565b6001818101805460ff19169091179055610be6565b6001600160a01b0384166000908152600a6020526040902054158015610bb65750610bb3610f65565b82105b15610be65781610bc4610f65565b6040516310e45ff160e21b81526004810192909252602482015260440161009b565b6001600160a01b0384166000908152600a60205260408120549003610cb7576000610c11858561102b565b604080518082019091526001600160a01b03808816825291821660208201908152600c8054600181018255600091909152915160029092027fdf6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c7810180549385166001600160a01b031994851617905590517fdf6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c8909101805491909316911617905550610cc1565b610cc18484611049565b6001600160a01b0384166000908152600a602052604081208054849290610ce9908490611afd565b90915550506001600160a01b0384166000908152600b60205260408120429055610d116111ca565b90506000610d1d611236565b60c051909150610d2d8284611afd565b1115610d5457818160c05160405163513b428b60e11b815260040161009b93929190611aa4565b61010051600c541115610d8157610100516040516315b5c3d560e21b815260040161009b91815260200190565b60c0518203610dd35760e0516002546040516001600160a01b03909216917f48996614f3380f3dcd5a2d2e6eaddfd66207ff3457fee339e09b4735bc9ec95890600090a3600f805460ff191660021790555b856001600160a01b03167fbdaa686eb6f59012d211a74523da260341c516896e9e5be954163d6ecf26ffa285604051610e0e91815260200190565b60405180910390a2608051610e2e906001600160a01b03168730876112a8565b6002600f5460ff166003811115610e4757610e47611751565b148015610e5c5750600f54610100900460ff16155b15610e6957610e696112e1565b505050505050565b610ec983846001600160a01b031663a9059cbb8585604051602401610e97929190611b10565b60408051808303601f1901815291905260208101805160e09390931b6001600160e01b03938416179052915061154216565b505050565b6000828211610efa576040516376675ed960e11b8152600481018490526024810183905260440161009b565b82600003610f2b576004610f0f600186611ad0565b610f199190611b29565b610f24906001611afd565b9050610f5e565b6000610f378484611ad0565b905080610f45600187611ad0565b610f4f9190611b29565b610f5a906001611afd565b9150505b9392505050565b600e5460009081908190815b81811015610fe8576000600e8281548110610f8e57610f8e611a66565b60009182526020808320909101546001600160a01b0316808352600d909152604090912060018101549192509060ff16610fde578054610fce9087611afd565b9550610fdb600186611afd565b94505b5050600101610f71565b5061102383610ff56111ca565b60c0516110029190611ad0565b61100c9190611ad0565b600c5461101a908590611afd565b61010051610ece565b935050505090565b60006001600160a01b038216156110425781610f5e565b5090919050565b6001600f5460ff16600381111561106257611062611751565b1415801561108757506002600f5460ff16600381111561108457611084611751565b14155b156110ac576002546040516310004b0160e11b8152600481019190915260240161009b565b60006110b8838361102b565b600c549091506000908190815b8181101561115e576000600c82815481106110e2576110e2611a66565b6000918252602090912060029091020180549091506001600160a01b03808a169116036111555760018101546001600160a01b0380881691160361112a575050505050505050565b600190810180546001600160a01b038881166001600160a01b0319831617909255169450925061115e565b506001016110c5565b508161117f5785604051632fd768d360e11b815260040161009b9190611b4b565b856001600160a01b03167f25639ce407c98fba722dddf1f023c8242e3c77732cfe4660d1c04930bf4cbe7284866040516111ba929190611ae3565b60405180910390a2505050505050565b600c54600090815b81811015611231576000600c82815481106111ef576111ef611a66565b600091825260208083206002909202909101546001600160a01b0316808352600a9091526040909120549091506112269085611afd565b9350506001016111d2565b505090565b600e54600090815b81811015611231576000600e828154811061125b5761125b611a66565b60009182526020808320909101546001600160a01b0316808352600d909152604090912060018101549192509060ff1661129e57805461129b9086611afd565b94505b505060010161123e565b6040516001600160a01b0384811660248301528381166044830152606482018390526108539186918216906323b872dd90608401610e97565b6002600f5460ff1660038111156112fa576112fa611751565b1461131f57600f546040516383696f6f60e01b815261009b9160ff1690600401611a7c565b600f805460ff191660031790556002546040517f839cf22e1ba87ce2f5b9bbf46cf0175a09eed52febdfaac8852478e68203c76390600090a2600c546000816001600160401b038111156113755761137561177c565b6040519080825280602002602001820160405280156113c957816020015b60408051608081018252600091810182815260608201839052815260208101919091528152602001906001900390816113935790505b50905060005b82811015611464576000600c82815481106113ec576113ec611a66565b6000918252602080832060408051608081018252600290940290910180546001600160a01b03908116858401818152600184015490921660608701529085528552600a83529320549082015284519192509084908490811061145057611450611a66565b6020908102919091010152506001016113cf565b506080516001600160a01b031663095ea7b360a05160c0516040518363ffffffff1660e01b8152600401611499929190611b10565b6020604051808303816000875af11580156114b8573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906114dc9190611b5f565b5060a0516001600160a01b031663bc7efecc600060066002856040518563ffffffff1660e01b81526004016115149493929190611bd9565b600060405180830381600087803b15801561152e57600080fd5b505af1158015610e69573d6000803e3d6000fd5b60006115576001600160a01b0384168361159c565b9050805160001415801561157c57508080602001905181019061157a9190611b5f565b155b15610ec95782604051635274afe760e01b815260040161009b9190611b4b565b6060610f5e838360006115b0565b92915050565b6060814710156115d5573060405163cd78605960e01b815260040161009b9190611b4b565b600080856001600160a01b031684866040516115f19190611c50565b60006040518083038185875af1925050503d806000811461162e576040519150601f19603f3d011682016040523d82523d6000602084013e611633565b606091505b50909250905061164486838361164e565b9695505050505050565b6060826116635761165e826116a1565b610f5e565b815115801561167a57506001600160a01b0384163b155b1561169a5783604051639996b31560e01b815260040161009b9190611b4b565b5080610f5e565b8051156116b15780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b50565b50805460008255600202906000526020600020908101906116ca919061170c565b50805460008255906000526020600020908101906116ca919061173c565b5b808211156117385780546001600160a01b03199081168255600182018054909116905560020161170d565b5090565b5b80821115611738576000815560010161173d565b634e487b7160e01b600052602160045260246000fd5b6001600160a01b03811681146116ca57600080fd5b634e487b7160e01b600052604160045260246000fd5b604051608081016001600160401b03811182821017156117b4576117b461177c565b60405290565b604080519081016001600160401b03811182821017156117b4576117b461177c565b604051601f8201601f191681016001600160401b03811182821017156118045761180461177c565b604052919050565b60006080828403121561181e57600080fd5b611826611792565b8251815260208084015190820152604080840151908201526060928301519281019290925250919050565b60006080828403121561186357600080fd5b61186b611792565b825181526020808401519082015260408084015190820152606083015190915061ffff8116811461189b57600080fd5b606082015292915050565b600082601f8301126118b757600080fd5b81516001600160401b038111156118d0576118d061177c565b6118df60208260051b016117dc565b8082825260208201915060208360061b86010192508583111561190157600080fd5b602085015b8381101561194d576040818803121561191e57600080fd5b6119266117ba565b815161193181611767565b8152602082810151818301529084529290920191604001611906565b5095945050505050565b8051801515811461196757600080fd5b919050565b60008060008060008060008789036101c081121561198957600080fd5b885161199481611767565b60208a015190985096506040603f19820112156119b057600080fd5b506119b96117ba565b604089015181526060890151602082015294506119d98960808a0161180c565b93506119e9896101008a01611851565b6101808901519093506001600160401b03811115611a0657600080fd5b611a128a828b016118a6565b925050611a226101a08901611957565b905092959891949750929550565b600060208284031215611a4257600080fd5b5051919050565b600060208284031215611a5b57600080fd5b8151610f5e81611767565b634e487b7160e01b600052603260045260246000fd5b6020810160048310611a9e57634e487b7160e01b600052602160045260246000fd5b91905290565b9283526020830191909152604082015260600190565b634e487b7160e01b600052601160045260246000fd5b818103818111156115aa576115aa611aba565b6001600160a01b0392831681529116602082015260400190565b808201808211156115aa576115aa611aba565b6001600160a01b03929092168252602082015260400190565b600082611b4657634e487b7160e01b600052601260045260246000fd5b500490565b6001600160a01b0391909116815260200190565b600060208284031215611b7157600080fd5b610f5e82611957565b600081518084526020840193506020830160005b82811015611bcf578151805180516001600160a01b039081168952602091820151168189015290810151604088015260609096019590910190600101611b8e565b5093949350505050565b8454815260018501546020820152835460408201526001840154606082015260028401546080820152600384015460a0820152825460c0820152600183015460e0820152600283015461010082015261ffff6003840154166101208201526101606101408201526000611644610160830184611b7a565b6000825160005b81811015611c715760208186018101518583015201611c57565b506000920191825250919050565b60805160a05160c05160e051610100516135f2611e4b6000396000818161050501528181610f36015281816115b4015281816115dd0152818161179701528181611e3001528181611e56015281816123b501526123e90152600081816104660152818161064b01528181610673015281816106d601528181610a2801528181610a5001528181610afe01528181610b2601528181610bcb01528181610bf301528181610c5b01528181610c9301528181610cbb01528181610d0d01528181610d350152818161104b01528181611073015281816111b5015281816112140152818161123c01528181611469015281816114910152818161162e015281816116a00152818161205901528181612093015281816120f50152818161244c015281816129480152612a7d01526000818161043f01528181610ef80152818161159201528181611f9601528181611fbc01528181612342015281816123750152818161241701526127b601526000818161034a01528181611dae01528181611e7701528181611f1401528181611fdd015281816127940152818161283901526129170152600081816105a601528181611ab201528181611cef015281816124f2015261276701526135f26000f3fe608060405234801561001057600080fd5b50600436106101e25760003560e01c8062e9a407146101e75780630aaffd2a146101fc5780630d616d201461020f5780630d9639ba146102175780630dcf4b8f146102375780630ebb172a1461024d57806312fa329b1461026f578063200d2ed2146102905780632816ee73146102aa5780632c6cda93146102bd5780632c7baaf3146102d0578063313f336b1461030f5780633362379414610345578063356c299d1461037957806342e94c90146103b75780634564168b146103d75780634a387cdf146103f35780634bb278f3146103fb5780634e7320ce14610403578063565a9d481461041857806356d399e81461043a578063570ca7351461046157806358b810d3146104885780636786452c1461049b5780636cf72aa4146104ae5780637c0b4bfb146104c157806386fa5063146105005780638c7e7a31146105275780638dd085571461053a578063937e09b11461055a57806396a608c0146105625780639ca002a61461056a578063a4b6aa831461057d578063bc063e1a14610585578063c3aca558146105a1578063ccec3716146105c8578063d26146a5146105db578063d826f88f146105ee578063dda0a81a146105f6578063e020e4c41461060d578063e50d50d814610620575b600080fd5b6101fa6101f5366004612e8f565b610640565b005b6101fa61020a366004612ecb565b6106c2565b6101fa6106cc565b61021f6107b7565b60405161022e93929190612f5d565b60405180910390f35b61023f61096d565b60405190815260200161022e565b6102576201518081565b6040516001600160401b03909116815260200161022e565b61028261027d366004612fca565b6109d9565b60405161022e929190612fe3565b600f5461029d9060ff1681565b60405161022e9190613013565b6101fa6102b836600461303b565b610a12565b6101fa6102cb366004613079565b610a1d565b6102d8610a93565b60405161022e919081518152602080830151908201526040808301519082015260609182015161ffff169181019190915260800190565b6006546007546008546009546103259392919084565b60408051948552602085019390935291830152606082015260800161022e565b61036c7f000000000000000000000000000000000000000000000000000000000000000081565b60405161022e9190613094565b6002546003546004546005546103939392919061ffff1684565b6040805194855260208501939093529183015261ffff16606082015260800161022e565b61023f6103c5366004612ecb565b600a6020526000908152604090205481565b6000546001546103e5919082565b60405161022e9291906130a8565b600c5461023f565b6101fa610af3565b61040b610b68565b60405161022e91906130dc565b600f5461042a90610100900460ff1681565b604051901515815260200161022e565b61023f7f000000000000000000000000000000000000000000000000000000000000000081565b61036c7f000000000000000000000000000000000000000000000000000000000000000081565b6101fa610496366004613103565b610bc0565b6101fa6104a93660046131f1565b610c88565b6101fa6104bc366004613242565b610d02565b6104eb6104cf366004612ecb565b600d602052600090815260409020805460019091015460ff1682565b6040805192835290151560208301520161022e565b61023f7f000000000000000000000000000000000000000000000000000000000000000081565b61023f61053536600461325f565b610d78565b61054d610548366004612fca565b610e09565b60405161022e91906132a6565b61023f610e62565b61023f610f62565b61023f610578366004612fca565b610fb9565b61023f610fce565b61058e61271081565b60405161ffff909116815260200161022e565b61036c7f000000000000000000000000000000000000000000000000000000000000000081565b6101fa6105d6366004612ecb565b611040565b61036c6105e9366004612fca565b6111df565b6101fa611209565b6105fe61127e565b60405161022e939291906132b4565b6101fa61061b3660046132ed565b61145e565b61023f61062e366004612ecb565b600b6020526000908152604090205481565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146106b657337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b60405180910390fd5b6106bf816114e3565b50565b6106bf33826118a8565b6001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016330361070657610704611a29565b565b336000908152600b602052604081205461072090426133e4565b90506201518081101561076a57336000908152600b602052604090819020549051630429b06960e31b815260048101919091524260248201526201518060448201526064016106ad565b600061077533611b4c565b905080156107b35760405181815233907f249a82fbe5056a1940b4e996665ba2a82c340ae9fa1e069fd1ababf5508f396e9060200160405180910390a25b5050565b600e5460609081908190806001600160401b038111156107d9576107d9612d34565b604051908082528060200260200182016040528015610802578160200160208202803683370190505b509350806001600160401b0381111561081d5761081d612d34565b604051908082528060200260200182016040528015610846578160200160208202803683370190505b509250806001600160401b0381111561086157610861612d34565b60405190808252806020026020018201604052801561088a578160200160208202803683370190505b50915060005b81811015610966576000600e82815481106108ad576108ad6133f7565b60009182526020808320909101546001600160a01b0316808352600d909152604090912087519192509082908890859081106108eb576108eb6133f7565b60200260200101906001600160a01b031690816001600160a01b0316815250508060000154868481518110610922576109226133f7565b60209081029190910101526001810154855160ff9091169086908590811061094c5761094c6133f7565b911515602092830291909101909101525050600101610890565b5050909192565b600c54600090815b818110156109d4576000600c8281548110610992576109926133f7565b600091825260208083206002909202909101546001600160a01b0316808352600a9091526040909120549091506109c9908561340d565b935050600101610975565b505090565b600c81815481106109e957600080fd5b6000918252602090912060029091020180546001909101546001600160a01b0391821692501682565b6107b3338284611d49565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610a8a57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6106bf8161255d565b610ac26040518060800160405280600081526020016000815260200160008152602001600061ffff1681525090565b5060408051608081018252600254815260035460208201526004549181019190915260055461ffff16606082015290565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610b6057337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6107046125e7565b610b936040518060800160405280600081526020016000815260200160008152602001600081525090565b50604080516080810182526006548152600754602082015260085491810191909152600954606082015290565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610c2d57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b610c35611a29565b610c3e8561255d565b610c47846114e3565b610c50836128a5565b8015610c8157610c817f00000000000000000000000000000000000000000000000000000000000000008383611d49565b5050505050565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610cf557337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b610c8185858585856128bf565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610d6f57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6106bf816128a5565b6000828211610d9e5782826040516376675ed960e11b81526004016106ad9291906130a8565b82600003610dcf576004610db36001866133e4565b610dbd9190613420565b610dc890600161340d565b9050610e02565b6000610ddb84846133e4565b905080610de96001876133e4565b610df39190613420565b610dfe90600161340d565b9150505b9392505050565b610e11612c79565b600c8281548110610e2457610e246133f7565b60009182526020918290206040805180820190915260029092020180546001600160a01b039081168352600190910154169181019190915292915050565b600e5460009081908190815b81811015610ee5576000600e8281548110610e8b57610e8b6133f7565b60009182526020808320909101546001600160a01b0316808352600d909152604090912060018101549192509060ff16610edb578054610ecb908761340d565b9550610ed860018661340d565b94505b5050600101610e6e565b50610f5a83610ef261096d565b610f1c907f00000000000000000000000000000000000000000000000000000000000000006133e4565b610f2691906133e4565b600c54610f3490859061340d565b7f0000000000000000000000000000000000000000000000000000000000000000610d78565b935050505090565b600c54600090610f725750600090565b600a6000600c600081548110610f8a57610f8a6133f7565b600091825260208083206002909202909101546001600160a01b03168352820192909252604001902054905090565b6000610fc88260006001610d78565b92915050565b600e54600090815b818110156109d4576000600e8281548110610ff357610ff36133f7565b60009182526020808320909101546001600160a01b0316808352600d909152604090912060018101549192509060ff16611036578054611033908661340d565b94505b5050600101610fd6565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146110ad57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6003600f5460ff1660038111156110c6576110c6612ffd565b141580156110ea57506000600f5460ff1660038111156110e8576110e8612ffd565b145b1561110f57600f54604051632f3e69dd60e01b81526106ad9160ff1690600401613013565b6040516370a0823160e01b815281906000906001600160a01b038316906370a0823190611140903090600401613094565b602060405180830381865afa15801561115d573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906111819190613442565b9050600081116111a6578260405163e932c57360e01b81526004016106ad9190613094565b6111da6001600160a01b0383167f0000000000000000000000000000000000000000000000000000000000000000836129da565b505050565b600e81815481106111ef57600080fd5b6000918252602090912001546001600160a01b0316905081565b336001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000161461127657337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b610704611a29565b600c5460609081908190806001600160401b038111156112a0576112a0612d34565b6040519080825280602002602001820160405280156112c9578160200160208202803683370190505b509350806001600160401b038111156112e4576112e4612d34565b60405190808252806020026020018201604052801561130d578160200160208202803683370190505b509250806001600160401b0381111561132857611328612d34565b604051908082528060200260200182016040528015611351578160200160208202803683370190505b50915060005b81811015610966576000600c8281548110611374576113746133f7565b60009182526020909120600290910201805487519192506001600160a01b0316908790849081106113a7576113a76133f7565b6001600160a01b039283166020918202929092010152600182015486519116908690849081106113d9576113d96133f7565b60200260200101906001600160a01b031690816001600160a01b031681525050600a600087848151811061140f5761140f6133f7565b60200260200101516001600160a01b03166001600160a01b031681526020019081526020016000205484838151811061144a5761144a6133f7565b602090810291909101015250600101611357565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146114cb57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6114da87878787878787612a32565b50505050505050565b6000600f5460ff1660038111156114fc576114fc612ffd565b1461152157600f546040516312ba63f960e11b81526106ad9160ff1690600401613013565b600e5460005b8181101561158057600d6000600e8381548110611546576115466133f7565b60009182526020808320909101546001600160a01b0316835282019290925260400181209081556001908101805460ff1916905501611527565b5061158d600e6000612c90565b5080517f0000000000000000000000000000000000000000000000000000000000000000907f000000000000000000000000000000000000000000000000000000000000000010156116175781517f0000000000000000000000000000000000000000000000000000000000000000604051630962235760e31b81526004016106ad9291906130a8565b815160005b818110156118a257806000036116cd577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316848281518110611668576116686133f7565b6020026020010151600001516001600160a01b0316146116cd5760405163ef77b46760e01b8152600481018290526001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001660248201526044016106ad565b60006001600160a01b03168482815181106116ea576116ea6133f7565b6020026020010151600001516001600160a01b0316036117205760405163621caef560e11b8152600481018290526024016106ad565b6000600d6000868481518110611738576117386133f7565b6020026020010151600001516001600160a01b03166001600160a01b031681526020019081526020016000209050806000015460001461178e576040516308308c2360e21b8152600481018390526024016106ad565b60006117bb85847f0000000000000000000000000000000000000000000000000000000000000000610d78565b905060008684815181106117d1576117d16133f7565b6020026020010151602001519050818110156118065783818360405163c4aa8aa360e01b81526004016106ad9392919061345b565b8086101561182d57838187604051636d3ae3a760e01b81526004016106ad9392919061345b565b61183781876133e4565b9550600e87858151811061184d5761184d6133f7565b6020908102919091018101515182546001808201855560009485529290932090920180546001600160a01b0319166001600160a01b0390931692909217909155908355918201805460ff19169055500161161c565b50505050565b6001600f5460ff1660038111156118c1576118c1612ffd565b141580156118e657506002600f5460ff1660038111156118e3576118e3612ffd565b14155b1561190b576002546040516310004b0160e11b815260048101919091526024016106ad565b60006119178383612aa3565b600c549091506000908190815b818110156119bd576000600c8281548110611941576119416133f7565b6000918252602090912060029091020180549091506001600160a01b03808a169116036119b45760018101546001600160a01b03808816911603611989575050505050505050565b600190810180546001600160a01b038881166001600160a01b031983161790925516945092506119bd565b50600101611924565b50816119de5785604051632fd768d360e11b81526004016106ad9190613094565b856001600160a01b03167f25639ce407c98fba722dddf1f023c8242e3c77732cfe4660d1c04930bf4cbe728486604051611a19929190612fe3565b60405180910390a2505050505050565b600c5460005b81811015611aec576000600c8281548110611a4c57611a4c6133f7565b600091825260208083206002909202909101546001600160a01b0316808352600a9091526040909120549091506003600f5460ff166003811115611a9257611a92612ffd565b14158015611aa05750600081115b15611ad957611ad96001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001683836129da565b611ae282612ac1565b5050600101611a2f565b50611af9600c6000612cae565b600f805460ff191690556040805160008082526020820190925281611b40565b6040805180820190915260008082526020820152815260200190600190039081611b195790505b5090506107b3816114e3565b6001600160a01b0381166000908152600a602052604081205490819003611b7257919050565b611b7b82612ac1565b600c5460005b81811015611c9657600c8181548110611b9c57611b9c6133f7565b60009182526020909120600290910201546001600160a01b0390811690851603611c8e57600c611bcd6001846133e4565b81548110611bdd57611bdd6133f7565b9060005260206000209060020201600c8281548110611bfe57611bfe6133f7565b60009182526020909120825460029092020180546001600160a01b039283166001600160a01b0319918216178255600193840154939091018054939092169216919091179055600c805480611c5557611c55613471565b60008281526020902060026000199092019182020180546001600160a01b03199081168255600191909101805490911690559055611c96565b600101611b81565b506001600160a01b0383166000908152600d6020526040902060018101805460ff191690556003600f5460ff166003811115611cd457611cd4612ffd565b03611ce25760009250611d42565b611d166001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001685856129da565b6002600f5460ff166003811115611d2f57611d2f612ffd565b03611d4257600f805460ff191660011790555b5050919050565b6000600f5460ff166003811115611d6257611d62612ffd565b14158015611d8757506001600f5460ff166003811115611d8457611d84612ffd565b14155b15611dac57600f54604051630295f95f60e51b81526106ad9160ff1690600401613013565b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166386fa50636040518163ffffffff1660e01b8152600401602060405180830381865afa158015611e0a573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611e2e9190613442565b7f000000000000000000000000000000000000000000000000000000000000000014611f12577f00000000000000000000000000000000000000000000000000000000000000007f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166386fa50636040518163ffffffff1660e01b8152600401602060405180830381865afa158015611ed3573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611ef79190613442565b604051631248081b60e31b81526004016106ad9291906130a8565b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166356d399e86040518163ffffffff1660e01b8152600401602060405180830381865afa158015611f70573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611f949190613442565b7f000000000000000000000000000000000000000000000000000000000000000014612039577f00000000000000000000000000000000000000000000000000000000000000007f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166356d399e86040518163ffffffff1660e01b8152600401602060405180830381865afa158015611ed3573d6000803e3d6000fd5b6000600f5460ff16600381111561205257612052612ffd565b03612145577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316836001600160a01b0316146120cd57827f00000000000000000000000000000000000000000000000000000000000000006040516316a4ebc160e21b81526004016106ad929190612fe3565b600f805460ff1916600117905560025460055460405161ffff90911681526001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001691907fe52a01bb4ffcc3baf2c5d17111d7d5b676a7485809eb10671092a89d2bb22ff69060200160405180910390a35b6001600160a01b0383166000908152600d602052604090208054158015906121725750600181015460ff16155b156121b257805482101561219d57805460405163454cd8e160e01b81526106ad9184916004016130a8565b6001818101805460ff19169091179055612207565b6001600160a01b0384166000908152600a60205260409020541580156121de57506121db610e62565b82105b1561220757816121ec610e62565b6040516310e45ff160e21b81526004016106ad9291906130a8565b6001600160a01b0384166000908152600a602052604081205490036122d85760006122328585612aa3565b604080518082019091526001600160a01b03808816825291821660208201908152600c8054600181018255600091909152915160029092027fdf6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c7810180549385166001600160a01b031994851617905590517fdf6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c89091018054919093169116179055506122e2565b6122e284846118a8565b6001600160a01b0384166000908152600a60205260408120805484929061230a90849061340d565b90915550506001600160a01b0384166000908152600b6020526040812042905561233261096d565b9050600061233e610fce565b90507f000000000000000000000000000000000000000000000000000000000000000061236b828461340d565b11156123b05781817f000000000000000000000000000000000000000000000000000000000000000060405163513b428b60e11b81526004016106ad9392919061345b565b600c547f00000000000000000000000000000000000000000000000000000000000000001015612415576040516315b5c3d560e21b81527f000000000000000000000000000000000000000000000000000000000000000060048201526024016106ad565b7f000000000000000000000000000000000000000000000000000000000000000082036124a2576002546040516001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001691907f48996614f3380f3dcd5a2d2e6eaddfd66207ff3457fee339e09b4735bc9ec95890600090a3600f805460ff191660021790555b856001600160a01b03167fbdaa686eb6f59012d211a74523da260341c516896e9e5be954163d6ecf26ffa2856040516124dd91815260200190565b60405180910390a261251a6001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016873087612ae8565b6002600f5460ff16600381111561253357612533612ffd565b1480156125485750600f54610100900460ff16155b15612555576125556125e7565b505050505050565b6000600f5460ff16600381111561257657612576612ffd565b1461259b57600f5460405163ad88fc8f60e01b81526106ad9160ff1690600401613013565b61271061ffff821611156125cf57604051624ae8fd60e41b815261ffff8216600482015261271060248201526044016106ad565b6005805461ffff191661ffff92909216919091179055565b6002600f5460ff16600381111561260057612600612ffd565b1461262557600f546040516383696f6f60e01b81526106ad9160ff1690600401613013565b600f805460ff191660031790556002546040517f839cf22e1ba87ce2f5b9bbf46cf0175a09eed52febdfaac8852478e68203c76390600090a2600c546000816001600160401b0381111561267b5761267b612d34565b6040519080825280602002602001820160405280156126b457816020015b6126a1612ccf565b8152602001906001900390816126995790505b50905060005b8281101561274f576000600c82815481106126d7576126d76133f7565b6000918252602080832060408051608081018252600290940290910180546001600160a01b03908116858401818152600184015490921660608701529085528552600a83529320549082015284519192509084908490811061273b5761273b6133f7565b6020908102919091010152506001016126ba565b5060405163095ea7b360e01b81526001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169063095ea7b3906127de907f0000000000000000000000000000000000000000000000000000000000000000907f000000000000000000000000000000000000000000000000000000000000000090600401613487565b6020604051808303816000875af11580156127fd573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061282191906134a0565b50604051632f1fbfb360e21b81526001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169063bc7efecc90612877906000906006906002908790600401613504565b600060405180830381600087803b15801561289157600080fd5b505af1158015612555573d6000803e3d6000fd5b600f80549115156101000261ff0019909216919091179055565b6000600f5460ff1660038111156128d8576128d8612ffd565b146128fd57600f54604051639a0293fd60e01b81526106ad9160ff1690600401613013565b60405160016226579360e01b031981526001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169063ffd9a86d9061297290889088907f000000000000000000000000000000000000000000000000000000000000000090899060040161357b565b600060405180830381600087803b15801561298c57600080fd5b505af11580156129a0573d6000803e3d6000fd5b5050865160005550506020948501516001558351600655938301516007556040830151600855606090920151600955600255600355600455565b6111da83846001600160a01b031663a9059cbb8585604051602401612a00929190613487565b604051602081830303815290604052915060e01b6020820180516001600160e01b038381831617835250505050612b21565b612a3a611a29565b612a5387878760000151886020015189604001516128bf565b612a60856060015161255d565b612a69846114e3565b612a72836128a5565b80156114da576114da7f00000000000000000000000000000000000000000000000000000000000000008383611d49565b60006001600160a01b03821615612aba5781610e02565b5090919050565b6001600160a01b03166000908152600a60209081526040808320839055600b909152812055565b6040516001600160a01b0384811660248301528381166044830152606482018390526118a29186918216906323b872dd90608401612a00565b6000612b366001600160a01b03841683612b7b565b90508051600014158015612b5b575080806020019051810190612b5991906134a0565b155b156111da5782604051635274afe760e01b81526004016106ad9190613094565b6060610e028383600084600080856001600160a01b03168486604051612ba191906135b6565b60006040518083038185875af1925050503d8060008114612bde576040519150601f19603f3d011682016040523d82523d6000602084013e612be3565b606091505b5091509150612bf3868383612bfd565b9695505050505050565b606082612c1257612c0d82612c50565b610e02565b8151158015612c2957506001600160a01b0384163b155b15612c495783604051639996b31560e01b81526004016106ad9190613094565b5080610e02565b805115612c605780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b604080518082019091526000808252602082015290565b50805460008255906000526020600020908101906106bf9190612cef565b50805460008255600202906000526020600020908101906106bf9190612d08565b6040518060400160405280612ce2612c79565b8152602001600081525090565b5b80821115612d045760008155600101612cf0565b5090565b5b80821115612d045780546001600160a01b031990811682556001820180549091169055600201612d09565b634e487b7160e01b600052604160045260246000fd5b604080519081016001600160401b0381118282101715612d6c57612d6c612d34565b60405290565b604051608081016001600160401b0381118282101715612d6c57612d6c612d34565b604051601f8201601f191681016001600160401b0381118282101715612dbc57612dbc612d34565b604052919050565b80356001600160a01b0381168114612ddb57600080fd5b919050565b600082601f830112612df157600080fd5b81356001600160401b03811115612e0a57612e0a612d34565b612e1960208260051b01612d94565b8082825260208201915060208360061b860101925085831115612e3b57600080fd5b602085015b83811015612e855760408188031215612e5857600080fd5b612e60612d4a565b612e6982612dc4565b8152602082810135818301529084529290920191604001612e40565b5095945050505050565b600060208284031215612ea157600080fd5b81356001600160401b03811115612eb757600080fd5b612ec384828501612de0565b949350505050565b600060208284031215612edd57600080fd5b610e0282612dc4565b600081518084526020840193506020830160005b82811015612f215781516001600160a01b0316865260209586019590910190600101612efa565b5093949350505050565b600081518084526020840193506020830160005b82811015612f21578151865260209586019590910190600101612f3f565b606081526000612f706060830186612ee6565b8281036020840152612f828186612f2b565b83810360408501528451808252602080870193509091019060005b81811015612fbd5783511515835260209384019390920191600101612f9d565b5090979650505050505050565b600060208284031215612fdc57600080fd5b5035919050565b6001600160a01b0392831681529116602082015260400190565b634e487b7160e01b600052602160045260246000fd5b602081016004831061303557634e487b7160e01b600052602160045260246000fd5b91905290565b6000806040838503121561304e57600080fd5b8235915061305e60208401612dc4565b90509250929050565b803561ffff81168114612ddb57600080fd5b60006020828403121561308b57600080fd5b610e0282613067565b6001600160a01b0391909116815260200190565b918252602082015260400190565b805182526020810151602083015260408101516040830152606081015160608301525050565b60808101610fc882846130b6565b80151581146106bf57600080fd5b8035612ddb816130ea565b600080600080600060a0868803121561311b57600080fd5b61312486613067565b945060208601356001600160401b0381111561313f57600080fd5b61314b88828901612de0565b945050604086013561315c816130ea565b925061316a60608701612dc4565b949793965091946080013592915050565b60006040828403121561318d57600080fd5b613195612d4a565b823581526020928301359281019290925250919050565b6000608082840312156131be57600080fd5b6131c6612d72565b8235815260208084013590820152604080840135908201526060928301359281019290925250919050565b6000806000806000610120868803121561320a57600080fd5b613214878761317b565b945061322387604088016131ac565b949794965050505060c08301359260e081013592610100909101359150565b60006020828403121561325457600080fd5b8135610e02816130ea565b60008060006060848603121561327457600080fd5b505081359360208301359350604090920135919050565b80516001600160a01b03908116835260209182015116910152565b60408101610fc8828461328b565b6060815260006132c76060830186612ee6565b82810360208401526132d98186612ee6565b90508281036040840152612bf38185612f2b565b60008060008060008060008789036101c081121561330a57600080fd5b6133148a8a61317b565b97506133238a60408b016131ac565b9650608060bf198201121561333757600080fd5b50613340612d72565b60c0890135815260e0890135602082015261010089013560408201526133696101208a01613067565b606082015294506101408801356001600160401b0381111561338a57600080fd5b6133968a828b01612de0565b9450506133a661016089016130f8565b92506133b56101808901612dc4565b9699959850939692959194919350506101a09091013590565b634e487b7160e01b600052601160045260246000fd5b81810381811115610fc857610fc86133ce565b634e487b7160e01b600052603260045260246000fd5b80820180821115610fc857610fc86133ce565b60008261343d57634e487b7160e01b600052601260045260246000fd5b500490565b60006020828403121561345457600080fd5b5051919050565b9283526020830191909152604082015260600190565b634e487b7160e01b600052603160045260246000fd5b6001600160a01b03929092168252602082015260400190565b6000602082840312156134b257600080fd5b8151610e02816130ea565b600081518084526020840193506020830160005b82811015612f215781516134e687825161328b565b602090810151604088015260609096019591909101906001016134d1565b8454815260018501546020820152835460408201526001840154606082015260028401546080820152600384015460a0820152825460c0820152600183015460e0820152600283015461010082015261ffff6003840154166101208201526101606101408201526000612bf36101608301846134bd565b8451815260208086015190820152610100810161359b60408301866130b6565b6001600160a01b039390931660c082015260e0015292915050565b6000825160005b818110156135d757602081860181015185830152016135bd565b50600092019182525091905056fea164736f6c634300081a000aa164736f6c634300081a000a", + "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100af5760003560e01c806333623794146100b45780633f4ba83a146100dd5780635c975abb146100e7578063715018a6146100ff57806379ba5097146101075780638456cb591461010f5780638da5cb5b146101175780638faff8621461011f578063c4d66de814610132578063c70242ad14610145578063e30c397814610168578063f2fde38b14610170578063f7bc39bf14610183575b600080fd5b6000546100c7906001600160a01b031681565b6040516100d49190610809565b60405180910390f35b6100e56101af565b005b6100ef6101c1565b60405190151581526020016100d4565b6100e56101d6565b6100e56101e8565b6100e5610230565b6100c7610240565b6100c761012d36600461084a565b61025b565b6100e5610140366004610925565b610387565b6100ef610153366004610925565b60016020526000908152604090205460ff1681565b6100c76104a3565b6100e561017e366004610925565b6104ae565b6100ef610191366004610925565b6001600160a01b031660009081526001602052604090205460ff1690565b6101b761051f565b6101bf610551565b565b6000806101cc6105a8565b5460ff1692915050565b6101de61051f565b6101bf60006105cc565b33806101f26104a3565b6001600160a01b031614610224578060405163118cdaa760e01b815260040161021b9190610809565b60405180910390fd5b61022d816105cc565b50565b61023861051f565b6101bf6105f3565b60008061024b61063a565b546001600160a01b031692915050565b600061026561065e565b60008054604080516386fa506360e01b815290516001600160a01b039092169182916386fa50639160048083019260209291908290030181865afa1580156102b1573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906102d59190610947565b8989898989896040516102e7906107fc565b6102f89897969594939291906109eb565b604051809103906000f080158015610314573d6000803e3d6000fd5b506001600160a01b038116600081815260016020819052604091829020805460ff1916909117905551919350839250907feac84630ba02e5ab324a651281c90ec45563a21f07fdf52b6f601f312e2de27a90610374908935815260200190565b60405180910390a2509695505050505050565b6000610391610684565b805490915060ff600160401b82041615906001600160401b03166000811580156103b85750825b90506000826001600160401b031660011480156103d45750303b155b9050811580156103e2575080155b156104005760405163f92ee8a960e01b815260040160405180910390fd5b84546001600160401b0319166001178555831561042957845460ff60401b1916600160401b1785555b600080546001600160a01b0319166001600160a01b03881617905561044d336106a8565b6104556106b9565b831561049b57845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b505050505050565b60008061024b6106c9565b6104b661051f565b60006104c06106c9565b80546001600160a01b0319166001600160a01b03841690811782559091506104e6610240565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b33610528610240565b6001600160a01b0316146101bf573360405163118cdaa760e01b815260040161021b9190610809565b6105596106ed565b60006105636105a8565b805460ff1916815590507f5db9ee0a495bf2e6ff9c91a7834c1ba4fdd244a5e8aa4e537bd38aeae4b073aa335b60405161059d9190610809565b60405180910390a150565b7fcd5ed15c6e187e77e9aee88184c21f4f2182ab5827cb3b7e07fbedcd63f0330090565b60006105d66106c9565b80546001600160a01b031916815590506105ef82610712565b5050565b6105fb61065e565b60006106056105a8565b805460ff1916600117815590507f62e78cea01bee320cd4e420270b5ea74000d11b0c9f74754ebdbfc544b05a2586105903390565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930090565b6106666101c1565b156101bf5760405163d93c066560e01b815260040160405180910390fd5b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0090565b6106b061076e565b61022d81610793565b6106c161076e565b6101bf6107c5565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0090565b6106f56101c1565b6101bf57604051638dfc202b60e01b815260040160405180910390fd5b600061071c61063a565b80546001600160a01b038481166001600160a01b031983168117845560405193945091169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a3505050565b6107766107e2565b6101bf57604051631afcd79f60e31b815260040160405180910390fd5b61079b61076e565b6001600160a01b038116610224576000604051631e4fbdf760e01b815260040161021b9190610809565b6107cd61076e565b60006107d76105a8565b805460ff1916905550565b60006107ec610684565b54600160401b900460ff16919050565b61543d80610a7883390190565b6001600160a01b0391909116815260200190565b60006080828403121561082f57600080fd5b50919050565b8035801515811461084557600080fd5b919050565b60008060008060008086880361018081121561086557600080fd5b604081121561087357600080fd5b50869550610884886040890161081d565b94506108938860c0890161081d565b93506101408701356001600160401b038111156108af57600080fd5b8701601f810189136108c057600080fd5b80356001600160401b038111156108d657600080fd5b8960208260061b84010111156108eb57600080fd5b602091909101935091506109026101608801610835565b90509295509295509295565b80356001600160a01b038116811461084557600080fd5b60006020828403121561093757600080fd5b6109408261090e565b9392505050565b60006020828403121561095957600080fd5b5051919050565b803582526020808201359083015260408082013590830152606081013561ffff811680821461098e57600080fd5b80606085015250505050565b81835260208301925060008160005b848110156109e1576001600160a01b036109c28361090e565b16865260208281013590870152604095860195909101906001016109a9565b5093949350505050565b6001600160a01b03891681526020808201899052873560408084019190915288820135606080850191909152883560808501529188013560a084015287013560c083015286013560e08201526000610a47610100830187610960565b6101c0610180830152610a5f6101c08301858761099a565b8315156101a08401529050999850505050505050505056fe610120604052600f805460ff1916905534801561001b57600080fd5b5060405161543d38038061543d83398101604081905261003a9161196c565b866001600160a01b0381166100a45760405162461bcd60e51b815260206004820152602560248201527f5368617265643a205a65726f2d61646472657373206973206e6f74207065726d6044820152641a5d1d195960da1b60648201526084015b60405180910390fd5b86806000036100f55760405162461bcd60e51b815260206004820152601b60248201527f5368617265643a2075696e7420696e70757420697320656d7074790000000000604482015260640161009b565b6001600160a01b03891660a081905260408051630ada733d60e31b815290516356d399e8916004808201926020929091908290030181865afa15801561013f573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101639190611a30565b60c0818152505060a0516001600160a01b0316637c89d2f06040518163ffffffff1660e01b8152600401602060405180830381865afa1580156101aa573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101ce9190611a49565b6001600160a01b03166080526101008890523260e05260006101f588888888888680610204565b50505050505050505050611c7f565b61020c61026c565b845160208601516040870151610225928a928a92610398565b6060850151610233906104ad565b61023c84610537565b600f805461ff0019166101008515150217905580156102635760e051610263908383610859565b50505050505050565b600c5460005b81811015610334576000600c828154811061028f5761028f611a66565b600091825260208083206002909202909101546001600160a01b0316808352600a9091526040909120549091506003600f5460ff1660038111156102d5576102d5611751565b141580156102e35750600081115b156102ff576080516102ff906001600160a01b03168383610e71565b61032a826001600160a01b03166000908152600a60209081526040808320839055600b909152812055565b5050600101610272565b50610341600c60006116cd565b600f805460ff191690556040805160008082526020820190925281610388565b60408051808201909152600080825260208201528152602001906001900390816103615790505b50905061039481610537565b5050565b6000600f5460ff1660038111156103b1576103b1611751565b146103d657600f54604051639a0293fd60e01b815261009b9160ff1690600401611a7c565b60a05160e0516040805160016226579360e01b03198152885160048201526020808a01516024830152885160448301528801516064820152908701516084820152606087015160a48201526001600160a01b0391821660c482015260e4810186905291169063ffd9a86d9061010401600060405180830381600087803b15801561045f57600080fd5b505af1158015610473573d6000803e3d6000fd5b5050865160005550506020948501516001558351600655938301516007556040830151600855606090920151600955600255600355600455565b6000600f5460ff1660038111156104c6576104c6611751565b146104eb57600f5460405163ad88fc8f60e01b815261009b9160ff1690600401611a7c565b61271061ffff8216111561051f57604051624ae8fd60e41b815261ffff82166004820152612710602482015260440161009b565b6005805461ffff191661ffff92909216919091179055565b6000600f5460ff16600381111561055057610550611751565b1461057557600f546040516312ba63f960e11b815261009b9160ff1690600401611a7c565b600e5460005b818110156105d457600d6000600e838154811061059a5761059a611a66565b60009182526020808320909101546001600160a01b0316835282019290925260400181209081556001908101805460ff191690550161057b565b506105e1600e60006116ee565b5060c051610100518251111561061957815161010051604051630962235760e31b81526004810192909252602482015260440161009b565b815160005b8181101561085357806000036106955760e0516001600160a01b031684828151811061064c5761064c611a66565b6020026020010151600001516001600160a01b0316146106955760e05160405163ef77b46760e01b8152600481018390526001600160a01b03909116602482015260440161009b565b60006001600160a01b03168482815181106106b2576106b2611a66565b6020026020010151600001516001600160a01b0316036106e85760405163621caef560e11b81526004810182905260240161009b565b6000600d600086848151811061070057610700611a66565b6020026020010151600001516001600160a01b03166001600160a01b0316815260200190815260200160002090508060000154600014610756576040516308308c2360e21b81526004810183905260240161009b565b600061076c858461010051610ece60201b60201c565b9050600086848151811061078257610782611a66565b6020026020010151602001519050818110156107b75783818360405163c4aa8aa360e01b815260040161009b93929190611aa4565b808610156107de57838187604051636d3ae3a760e01b815260040161009b93929190611aa4565b6107e88187611ad0565b9550600e8785815181106107fe576107fe611a66565b6020908102919091018101515182546001808201855560009485529290932090920180546001600160a01b0319166001600160a01b0390931692909217909155908355918201805460ff19169055500161061e565b50505050565b6000600f5460ff16600381111561087257610872611751565b1415801561089757506001600f5460ff16600381111561089457610894611751565b14155b156108bc57600f54604051630295f95f60e51b815261009b9160ff1690600401611a7c565b60a0516001600160a01b03166386fa50636040518163ffffffff1660e01b8152600401602060405180830381865afa1580156108fc573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109209190611a30565b61010051146109b3576101005160a0516001600160a01b03166386fa50636040518163ffffffff1660e01b8152600401602060405180830381865afa15801561096d573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109919190611a30565b604051631248081b60e31b81526004810192909252602482015260440161009b565b60a0516001600160a01b03166356d399e86040518163ffffffff1660e01b8152600401602060405180830381865afa1580156109f3573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610a179190611a30565b60c05114610a625760c05160a0516001600160a01b03166356d399e86040518163ffffffff1660e01b8152600401602060405180830381865afa15801561096d573d6000803e3d6000fd5b6000600f5460ff166003811115610a7b57610a7b611751565b03610b145760e0516001600160a01b0316836001600160a01b031614610ab95760e0516040516316a4ebc160e21b815261009b918591600401611ae3565b600f805460ff1916600117905560e05160025460055460405161ffff90911681526001600160a01b03909216917fe52a01bb4ffcc3baf2c5d17111d7d5b676a7485809eb10671092a89d2bb22ff69060200160405180910390a35b6001600160a01b0383166000908152600d60205260409020805415801590610b415750600181015460ff16155b15610b8a578054821015610b7557805460405163454cd8e160e01b815261009b918491600401918252602082015260400190565b6001818101805460ff19169091179055610be6565b6001600160a01b0384166000908152600a6020526040902054158015610bb65750610bb3610f65565b82105b15610be65781610bc4610f65565b6040516310e45ff160e21b81526004810192909252602482015260440161009b565b6001600160a01b0384166000908152600a60205260408120549003610cb7576000610c11858561102b565b604080518082019091526001600160a01b03808816825291821660208201908152600c8054600181018255600091909152915160029092027fdf6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c7810180549385166001600160a01b031994851617905590517fdf6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c8909101805491909316911617905550610cc1565b610cc18484611049565b6001600160a01b0384166000908152600a602052604081208054849290610ce9908490611afd565b90915550506001600160a01b0384166000908152600b60205260408120429055610d116111ca565b90506000610d1d611236565b60c051909150610d2d8284611afd565b1115610d5457818160c05160405163513b428b60e11b815260040161009b93929190611aa4565b61010051600c541115610d8157610100516040516315b5c3d560e21b815260040161009b91815260200190565b60c0518203610dd35760e0516002546040516001600160a01b03909216917f48996614f3380f3dcd5a2d2e6eaddfd66207ff3457fee339e09b4735bc9ec95890600090a3600f805460ff191660021790555b856001600160a01b03167fbdaa686eb6f59012d211a74523da260341c516896e9e5be954163d6ecf26ffa285604051610e0e91815260200190565b60405180910390a2608051610e2e906001600160a01b03168730876112a8565b6002600f5460ff166003811115610e4757610e47611751565b148015610e5c5750600f54610100900460ff16155b15610e6957610e696112e1565b505050505050565b610ec983846001600160a01b031663a9059cbb8585604051602401610e97929190611b10565b60408051808303601f1901815291905260208101805160e09390931b6001600160e01b03938416179052915061154216565b505050565b6000828211610efa576040516376675ed960e11b8152600481018490526024810183905260440161009b565b82600003610f2b576004610f0f600186611ad0565b610f199190611b29565b610f24906001611afd565b9050610f5e565b6000610f378484611ad0565b905080610f45600187611ad0565b610f4f9190611b29565b610f5a906001611afd565b9150505b9392505050565b600e5460009081908190815b81811015610fe8576000600e8281548110610f8e57610f8e611a66565b60009182526020808320909101546001600160a01b0316808352600d909152604090912060018101549192509060ff16610fde578054610fce9087611afd565b9550610fdb600186611afd565b94505b5050600101610f71565b5061102383610ff56111ca565b60c0516110029190611ad0565b61100c9190611ad0565b600c5461101a908590611afd565b61010051610ece565b935050505090565b60006001600160a01b038216156110425781610f5e565b5090919050565b6001600f5460ff16600381111561106257611062611751565b1415801561108757506002600f5460ff16600381111561108457611084611751565b14155b156110ac576002546040516310004b0160e11b8152600481019190915260240161009b565b60006110b8838361102b565b600c549091506000908190815b8181101561115e576000600c82815481106110e2576110e2611a66565b6000918252602090912060029091020180549091506001600160a01b03808a169116036111555760018101546001600160a01b0380881691160361112a575050505050505050565b600190810180546001600160a01b038881166001600160a01b0319831617909255169450925061115e565b506001016110c5565b508161117f5785604051632fd768d360e11b815260040161009b9190611b4b565b856001600160a01b03167f25639ce407c98fba722dddf1f023c8242e3c77732cfe4660d1c04930bf4cbe7284866040516111ba929190611ae3565b60405180910390a2505050505050565b600c54600090815b81811015611231576000600c82815481106111ef576111ef611a66565b600091825260208083206002909202909101546001600160a01b0316808352600a9091526040909120549091506112269085611afd565b9350506001016111d2565b505090565b600e54600090815b81811015611231576000600e828154811061125b5761125b611a66565b60009182526020808320909101546001600160a01b0316808352600d909152604090912060018101549192509060ff1661129e57805461129b9086611afd565b94505b505060010161123e565b6040516001600160a01b0384811660248301528381166044830152606482018390526108539186918216906323b872dd90608401610e97565b6002600f5460ff1660038111156112fa576112fa611751565b1461131f57600f546040516383696f6f60e01b815261009b9160ff1690600401611a7c565b600f805460ff191660031790556002546040517f839cf22e1ba87ce2f5b9bbf46cf0175a09eed52febdfaac8852478e68203c76390600090a2600c546000816001600160401b038111156113755761137561177c565b6040519080825280602002602001820160405280156113c957816020015b60408051608081018252600091810182815260608201839052815260208101919091528152602001906001900390816113935790505b50905060005b82811015611464576000600c82815481106113ec576113ec611a66565b6000918252602080832060408051608081018252600290940290910180546001600160a01b03908116858401818152600184015490921660608701529085528552600a83529320549082015284519192509084908490811061145057611450611a66565b6020908102919091010152506001016113cf565b506080516001600160a01b031663095ea7b360a05160c0516040518363ffffffff1660e01b8152600401611499929190611b10565b6020604051808303816000875af11580156114b8573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906114dc9190611b5f565b5060a0516001600160a01b031663bc7efecc600060066002856040518563ffffffff1660e01b81526004016115149493929190611bd9565b600060405180830381600087803b15801561152e57600080fd5b505af1158015610e69573d6000803e3d6000fd5b60006115576001600160a01b0384168361159c565b9050805160001415801561157c57508080602001905181019061157a9190611b5f565b155b15610ec95782604051635274afe760e01b815260040161009b9190611b4b565b6060610f5e838360006115b0565b92915050565b6060814710156115d5573060405163cd78605960e01b815260040161009b9190611b4b565b600080856001600160a01b031684866040516115f19190611c50565b60006040518083038185875af1925050503d806000811461162e576040519150601f19603f3d011682016040523d82523d6000602084013e611633565b606091505b50909250905061164486838361164e565b9695505050505050565b6060826116635761165e826116a1565b610f5e565b815115801561167a57506001600160a01b0384163b155b1561169a5783604051639996b31560e01b815260040161009b9190611b4b565b5080610f5e565b8051156116b15780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b50565b50805460008255600202906000526020600020908101906116ca919061170c565b50805460008255906000526020600020908101906116ca919061173c565b5b808211156117385780546001600160a01b03199081168255600182018054909116905560020161170d565b5090565b5b80821115611738576000815560010161173d565b634e487b7160e01b600052602160045260246000fd5b6001600160a01b03811681146116ca57600080fd5b634e487b7160e01b600052604160045260246000fd5b604051608081016001600160401b03811182821017156117b4576117b461177c565b60405290565b604080519081016001600160401b03811182821017156117b4576117b461177c565b604051601f8201601f191681016001600160401b03811182821017156118045761180461177c565b604052919050565b60006080828403121561181e57600080fd5b611826611792565b8251815260208084015190820152604080840151908201526060928301519281019290925250919050565b60006080828403121561186357600080fd5b61186b611792565b825181526020808401519082015260408084015190820152606083015190915061ffff8116811461189b57600080fd5b606082015292915050565b600082601f8301126118b757600080fd5b81516001600160401b038111156118d0576118d061177c565b6118df60208260051b016117dc565b8082825260208201915060208360061b86010192508583111561190157600080fd5b602085015b8381101561194d576040818803121561191e57600080fd5b6119266117ba565b815161193181611767565b8152602082810151818301529084529290920191604001611906565b5095945050505050565b8051801515811461196757600080fd5b919050565b60008060008060008060008789036101c081121561198957600080fd5b885161199481611767565b60208a015190985096506040603f19820112156119b057600080fd5b506119b96117ba565b604089015181526060890151602082015294506119d98960808a0161180c565b93506119e9896101008a01611851565b6101808901519093506001600160401b03811115611a0657600080fd5b611a128a828b016118a6565b925050611a226101a08901611957565b905092959891949750929550565b600060208284031215611a4257600080fd5b5051919050565b600060208284031215611a5b57600080fd5b8151610f5e81611767565b634e487b7160e01b600052603260045260246000fd5b6020810160048310611a9e57634e487b7160e01b600052602160045260246000fd5b91905290565b9283526020830191909152604082015260600190565b634e487b7160e01b600052601160045260246000fd5b818103818111156115aa576115aa611aba565b6001600160a01b0392831681529116602082015260400190565b808201808211156115aa576115aa611aba565b6001600160a01b03929092168252602082015260400190565b600082611b4657634e487b7160e01b600052601260045260246000fd5b500490565b6001600160a01b0391909116815260200190565b600060208284031215611b7157600080fd5b610f5e82611957565b600081518084526020840193506020830160005b82811015611bcf578151805180516001600160a01b039081168952602091820151168189015290810151604088015260609096019590910190600101611b8e565b5093949350505050565b8454815260018501546020820152835460408201526001840154606082015260028401546080820152600384015460a0820152825460c0820152600183015460e0820152600283015461010082015261ffff6003840154166101208201526101606101408201526000611644610160830184611b7a565b6000825160005b81811015611c715760208186018101518583015201611c57565b506000920191825250919050565b60805160a05160c05160e051610100516135f2611e4b6000396000818161050501528181610f36015281816115b4015281816115dd0152818161179701528181611e3001528181611e56015281816123b501526123e90152600081816104660152818161064b01528181610673015281816106d601528181610a2801528181610a5001528181610afe01528181610b2601528181610bcb01528181610bf301528181610c5b01528181610c9301528181610cbb01528181610d0d01528181610d350152818161104b01528181611073015281816111b5015281816112140152818161123c01528181611469015281816114910152818161162e015281816116a00152818161205901528181612093015281816120f50152818161244c015281816129480152612a7d01526000818161043f01528181610ef80152818161159201528181611f9601528181611fbc01528181612342015281816123750152818161241701526127b601526000818161034a01528181611dae01528181611e7701528181611f1401528181611fdd015281816127940152818161283901526129170152600081816105a601528181611ab201528181611cef015281816124f2015261276701526135f26000f3fe608060405234801561001057600080fd5b50600436106101e25760003560e01c8062e9a407146101e75780630aaffd2a146101fc5780630d616d201461020f5780630d9639ba146102175780630dcf4b8f146102375780630ebb172a1461024d57806312fa329b1461026f578063200d2ed2146102905780632816ee73146102aa5780632c6cda93146102bd5780632c7baaf3146102d0578063313f336b1461030f5780633362379414610345578063356c299d1461037957806342e94c90146103b75780634564168b146103d75780634a387cdf146103f35780634bb278f3146103fb5780634e7320ce14610403578063565a9d481461041857806356d399e81461043a578063570ca7351461046157806358b810d3146104885780636786452c1461049b5780636cf72aa4146104ae5780637c0b4bfb146104c157806386fa5063146105005780638c7e7a31146105275780638dd085571461053a578063937e09b11461055a57806396a608c0146105625780639ca002a61461056a578063a4b6aa831461057d578063bc063e1a14610585578063c3aca558146105a1578063ccec3716146105c8578063d26146a5146105db578063d826f88f146105ee578063dda0a81a146105f6578063e020e4c41461060d578063e50d50d814610620575b600080fd5b6101fa6101f5366004612e8f565b610640565b005b6101fa61020a366004612ecb565b6106c2565b6101fa6106cc565b61021f6107b7565b60405161022e93929190612f5d565b60405180910390f35b61023f61096d565b60405190815260200161022e565b6102576201518081565b6040516001600160401b03909116815260200161022e565b61028261027d366004612fca565b6109d9565b60405161022e929190612fe3565b600f5461029d9060ff1681565b60405161022e9190613013565b6101fa6102b836600461303b565b610a12565b6101fa6102cb366004613079565b610a1d565b6102d8610a93565b60405161022e919081518152602080830151908201526040808301519082015260609182015161ffff169181019190915260800190565b6006546007546008546009546103259392919084565b60408051948552602085019390935291830152606082015260800161022e565b61036c7f000000000000000000000000000000000000000000000000000000000000000081565b60405161022e9190613094565b6002546003546004546005546103939392919061ffff1684565b6040805194855260208501939093529183015261ffff16606082015260800161022e565b61023f6103c5366004612ecb565b600a6020526000908152604090205481565b6000546001546103e5919082565b60405161022e9291906130a8565b600c5461023f565b6101fa610af3565b61040b610b68565b60405161022e91906130dc565b600f5461042a90610100900460ff1681565b604051901515815260200161022e565b61023f7f000000000000000000000000000000000000000000000000000000000000000081565b61036c7f000000000000000000000000000000000000000000000000000000000000000081565b6101fa610496366004613103565b610bc0565b6101fa6104a93660046131f1565b610c88565b6101fa6104bc366004613242565b610d02565b6104eb6104cf366004612ecb565b600d602052600090815260409020805460019091015460ff1682565b6040805192835290151560208301520161022e565b61023f7f000000000000000000000000000000000000000000000000000000000000000081565b61023f61053536600461325f565b610d78565b61054d610548366004612fca565b610e09565b60405161022e91906132a6565b61023f610e62565b61023f610f62565b61023f610578366004612fca565b610fb9565b61023f610fce565b61058e61271081565b60405161ffff909116815260200161022e565b61036c7f000000000000000000000000000000000000000000000000000000000000000081565b6101fa6105d6366004612ecb565b611040565b61036c6105e9366004612fca565b6111df565b6101fa611209565b6105fe61127e565b60405161022e939291906132b4565b6101fa61061b3660046132ed565b61145e565b61023f61062e366004612ecb565b600b6020526000908152604090205481565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146106b657337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b60405180910390fd5b6106bf816114e3565b50565b6106bf33826118a8565b6001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016330361070657610704611a29565b565b336000908152600b602052604081205461072090426133e4565b90506201518081101561076a57336000908152600b602052604090819020549051630429b06960e31b815260048101919091524260248201526201518060448201526064016106ad565b600061077533611b4c565b905080156107b35760405181815233907f249a82fbe5056a1940b4e996665ba2a82c340ae9fa1e069fd1ababf5508f396e9060200160405180910390a25b5050565b600e5460609081908190806001600160401b038111156107d9576107d9612d34565b604051908082528060200260200182016040528015610802578160200160208202803683370190505b509350806001600160401b0381111561081d5761081d612d34565b604051908082528060200260200182016040528015610846578160200160208202803683370190505b509250806001600160401b0381111561086157610861612d34565b60405190808252806020026020018201604052801561088a578160200160208202803683370190505b50915060005b81811015610966576000600e82815481106108ad576108ad6133f7565b60009182526020808320909101546001600160a01b0316808352600d909152604090912087519192509082908890859081106108eb576108eb6133f7565b60200260200101906001600160a01b031690816001600160a01b0316815250508060000154868481518110610922576109226133f7565b60209081029190910101526001810154855160ff9091169086908590811061094c5761094c6133f7565b911515602092830291909101909101525050600101610890565b5050909192565b600c54600090815b818110156109d4576000600c8281548110610992576109926133f7565b600091825260208083206002909202909101546001600160a01b0316808352600a9091526040909120549091506109c9908561340d565b935050600101610975565b505090565b600c81815481106109e957600080fd5b6000918252602090912060029091020180546001909101546001600160a01b0391821692501682565b6107b3338284611d49565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610a8a57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6106bf8161255d565b610ac26040518060800160405280600081526020016000815260200160008152602001600061ffff1681525090565b5060408051608081018252600254815260035460208201526004549181019190915260055461ffff16606082015290565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610b6057337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6107046125e7565b610b936040518060800160405280600081526020016000815260200160008152602001600081525090565b50604080516080810182526006548152600754602082015260085491810191909152600954606082015290565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610c2d57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b610c35611a29565b610c3e8561255d565b610c47846114e3565b610c50836128a5565b8015610c8157610c817f00000000000000000000000000000000000000000000000000000000000000008383611d49565b5050505050565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610cf557337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b610c8185858585856128bf565b336001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610d6f57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6106bf816128a5565b6000828211610d9e5782826040516376675ed960e11b81526004016106ad9291906130a8565b82600003610dcf576004610db36001866133e4565b610dbd9190613420565b610dc890600161340d565b9050610e02565b6000610ddb84846133e4565b905080610de96001876133e4565b610df39190613420565b610dfe90600161340d565b9150505b9392505050565b610e11612c79565b600c8281548110610e2457610e246133f7565b60009182526020918290206040805180820190915260029092020180546001600160a01b039081168352600190910154169181019190915292915050565b600e5460009081908190815b81811015610ee5576000600e8281548110610e8b57610e8b6133f7565b60009182526020808320909101546001600160a01b0316808352600d909152604090912060018101549192509060ff16610edb578054610ecb908761340d565b9550610ed860018661340d565b94505b5050600101610e6e565b50610f5a83610ef261096d565b610f1c907f00000000000000000000000000000000000000000000000000000000000000006133e4565b610f2691906133e4565b600c54610f3490859061340d565b7f0000000000000000000000000000000000000000000000000000000000000000610d78565b935050505090565b600c54600090610f725750600090565b600a6000600c600081548110610f8a57610f8a6133f7565b600091825260208083206002909202909101546001600160a01b03168352820192909252604001902054905090565b6000610fc88260006001610d78565b92915050565b600e54600090815b818110156109d4576000600e8281548110610ff357610ff36133f7565b60009182526020808320909101546001600160a01b0316808352600d909152604090912060018101549192509060ff16611036578054611033908661340d565b94505b5050600101610fd6565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146110ad57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6003600f5460ff1660038111156110c6576110c6612ffd565b141580156110ea57506000600f5460ff1660038111156110e8576110e8612ffd565b145b1561110f57600f54604051632f3e69dd60e01b81526106ad9160ff1690600401613013565b6040516370a0823160e01b815281906000906001600160a01b038316906370a0823190611140903090600401613094565b602060405180830381865afa15801561115d573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906111819190613442565b9050600081116111a6578260405163e932c57360e01b81526004016106ad9190613094565b6111da6001600160a01b0383167f0000000000000000000000000000000000000000000000000000000000000000836129da565b505050565b600e81815481106111ef57600080fd5b6000918252602090912001546001600160a01b0316905081565b336001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000161461127657337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b610704611a29565b600c5460609081908190806001600160401b038111156112a0576112a0612d34565b6040519080825280602002602001820160405280156112c9578160200160208202803683370190505b509350806001600160401b038111156112e4576112e4612d34565b60405190808252806020026020018201604052801561130d578160200160208202803683370190505b509250806001600160401b0381111561132857611328612d34565b604051908082528060200260200182016040528015611351578160200160208202803683370190505b50915060005b81811015610966576000600c8281548110611374576113746133f7565b60009182526020909120600290910201805487519192506001600160a01b0316908790849081106113a7576113a76133f7565b6001600160a01b039283166020918202929092010152600182015486519116908690849081106113d9576113d96133f7565b60200260200101906001600160a01b031690816001600160a01b031681525050600a600087848151811061140f5761140f6133f7565b60200260200101516001600160a01b03166001600160a01b031681526020019081526020016000205484838151811061144a5761144a6133f7565b602090810291909101015250600101611357565b336001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146114cb57337f00000000000000000000000000000000000000000000000000000000000000006040516321afccb160e01b81526004016106ad929190612fe3565b6114da87878787878787612a32565b50505050505050565b6000600f5460ff1660038111156114fc576114fc612ffd565b1461152157600f546040516312ba63f960e11b81526106ad9160ff1690600401613013565b600e5460005b8181101561158057600d6000600e8381548110611546576115466133f7565b60009182526020808320909101546001600160a01b0316835282019290925260400181209081556001908101805460ff1916905501611527565b5061158d600e6000612c90565b5080517f0000000000000000000000000000000000000000000000000000000000000000907f000000000000000000000000000000000000000000000000000000000000000010156116175781517f0000000000000000000000000000000000000000000000000000000000000000604051630962235760e31b81526004016106ad9291906130a8565b815160005b818110156118a257806000036116cd577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316848281518110611668576116686133f7565b6020026020010151600001516001600160a01b0316146116cd5760405163ef77b46760e01b8152600481018290526001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001660248201526044016106ad565b60006001600160a01b03168482815181106116ea576116ea6133f7565b6020026020010151600001516001600160a01b0316036117205760405163621caef560e11b8152600481018290526024016106ad565b6000600d6000868481518110611738576117386133f7565b6020026020010151600001516001600160a01b03166001600160a01b031681526020019081526020016000209050806000015460001461178e576040516308308c2360e21b8152600481018390526024016106ad565b60006117bb85847f0000000000000000000000000000000000000000000000000000000000000000610d78565b905060008684815181106117d1576117d16133f7565b6020026020010151602001519050818110156118065783818360405163c4aa8aa360e01b81526004016106ad9392919061345b565b8086101561182d57838187604051636d3ae3a760e01b81526004016106ad9392919061345b565b61183781876133e4565b9550600e87858151811061184d5761184d6133f7565b6020908102919091018101515182546001808201855560009485529290932090920180546001600160a01b0319166001600160a01b0390931692909217909155908355918201805460ff19169055500161161c565b50505050565b6001600f5460ff1660038111156118c1576118c1612ffd565b141580156118e657506002600f5460ff1660038111156118e3576118e3612ffd565b14155b1561190b576002546040516310004b0160e11b815260048101919091526024016106ad565b60006119178383612aa3565b600c549091506000908190815b818110156119bd576000600c8281548110611941576119416133f7565b6000918252602090912060029091020180549091506001600160a01b03808a169116036119b45760018101546001600160a01b03808816911603611989575050505050505050565b600190810180546001600160a01b038881166001600160a01b031983161790925516945092506119bd565b50600101611924565b50816119de5785604051632fd768d360e11b81526004016106ad9190613094565b856001600160a01b03167f25639ce407c98fba722dddf1f023c8242e3c77732cfe4660d1c04930bf4cbe728486604051611a19929190612fe3565b60405180910390a2505050505050565b600c5460005b81811015611aec576000600c8281548110611a4c57611a4c6133f7565b600091825260208083206002909202909101546001600160a01b0316808352600a9091526040909120549091506003600f5460ff166003811115611a9257611a92612ffd565b14158015611aa05750600081115b15611ad957611ad96001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001683836129da565b611ae282612ac1565b5050600101611a2f565b50611af9600c6000612cae565b600f805460ff191690556040805160008082526020820190925281611b40565b6040805180820190915260008082526020820152815260200190600190039081611b195790505b5090506107b3816114e3565b6001600160a01b0381166000908152600a602052604081205490819003611b7257919050565b611b7b82612ac1565b600c5460005b81811015611c9657600c8181548110611b9c57611b9c6133f7565b60009182526020909120600290910201546001600160a01b0390811690851603611c8e57600c611bcd6001846133e4565b81548110611bdd57611bdd6133f7565b9060005260206000209060020201600c8281548110611bfe57611bfe6133f7565b60009182526020909120825460029092020180546001600160a01b039283166001600160a01b0319918216178255600193840154939091018054939092169216919091179055600c805480611c5557611c55613471565b60008281526020902060026000199092019182020180546001600160a01b03199081168255600191909101805490911690559055611c96565b600101611b81565b506001600160a01b0383166000908152600d6020526040902060018101805460ff191690556003600f5460ff166003811115611cd457611cd4612ffd565b03611ce25760009250611d42565b611d166001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001685856129da565b6002600f5460ff166003811115611d2f57611d2f612ffd565b03611d4257600f805460ff191660011790555b5050919050565b6000600f5460ff166003811115611d6257611d62612ffd565b14158015611d8757506001600f5460ff166003811115611d8457611d84612ffd565b14155b15611dac57600f54604051630295f95f60e51b81526106ad9160ff1690600401613013565b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166386fa50636040518163ffffffff1660e01b8152600401602060405180830381865afa158015611e0a573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611e2e9190613442565b7f000000000000000000000000000000000000000000000000000000000000000014611f12577f00000000000000000000000000000000000000000000000000000000000000007f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166386fa50636040518163ffffffff1660e01b8152600401602060405180830381865afa158015611ed3573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611ef79190613442565b604051631248081b60e31b81526004016106ad9291906130a8565b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166356d399e86040518163ffffffff1660e01b8152600401602060405180830381865afa158015611f70573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611f949190613442565b7f000000000000000000000000000000000000000000000000000000000000000014612039577f00000000000000000000000000000000000000000000000000000000000000007f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166356d399e86040518163ffffffff1660e01b8152600401602060405180830381865afa158015611ed3573d6000803e3d6000fd5b6000600f5460ff16600381111561205257612052612ffd565b03612145577f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316836001600160a01b0316146120cd57827f00000000000000000000000000000000000000000000000000000000000000006040516316a4ebc160e21b81526004016106ad929190612fe3565b600f805460ff1916600117905560025460055460405161ffff90911681526001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001691907fe52a01bb4ffcc3baf2c5d17111d7d5b676a7485809eb10671092a89d2bb22ff69060200160405180910390a35b6001600160a01b0383166000908152600d602052604090208054158015906121725750600181015460ff16155b156121b257805482101561219d57805460405163454cd8e160e01b81526106ad9184916004016130a8565b6001818101805460ff19169091179055612207565b6001600160a01b0384166000908152600a60205260409020541580156121de57506121db610e62565b82105b1561220757816121ec610e62565b6040516310e45ff160e21b81526004016106ad9291906130a8565b6001600160a01b0384166000908152600a602052604081205490036122d85760006122328585612aa3565b604080518082019091526001600160a01b03808816825291821660208201908152600c8054600181018255600091909152915160029092027fdf6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c7810180549385166001600160a01b031994851617905590517fdf6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c89091018054919093169116179055506122e2565b6122e284846118a8565b6001600160a01b0384166000908152600a60205260408120805484929061230a90849061340d565b90915550506001600160a01b0384166000908152600b6020526040812042905561233261096d565b9050600061233e610fce565b90507f000000000000000000000000000000000000000000000000000000000000000061236b828461340d565b11156123b05781817f000000000000000000000000000000000000000000000000000000000000000060405163513b428b60e11b81526004016106ad9392919061345b565b600c547f00000000000000000000000000000000000000000000000000000000000000001015612415576040516315b5c3d560e21b81527f000000000000000000000000000000000000000000000000000000000000000060048201526024016106ad565b7f000000000000000000000000000000000000000000000000000000000000000082036124a2576002546040516001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001691907f48996614f3380f3dcd5a2d2e6eaddfd66207ff3457fee339e09b4735bc9ec95890600090a3600f805460ff191660021790555b856001600160a01b03167fbdaa686eb6f59012d211a74523da260341c516896e9e5be954163d6ecf26ffa2856040516124dd91815260200190565b60405180910390a261251a6001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016873087612ae8565b6002600f5460ff16600381111561253357612533612ffd565b1480156125485750600f54610100900460ff16155b15612555576125556125e7565b505050505050565b6000600f5460ff16600381111561257657612576612ffd565b1461259b57600f5460405163ad88fc8f60e01b81526106ad9160ff1690600401613013565b61271061ffff821611156125cf57604051624ae8fd60e41b815261ffff8216600482015261271060248201526044016106ad565b6005805461ffff191661ffff92909216919091179055565b6002600f5460ff16600381111561260057612600612ffd565b1461262557600f546040516383696f6f60e01b81526106ad9160ff1690600401613013565b600f805460ff191660031790556002546040517f839cf22e1ba87ce2f5b9bbf46cf0175a09eed52febdfaac8852478e68203c76390600090a2600c546000816001600160401b0381111561267b5761267b612d34565b6040519080825280602002602001820160405280156126b457816020015b6126a1612ccf565b8152602001906001900390816126995790505b50905060005b8281101561274f576000600c82815481106126d7576126d76133f7565b6000918252602080832060408051608081018252600290940290910180546001600160a01b03908116858401818152600184015490921660608701529085528552600a83529320549082015284519192509084908490811061273b5761273b6133f7565b6020908102919091010152506001016126ba565b5060405163095ea7b360e01b81526001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169063095ea7b3906127de907f0000000000000000000000000000000000000000000000000000000000000000907f000000000000000000000000000000000000000000000000000000000000000090600401613487565b6020604051808303816000875af11580156127fd573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061282191906134a0565b50604051632f1fbfb360e21b81526001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169063bc7efecc90612877906000906006906002908790600401613504565b600060405180830381600087803b15801561289157600080fd5b505af1158015612555573d6000803e3d6000fd5b600f80549115156101000261ff0019909216919091179055565b6000600f5460ff1660038111156128d8576128d8612ffd565b146128fd57600f54604051639a0293fd60e01b81526106ad9160ff1690600401613013565b60405160016226579360e01b031981526001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169063ffd9a86d9061297290889088907f000000000000000000000000000000000000000000000000000000000000000090899060040161357b565b600060405180830381600087803b15801561298c57600080fd5b505af11580156129a0573d6000803e3d6000fd5b5050865160005550506020948501516001558351600655938301516007556040830151600855606090920151600955600255600355600455565b6111da83846001600160a01b031663a9059cbb8585604051602401612a00929190613487565b604051602081830303815290604052915060e01b6020820180516001600160e01b038381831617835250505050612b21565b612a3a611a29565b612a5387878760000151886020015189604001516128bf565b612a60856060015161255d565b612a69846114e3565b612a72836128a5565b80156114da576114da7f00000000000000000000000000000000000000000000000000000000000000008383611d49565b60006001600160a01b03821615612aba5781610e02565b5090919050565b6001600160a01b03166000908152600a60209081526040808320839055600b909152812055565b6040516001600160a01b0384811660248301528381166044830152606482018390526118a29186918216906323b872dd90608401612a00565b6000612b366001600160a01b03841683612b7b565b90508051600014158015612b5b575080806020019051810190612b5991906134a0565b155b156111da5782604051635274afe760e01b81526004016106ad9190613094565b6060610e028383600084600080856001600160a01b03168486604051612ba191906135b6565b60006040518083038185875af1925050503d8060008114612bde576040519150601f19603f3d011682016040523d82523d6000602084013e612be3565b606091505b5091509150612bf3868383612bfd565b9695505050505050565b606082612c1257612c0d82612c50565b610e02565b8151158015612c2957506001600160a01b0384163b155b15612c495783604051639996b31560e01b81526004016106ad9190613094565b5080610e02565b805115612c605780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b604080518082019091526000808252602082015290565b50805460008255906000526020600020908101906106bf9190612cef565b50805460008255600202906000526020600020908101906106bf9190612d08565b6040518060400160405280612ce2612c79565b8152602001600081525090565b5b80821115612d045760008155600101612cf0565b5090565b5b80821115612d045780546001600160a01b031990811682556001820180549091169055600201612d09565b634e487b7160e01b600052604160045260246000fd5b604080519081016001600160401b0381118282101715612d6c57612d6c612d34565b60405290565b604051608081016001600160401b0381118282101715612d6c57612d6c612d34565b604051601f8201601f191681016001600160401b0381118282101715612dbc57612dbc612d34565b604052919050565b80356001600160a01b0381168114612ddb57600080fd5b919050565b600082601f830112612df157600080fd5b81356001600160401b03811115612e0a57612e0a612d34565b612e1960208260051b01612d94565b8082825260208201915060208360061b860101925085831115612e3b57600080fd5b602085015b83811015612e855760408188031215612e5857600080fd5b612e60612d4a565b612e6982612dc4565b8152602082810135818301529084529290920191604001612e40565b5095945050505050565b600060208284031215612ea157600080fd5b81356001600160401b03811115612eb757600080fd5b612ec384828501612de0565b949350505050565b600060208284031215612edd57600080fd5b610e0282612dc4565b600081518084526020840193506020830160005b82811015612f215781516001600160a01b0316865260209586019590910190600101612efa565b5093949350505050565b600081518084526020840193506020830160005b82811015612f21578151865260209586019590910190600101612f3f565b606081526000612f706060830186612ee6565b8281036020840152612f828186612f2b565b83810360408501528451808252602080870193509091019060005b81811015612fbd5783511515835260209384019390920191600101612f9d565b5090979650505050505050565b600060208284031215612fdc57600080fd5b5035919050565b6001600160a01b0392831681529116602082015260400190565b634e487b7160e01b600052602160045260246000fd5b602081016004831061303557634e487b7160e01b600052602160045260246000fd5b91905290565b6000806040838503121561304e57600080fd5b8235915061305e60208401612dc4565b90509250929050565b803561ffff81168114612ddb57600080fd5b60006020828403121561308b57600080fd5b610e0282613067565b6001600160a01b0391909116815260200190565b918252602082015260400190565b805182526020810151602083015260408101516040830152606081015160608301525050565b60808101610fc882846130b6565b80151581146106bf57600080fd5b8035612ddb816130ea565b600080600080600060a0868803121561311b57600080fd5b61312486613067565b945060208601356001600160401b0381111561313f57600080fd5b61314b88828901612de0565b945050604086013561315c816130ea565b925061316a60608701612dc4565b949793965091946080013592915050565b60006040828403121561318d57600080fd5b613195612d4a565b823581526020928301359281019290925250919050565b6000608082840312156131be57600080fd5b6131c6612d72565b8235815260208084013590820152604080840135908201526060928301359281019290925250919050565b6000806000806000610120868803121561320a57600080fd5b613214878761317b565b945061322387604088016131ac565b949794965050505060c08301359260e081013592610100909101359150565b60006020828403121561325457600080fd5b8135610e02816130ea565b60008060006060848603121561327457600080fd5b505081359360208301359350604090920135919050565b80516001600160a01b03908116835260209182015116910152565b60408101610fc8828461328b565b6060815260006132c76060830186612ee6565b82810360208401526132d98186612ee6565b90508281036040840152612bf38185612f2b565b60008060008060008060008789036101c081121561330a57600080fd5b6133148a8a61317b565b97506133238a60408b016131ac565b9650608060bf198201121561333757600080fd5b50613340612d72565b60c0890135815260e0890135602082015261010089013560408201526133696101208a01613067565b606082015294506101408801356001600160401b0381111561338a57600080fd5b6133968a828b01612de0565b9450506133a661016089016130f8565b92506133b56101808901612dc4565b9699959850939692959194919350506101a09091013590565b634e487b7160e01b600052601160045260246000fd5b81810381811115610fc857610fc86133ce565b634e487b7160e01b600052603260045260246000fd5b80820180821115610fc857610fc86133ce565b60008261343d57634e487b7160e01b600052601260045260246000fd5b500490565b60006020828403121561345457600080fd5b5051919050565b9283526020830191909152604082015260600190565b634e487b7160e01b600052603160045260246000fd5b6001600160a01b03929092168252602082015260400190565b6000602082840312156134b257600080fd5b8151610e02816130ea565b600081518084526020840193506020830160005b82811015612f215781516134e687825161328b565b602090810151604088015260609096019591909101906001016134d1565b8454815260018501546020820152835460408201526001840154606082015260028401546080820152600384015460a0820152825460c0820152600183015460e0820152600283015461010082015261ffff6003840154166101208201526101606101408201526000612bf36101608301846134bd565b8451815260208086015190820152610100810161359b60408301866130b6565b6001600160a01b039390931660c082015260e0015292915050565b6000825160005b818110156135d757602081860181015185830152016135bd565b50600092019182525091905056fea164736f6c634300081a000aa164736f6c634300081a000a", "linkReferences": {}, "deployedLinkReferences": {} } diff --git a/src/web3client/abis/ServiceNodeRewards.json b/src/web3client/abis/ServiceNodeRewards.json index 91e3e5f..f21f1ef 100644 --- a/src/web3client/abis/ServiceNodeRewards.json +++ b/src/web3client/abis/ServiceNodeRewards.json @@ -36,6 +36,29 @@ "name": "BLSPubkeyAlreadyExists", "type": "error" }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "X", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "Y", + "type": "uint256" + } + ], + "internalType": "struct BN256G1.G1Point", + "name": "pubkey", + "type": "tuple" + } + ], + "name": "BLSPubkeyDoesNotExist", + "type": "error" + }, { "inputs": [ { @@ -132,6 +155,27 @@ "name": "EnforcedPause", "type": "error" }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "serviceNodeID", + "type": "uint64" + }, + { + "internalType": "uint256", + "name": "addedTimestamp", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "currenttime", + "type": "uint256" + } + ], + "name": "ExitTooEarly", + "type": "error" + }, { "inputs": [], "name": "ExpectedPause", @@ -249,30 +293,9 @@ "name": "LeaveRequestTooEarly", "type": "error" }, - { - "inputs": [ - { - "internalType": "uint64", - "name": "serviceNodeID", - "type": "uint64" - }, - { - "internalType": "uint256", - "name": "addedTimestamp", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "currenttime", - "type": "uint256" - } - ], - "name": "LiquidationTooEarly", - "type": "error" - }, { "inputs": [], - "name": "LiquidatorRewardsTooLow", + "name": "LiquidatorPenaltyTooHigh", "type": "error" }, { @@ -480,11 +503,23 @@ { "indexed": false, "internalType": "uint256", - "name": "newValue", + "name": "liquidatorRatio", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "poolRatio", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "recipientRatio", "type": "uint256" } ], - "name": "LiquidatorRewardRatioUpdated", + "name": "LiquidationRatiosUpdated", "type": "event" }, { @@ -670,32 +705,6 @@ "name": "Paused", "type": "event" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "newValue", - "type": "uint256" - } - ], - "name": "PoolShareOfLiquidationRatioUpdated", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "newValue", - "type": "uint256" - } - ], - "name": "RecipientRatioUpdated", - "type": "event" - }, { "anonymous": false, "inputs": [ @@ -935,19 +944,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [], - "name": "MINIMUM_LIQUIDATION_AGE", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "MIN_TIME_BEFORE_REPEATED_LEAVE_REQUEST", @@ -1785,6 +1781,11 @@ "name": "ed25519Pubkey", "type": "uint256" }, + { + "internalType": "uint256", + "name": "addedTimestamp", + "type": "uint256" + }, { "components": [ { @@ -1994,37 +1995,21 @@ "inputs": [ { "internalType": "uint256", - "name": "newValue", + "name": "liquidator", "type": "uint256" - } - ], - "name": "setLiquidatorRewardRatio", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ + }, { "internalType": "uint256", - "name": "newValue", + "name": "pool", "type": "uint256" - } - ], - "name": "setPoolShareOfLiquidationRatio", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ + }, { "internalType": "uint256", - "name": "newValue", + "name": "recipient", "type": "uint256" } ], - "name": "setRecipientRatio", + "name": "setLiquidationRatios", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -2234,8 +2219,8 @@ "type": "function" } ], - "bytecode": "0x6080604052348015600f57600080fd5b506158fd8061001f6000396000f3fe608060405234801561001057600080fd5b50600436106103015760003560e01c8063040f985314610306578063095f14041461032f5780630962ef79146103465780630bd1b4191461035b5780630caca63b1461036457806317f33a7d146103775780631f744b541461039e578063245fc183146103a657806334fde376146103b9578063372500ab146103c15780633f4ba83a146103c957806343039d90146103d1578063538d2709146103e4578063544736e6146103ed57806356d399e81461040a57806358e93308146104135780635c975abb1461041c578063623b94221461042457806362f1fbc21461042d57806365ca819d146104375780636b348ab41461046b578063715018a61461047457806379ba50971461047c5780637a6d4065146104845780637c89d2f0146104975780638328b610146104bc5780638456cb59146104cf57806384cd9b57146104d757806386e76685146104ea57806386fa5063146104f3578063879d0f0c146104fc5780638a2209e6146105045780638a399481146105195780638da5cb5b146105225780639156ac801461052a57806395055ffd146105335780639592d424146105465780639c80ebee1461054f5780639cebc4741461042d5780639e3b372d14610557578063a44285a514610560578063ab0122ae14610569578063abf2c5031461057c578063ae6c606314610592578063b13aaebd1461059b578063b687f85c146105a4578063bc7efecc146105ac578063be9a6555146105bf578063c0ebedab146105c7578063c4f56d9a146105f0578063d5a8125414610603578063d5ca793f1461060c578063d655422f1461061f578063d84f0bf814610632578063d9054b091461063b578063d995b00c1461064e578063da1d543514610657578063dd644d7414610660578063e30c397814610669578063eb82031214610671578063edbf4ac2146106a6578063ee94e412146106b9578063f2fde38b146106c2578063f324c8eb146106d5578063f88d658b146106e8578063f907f5fc146106fb578063ffd9a86d1461070e575b600080fd5b610319610314366004614c39565b610721565b6040516103269190614cce565b60405180910390f35b610338600d5481565b604051908152602001610326565b610359610354366004614d8c565b610852565b005b610338600f5481565b610359610372366004614d8c565b61085f565b60015461039190600160a01b90046001600160401b031681565b6040516103269190614da5565b6103596108c4565b6103596103b4366004614d8c565b610994565b6103386109f2565b610359610a29565b610359610a5c565b6103596103df366004614d8c565b610a6e565b61033860075481565b6000546103fa9060ff1681565b6040519015158152602001610326565b610338600b5481565b610338600e5481565b6103fa610acc565b61033860155481565b61033862278d0081565b610391610445366004614e49565b80516020818301810180516012825292820191909301209152546001600160401b031681565b610338600a5481565b610359610ae1565b610359610af3565b610359610492366004614f9f565b610b38565b6000546104af9061010090046001600160a01b031681565b6040516103269190615009565b6103596104ca366004614d8c565b610cd6565b610359610d33565b6103596104e5366004614d8c565b610d43565b61033860045481565b610338600c5481565b610338601481565b61050c610da1565b604051610326919061501d565b61033860035481565b6104af610dc4565b61033860195481565b61035961054136600461502b565b610ddf565b61033860025481565b610391600081565b61033860095481565b61033860175481565b610359610577366004614c39565b610f8c565b610584611055565b604051610326929190615084565b61033860065481565b61033860185481565b610338600481565b6103596105ba36600461524e565b6111e0565b610359611509565b6103916105d5366004614d8c565b601b602052600090815260409020546001600160401b031681565b6103596105fe36600461502b565b611520565b61033860055481565b61035961061a3660046152fb565b611923565b61035961062d366004614d8c565b611bb6565b61033860085481565b610359610649366004614c39565b611bf3565b610338611c2081565b61033860165481565b610338601a5481565b6104af611c05565b61069861067f366004615370565b6011602052600090815260409020805460019091015482565b60405161032692919061538d565b6103596106b436600461539b565b611c10565b610338610e1081565b6103596106d0366004615370565b611f6a565b6103596106e3366004614d8c565b611fdb565b6103596106f6366004614d8c565b612039565b6001546104af906001600160a01b031681565b61035961071c3660046153fd565b612097565b610729614ab4565b6001600160401b03808316600090815260106020908152604080832081516101408101835281548087168252600160401b90049095168584015260018101546001600160a01b0316858301528151808301835260028201548152600382015481850152606086015260048101546080860152600581015460a0860152600681015460c0860152600781015460e0860152600881018054835181860281018601909452808452919461010087019491929184015b82821015610839576000848152602090819020604080516080810182526003860290920180546001600160a01b039081169284019283526001808301549091166060850152918352600201548284015290835290920191016107dc565b5050505081526020016009820154815250509050919050565b61085c33826120a9565b50565b6108676121ec565b6000811161088857604051630ef6cf4560e41b815260040160405180910390fd5b600f8190556040518181527f20d448c79534efb2c0adc259142770a7979eea09d1e5850fc1b02403337e11a8906020015b60405180910390a150565b6000600281905580526010602052600080516020615890833981519152546001600160401b03165b6001600160401b0381161561085c576001600160401b0381166000908152601060205260408120600254909103610932576002810154601355600381015460145561097a565b604080518082018252601354815260145460208083019190915282518084019093526002840154835260038401549083015261096d9161221e565b8051601355602001516014555b546002805460010190556001600160401b031690506108ec565b61099c6121ec565b600081116109bd57604051630ef6cf4560e41b815260040160405180910390fd5b60188190556040518181527fa83e79dc52a438d552d0631ce4fbfe7dec7656d10398a2849f352a6ab4f0163b906020016108b9565b60008060646002546002610a069190615463565b610a109190615490565b905060148111610a21576014610a23565b805b91505090565b336000908152601160205260408120600181015490549091610a4b83836154a4565b9050610a5733826120a9565b505050565b610a646121ec565b610a6c6122ca565b565b610a766121ec565b60008111610a9757604051630ef6cf4560e41b815260040160405180910390fd5b60048190556040518181527f0ac8ee09138dfaf5e3ebe4cb4fd42dd1a0695535a530171223fb5066f52e0e3b906020016108b9565b600080610ad7612316565b5460ff1692915050565b610ae96121ec565b610a6c600061233a565b3380610afd611c05565b6001600160a01b031614610b2f578060405163118cdaa760e01b8152600401610b269190615009565b60405180910390fd5b61085c8161233a565b610b40612361565b60005460ff16610b635760405163348b55eb60e21b815260040160405180910390fd5b8051600354811115610ba85780600254610b7d91906154a4565b600354600254610b8d91906154a4565b604051635eee4a3560e01b8152600401610b2692919061538d565b6001600160a01b038516610bcf5760405163e99d5ac560e01b815260040160405180910390fd5b6001600160a01b0385166000908152601160205260409020548411610c07576040516333938e6360e01b815260040160405180910390fd5b6007546040805160208101929092526001600160601b0319606088901b16908201526054810185905260009060740160405160208183030381529060405290506000610c5582600a54612387565b9050610c7084610c6a368890038801886154b7565b83612447565b50506001600160a01b0385166000818152601160205260409081902080549087905590519091907f95390641529563dbfb446535fa996c5ac3be00f90f5705b3abda59a4467b797f90610cc6908890859061538d565b60405180910390a2505050505050565b610cde6121ec565b60008111610cff57604051630ef6cf4560e41b815260040160405180910390fd5b600b8190556040518181527e6b7a1ea14ff2794527a64af37d55a2040e351f8b4c1adcdc9aea80d64e0429906020016108b9565b610d3b6121ec565b610a6c612580565b610d4b6121ec565b60008111610d6c576040516352513c5960e01b815260040160405180910390fd5b600d8190556040518181527f7c6ad2fef3b5f9e109dad55b811c13b9c880062367b00074d9286d7e7300b35f906020016108b9565b610da9614b0e565b50604080518082019091526013548152601454602082015290565b600080610dcf6125c7565b546001600160a01b031692915050565b610de7612361565b60005460ff16610e0a5760405163348b55eb60e21b815260040160405180910390fd5b8051600354811115610e245780600254610b7d91906154a4565b6000610e3d610e38368890038801886154d3565b6125eb565b90506000601282604051610e519190615513565b908152604051908190036020019020546001600160401b03169050610e7586612614565b15610e99578086426040516337030b8b60e01b8152600401610b269392919061552f565b6001600160401b0381166000908152601060205260409020600201548735141580610ee557506001600160401b0381166000908152601060209081526040909120600301549088013514155b15610f0757808760405163c43283b560e01b8152600401610b26929190615550565b600854604051600091610f26918a35906020808d0135918c9101615577565b60405160208183030381529060405290506000610f4582600a54612387565b9050610f5a86610c6a368a90038a018a6154b7565b50506001600160401b038116600090815260106020526040902060070154610f8390829061262c565b50505050505050565b610f94612361565b60005460ff16610fb75760405163348b55eb60e21b815260040160405180910390fd5b6001600160401b03811660009081526010602052604081206005015490819003610ff65781604051630cf4827360e31b8152600401610b269190614da5565b600061100562278d0083615592565b90508042101561102e57828142604051637a54a45360e11b8152600401610b269392919061552f565b6001600160401b038316600090815260106020526040902060070154610a5790849061262c565b6060806002546001600160401b0381111561107257611072614db9565b60405190808252806020026020018201604052801561109b578160200160208202803683370190505b5091506002546001600160401b038111156110b8576110b8614db9565b6040519080825280602002602001820160405280156110f157816020015b6110de614b0e565b8152602001906001900390816110d65790505b5060008080526010602052600080516020615890833981519152549192506001600160401b03909116905b6001600160401b038216156111da576001600160401b0380831660009081526010602052604090208551909184918791851690811061115d5761115d6155a5565b60200260200101906001600160401b031690816001600160401b031681525050806002016040518060400160405290816000820154815260200160018201548152505084836001600160401b0316815181106111bb576111bb6155a5565b6020908102919091010152546001600160401b0316915060010161111c565b50509091565b6111e8612361565b60005460ff1661120b5760405163348b55eb60e21b815260040160405180910390fd5b8051600c548111156112305760405163226c2d8360e21b815260040160405180910390fd5b80156112a4576000805b8281101561127557838181518110611254576112546155a5565b6020026020010151602001518261126b9190615592565b915060010161123a565b50600b54811461129e57600b54816040516367d22bd960e01b8152600401610b2692919061538d565b50611339565b60408051600180825281830190925290816020015b60408051608081018252600091810182815260608201839052815260208101919091528152602001906001900390816112b957505060408051608081018252339181018281526060820192909252908152600b5460208201528151919350908390600090611329576113296155a5565b6020026020010181905250815190505b60006012611346876125eb565b6040516113539190615513565b908152604051908190036020019020546001600160401b03169050801561138f578060405163459c639360e01b8152600401610b269190614da5565b6000836000815181106113a4576113a46155a5565b6020026020010151600001516000015190506113c687878388600001516126cf565b6000806113d78988600001516127b9565b600b5460078201556001810180546001600160a01b0319166001600160a01b038716179055909250905060005b8581101561148b5781600801878281518110611422576114226155a5565b60209081029190910181015182546001808201855560009485529383902082518051600390930290910180546001600160a01b03199081166001600160a01b03948516178255918501518187018054909316931692909217905591015160029091015501611404565b50611494612aed565b816001600160401b03167f533c16cb4158fe7c77021b90e9290bbefa457dd392bd8abc1eab59747834fd5b848b8a8a6040516114d394939291906155bb565b60405180910390a26114fe600060019054906101000a90046001600160a01b03163330600b54612b19565b505050505050505050565b6115116121ec565b6000805460ff19166001179055565b611528612361565b60005460ff1661154b5760405163348b55eb60e21b815260040160405180910390fd5b80516003548111156115655780600254610b7d91906154a4565b6000611579610e38368890038801886154d3565b9050600060128260405161158d9190615513565b908152604051908190036020019020546001600160401b031690506115b186612614565b156115d5578086426040516337030b8b60e01b8152600401610b269392919061552f565b6001600160401b03808216600090815260106020908152604080832081516101408101835281548087168252600160401b90049095168584015260018101546001600160a01b0316858301528151808301835260028201548152600382015481850152606086015260048101546080860152600581015460a0860152600681015460c0860152600781015460e086015260088101805483518186028101860190945280845294959491936101008601939290879084015b828210156116e9576000848152602090819020604080516080810182526003860290920180546001600160a01b0390811692840192835260018083015490911660608501529183526002015482840152908352909201910161168c565b505050908252506009919091015460209091015260608101515190915088351415806117215750806060015160200151886020013514155b1561174357818860405163c43283b560e01b8152600401610b26929190615550565b611c2081608001516117559190615592565b42101561177e57608081015160405163c666e87d60e01b8152610b26918491429060040161552f565b60095460405160009161179d918b35906020808e0135918d9101615577565b604051602081830303815290604052905060006117bc82600a54612387565b90506117d187610c6a368b90038b018b6154b7565b5050816001600160401b03167f0bfb12191b00293af29126b1c5489f8daeb4a4af82db2960b7f8353c3105cd7c82604001518360600151604051611816929190615654565b60405180910390a26000600f54600d54600e546118339190615592565b61183d9190615592565b905060008260e001519050600082600d54836118599190615463565b6118639190615490565b90506000600e54836118759190615463565b156118a457836001600e5461188a91906154a4565b6118949190615490565b61189f906001615592565b6118a7565b60005b90506118c786826118b885876154a4565b6118c291906154a4565b61262c565b600d54156118eb576000546118eb9061010090046001600160a01b03163384612b80565b600e541561191557600054600154611915916001600160a01b036101009091048116911683612b80565b505050505050505050505050565b61192b6121ec565b60005460ff161561194f57604051630a35d5c360e31b815260040160405180910390fd5b8060005b81811015611bad573684848381811061196e5761196e6155a5565b90506020028101906119809190615671565b905060006119916060830183615687565b9050116119b157604051631a10033d60e11b815260040160405180910390fd5b600c546119c16060830183615687565b905011156119e25760405163226c2d8360e21b815260040160405180910390fd5b600080611a016119f7368590038501856154d3565b84604001356127b9565b600b5460078201559092509050611a1b6060840184615687565b6000818110611a2c57611a2c6155a5565b611a429260206060909202019081019150615370565b6001820180546001600160a01b0319166001600160a01b0392909216919091179055600080611a746060860186615687565b9050905060005b81811015611b225736611a916060880188615687565b83818110611aa157611aa16155a5565b9050606002019050806040013584611ab99190615592565b93506000611aca6020830183615370565b6001600160a01b031603611af15760405163e99d5ac560e01b815260040160405180910390fd5b60088501805460018101825560009182526020909120829160030201611b1782826156ef565b505050600101611a7b565b50600b548214611b4b57600b54826040516367d22bd960e01b8152600401610b2692919061538d565b604080518635815260208088013590820152868201358183015290516001600160401b038616917f34be2fa283e1d5eb453459898f160448257aa1ef0f7dd2c3a3b55c45d1af768f919081900360600190a26001860195505050505050611953565b50610a57612aed565b611bbe6121ec565b600e8190556040518181527f4751fdab53c6c42462ab92ce837e339f8efc4c64fa76dadd99cc95baa68a4c00906020016108b9565b611bfb612361565b61085c8133612bb1565b600080610dcf612d81565b6000611c1a612da5565b805490915060ff600160401b82041615906001600160401b0316600081158015611c415750825b90506000826001600160401b03166001148015611c5d5750303b155b905081158015611c6b575080155b15611c895760405163f92ee8a960e01b815260040160405180910390fd5b84546001600160401b03191660011785558315611cb257845460ff60401b1916600160401b1785555b6001861015611cd457604051630ef6cf4560e41b815260040160405180910390fd5b6001881015611cf6576040516352513c5960e01b815260040160405180910390fd5b6000805460ff19168155600281905560035561012c60045560408051808201909152601b81527a0424c535f5349475f545259414e44494e4352454d454e545f504f5602c1b6020820152611d4990612dc9565b60065560408051808201909152601e81527f424c535f5349475f545259414e44494e4352454d454e545f52455741524400006020820152611d8990612dc9565b60075560408051808201909152601c81527b109314d7d4d251d7d51496505391125390d4915351539517d156125560221b6020820152611dc890612dc9565b600881905550611def6040518060600160405280602181526020016158b060219139612dc9565b600955604080518082019091526019815278424c535f5349475f484153485f544f5f4649454c445f54414760381b6020820152611e2b90612dc9565b600a5561025860055566038d7ea4c6800060175561a8c060185560006019819055601a8190558054610100600160a81b0319166101006001600160a01b038f811691909102919091178255600180546001600160a01b031916918e16919091178155600b8c9055600c8b9055600d8a9055600e899055600f889055611eb0919061572f565b600180546001600160401b0392909216600160a01b02600160a01b600160e01b031990921691909117905560008052601060205260008051602061589083398151915280546001600160801b0319169055611f0a33612dfd565b611f12612e0e565b831561191557845460ff60401b191685556040517fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d290611f5490600190614da5565b60405180910390a1505050505050505050505050565b611f726121ec565b6000611f7c612d81565b80546001600160a01b0319166001600160a01b0384169081178255909150611fa2610dc4565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b611fe36121ec565b6000811161200457604051630ef6cf4560e41b815260040160405180910390fd5b60178190556040518181527fe604909b918af52702fa14cd9b5a8870e5bd66da3163365671e583f705fd5463906020016108b9565b6120416121ec565b6000811161206257604051630ef6cf4560e41b815260040160405180910390fd5b60058190556040518181527ffbbc3d0a51e101ce4a7fda20e6e4eb0e230bf7de27ee2e3683fc8539e3766395906020016108b9565b6120a3848484846126cf565b50505050565b6001600160a01b03821660009081526011602052604081206001810154905490916120d483836154a4565b9050808411156120f75760405163363cc6b360e21b815260040160405180910390fd5b6000601854426121079190615490565b9050601a5481111561211e57601a81905560006019555b84601960008282546121309190615592565b9091555050601754601954111561215a57604051638c10944b60e01b815260040160405180910390fd5b6001600160a01b03861660009081526011602052604081206001018054879290612185908490615592565b90915550506040518581526001600160a01b038716907ffc30cddea38e2bf4d6ea7d3f9ed3b6ad7f176419f4963bd81318067a4aee73fe9060200160405180910390a26000546121e49061010090046001600160a01b03168787612b80565b505050505050565b336121f5610dc4565b6001600160a01b031614610a6c573360405163118cdaa760e01b8152600401610b269190615009565b612226614b0e565b61222e614b28565b835181526020808501518183015283516040808401919091529084015160608301526000908360808460066107d05a03fa9050806122c25760405162461bcd60e51b815260206004820152602b60248201527f43616c6c20746f20707265636f6d70696c656420636f6e747261637420666f7260448201526a081859190819985a5b195960aa1b6064820152608401610b26565b505092915050565b6122d2612e1e565b60006122dc612316565b805460ff1916815590507f5db9ee0a495bf2e6ff9c91a7834c1ba4fdd244a5e8aa4e537bd38aeae4b073aa335b6040516108b99190615009565b7fcd5ed15c6e187e77e9aee88184c21f4f2182ab5827cb3b7e07fbedcd63f0330090565b6000612344612d81565b80546001600160a01b0319168155905061235d82612e43565b5050565b612369610acc565b15610a6c5760405163d93c066560e01b815260040160405180910390fd5b61238f614b46565b600061239b8484612e9f565b905060008060008061240885600001516001600281106123bd576123bd6155a5565b602002015186516000602002015187602001516001600281106123e2576123e26155a5565b602002015188602001516000600281106123fe576123fe6155a5565b6020020151613070565b60408051608081018252808201948552606081019590955292845282518084019093528252602082810191909152820152955050505050505b92915050565b61244f614b0e565b835160005b818110156124d5576000868281518110612470576124706155a5565b602002602001015190506124ca8460106000846001600160401b03166001600160401b031681526020019081526020016000206002016040518060400160405290816000820154815260200160018201548152505061221e565b935050600101612454565b5060408051808201909152601354815260145460208201526124ff906124fa84613100565b61221e565b604080516080810182526020808801518284019081528851606080850191909152908352835180850185529089015181529288015183820152810191909152909250600061254c84613100565b905061256161255961318a565b8383886131ab565b610f835780604051634fb09a2160e11b8152600401610b26919061501d565b612588612361565b6000612592612316565b805460ff1916600117815590507f62e78cea01bee320cd4e420270b5ea74000d11b0c9f74754ebdbfc544b05a2586123093390565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930090565b6060816040516020016125fe919061501d565b6040516020818303038152906040529050919050565b6000600554826126249190615592565b421192915050565b6001600160401b0382166000908152601060209081526040918290206001810154835180850190945260028201548452600390910154918301919091526001600160a01b03169061267c846132ac565b612684612aed565b836001600160401b03167f24c4411329b949fad2eba6f79a8ba090a08eac1af4b56c3a2f5a282ed45e233e8385846040516126c19392919061574e565b60405180910390a250505050565b600060065485600001518660200151858560405160200161272095949392919094855260208501939093526040840191909152606090811b6001600160601b03191690830152607482015260940190565b6040516020818303038152906040529050600061273f82600a54612387565b60408051608081018252602080890151828401908152895160608085019190915290835283518085018552908a01518152928901518382015281019190915290915061279c61278c61318a565b826127968a613100565b856131ab565b610f835760405163cf006ab760e01b815260040160405180910390fd5b8151600090819015806127ce57506020840151155b156127ec5760405163139b722760e31b815260040160405180910390fd5b8260000361280d57604051634eb8f11f60e01b815260040160405180910390fd5b6000612818856125eb565b905060006001600160401b03166012826040516128359190615513565b908152604051908190036020019020546001600160401b031614612895576012816040516128639190615513565b9081526040519081900360200181205463459c639360e01b8252610b26916001600160401b0390911690600401614da5565b6000848152601b60205260409020546001600160401b0316156128e5576000848152601b602052604090819020549051630cb4c72160e21b8152610b26916001600160401b031690600401614da5565b60005460ff161561294a57436015541015612904574360155560006016555b6016805490600061291483615772565b919050555060006129236109f2565b90508060165411156129485760405163689f6e7560e11b815260040160405180910390fd5b505b60018054600160a01b90046001600160401b031690601461296a8361578b565b91906101000a8154816001600160401b0302191690836001600160401b03160217905550925060026000815461299f90615772565b909155506001600160401b038381166000818152601060209081526040808320600080516020615890833981519152805482546001600160801b031916600160401b918290049098168082029890981783558154600160401b600160801b03191690870217815586855282852080546001600160401b031990811688179091558c5160028401558c8501516003840155426004840155600983018c90558b8652601b9094529382902080549093169094179091555191945091908590601290612a69908690615513565b90815260405190819003602001902080546001600160401b03929092166001600160401b0319909216919091179055600254600103612ab45786516013556020870151601455612ae2565b6040805180820190915260135481526014546020820152612ad5908861221e565b8051601355602001516014555b5050505b9250929050565b60006003600254612afe9190615490565b90506004548111612b0f5780612b13565b6004545b60035550565b6040516001600160a01b0384811660248301528381166044830152606482018390526120a39186918216906323b872dd906084015b604051602081830303815290604052915060e01b6020820180516001600160e01b0383818316178352505050506135b5565b6040516001600160a01b03838116602483015260448201839052610a5791859182169063a9059cbb90606401612b4e565b60005460ff16612bd45760405163348b55eb60e21b815260040160405180910390fd5b6001600160401b03821660009081526010602052604081206008810154829190825b81811015612c67576000836008018281548110612c1557612c156155a5565b6000918252602090912060039091020180549091506001600160a01b03808916911603612c5e576007840154600282015460019750612c55906004615463565b10945050612c67565b50600101612bf6565b5083612c8a578585604051635bf2837760e11b8152600401610b269291906157b7565b8160050154600003612ce157828015612cb4575062278d008260040154612cb19190615592565b42105b15612cd6578585604051633826feb560e01b8152600401610b269291906157b7565b426005830155612d20565b6000610e108360060154612cf59190615592565b905080421015612d1e57868142604051637a54a45360e11b8152600401610b269392919061552f565b505b426006830155604080516001600160a01b0387168152600284015460208201526003840154918101919091526001600160401b038716907f8600c0145511b317ae0189933fa71eb57a32d74891804c7993ef74ec63a5e80490606001610cc6565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0090565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0090565b6000814630604051602001612de0939291906157d9565b604051602081830303815290604052805190602001209050919050565b612e0561360f565b61085c81613634565b612e1661360f565b610a6c613666565b612e26610acc565b610a6c57604051638dfc202b60e01b815260040160405180910390fd5b6000612e4d6125c7565b80546001600160a01b038481166001600160a01b031983168117845560405193945091169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a3505050565b612ea7614b46565b600080600080600087516001612ebd9190615592565b6001600160401b03811115612ed457612ed4614db9565b6040519080825280601f01601f191660200182016040528015612efe576020820181803683370190505b50885190915060005b81811015612f5d57898181518110612f2157612f216155a5565b602001015160f81c60f81b838281518110612f3e57612f3e6155a5565b60200101906001600160f81b031916908160001a905350600101612f07565b5060005b8060f81b8360018551612f7491906154a4565b81518110612f8457612f846155a5565b60200101906001600160f81b031916908160001a9053506000612fa7848b613683565b91995097509050600080612fbb8a8a61377b565b91509150600080612fcc84846137ee565b9150915081600014158015612fe057508015155b1561301d578415612ffb57612ff5828261395d565b90925090505b9098509650878761300e8c8c84846139a2565b1561301d575050505050613035565b5050505050808061302d9061580d565b915050612f61565b505060408051608081018252808201958652606081019690965293855250825180840190935282526020808301919091528201529392505050565b600080600080613082888888886139b9565b61308e5761308e615823565b6040805160c081018252898152602081018990529081018790526060810186905260016080820152600060a08201526130c681613a5c565b8051602082015160408301516060840151608085015160a08601519596506130ed95613c0e565b929c919b50995090975095505050505050565b613108614b0e565b815115801561311957506020820151155b15613137575050604080518082019091526000808252602082015290565b604051806040016040528083600001518152602001600080516020615870833981519152846020015161316a9190615839565b613182906000805160206158708339815191526154a4565b905292915050565b613192614b0e565b5060408051808201909152600181526002602082015290565b60408051600280825260608201909252600091829190816020015b6131ce614b0e565b8152602001906001900390816131c65750506040805160028082526060820190925291925060009190602082015b613204614b46565b8152602001906001900390816131fc579050509050868260008151811061322d5761322d6155a5565b6020026020010181905250848260018151811061324c5761324c6155a5565b6020026020010181905250858160008151811061326b5761326b6155a5565b6020026020010181905250838160018151811061328a5761328a6155a5565b602002602001018190525061329f8282613c58565b925050505b949350505050565b6000600254116132cf576040516363cd35d560e11b815260040160405180910390fd5b6001600160401b0381166132f657604051631d58f8cb60e11b815260040160405180910390fd5b6001600160401b03808216600090815260106020908152604080832081516101408101835281548087168252600160401b90049095168584015260018101546001600160a01b0316858301528151808301835260028201548152600382015481850152606086015260048101546080860152600581015460a0860152600681015460c0860152600781015460e086015260088101805483518186028101860190945280845294959491936101008601939290879084015b8282101561340a576000848152602090819020604080516080810182526003860290920180546001600160a01b039081169284019283526001808301549091166060850152918352600201548284015290835290920191016133ad565b5050509082525060099190910154602091820152818101805183516001600160401b0390811660009081526010855260408082208054600160401b600160801b031916600160401b9585169590950294909417909355855193518216815282902080546001600160401b0319169390911692909217909155805180820190915260135481526014549181019190915260608201519192506134ae916124fa90613100565b80516013556020015160145560608101516000906134cb906125eb565b6101208301516000908152601b60205260409081902080546001600160401b031916905551909150601290613501908390615513565b908152604080516020928190038301902080546001600160401b03191690556001600160401b03851660009081526010909252812080546001600160801b03191681556001810180546001600160a01b03191690556002810182905560038101829055600481018290556005810182905560068101829055600781018290559061358e6008830182614b6b565b600982016000905550506001600260008282546135ab91906154a4565b9091555050505050565b60006135ca6001600160a01b03841683613f5e565b905080516000141580156135ef5750808060200190518101906135ed919061584d565b155b15610a575782604051635274afe760e01b8152600401610b269190615009565b613617613f73565b610a6c57604051631afcd79f60e31b815260040160405180910390fd5b61363c61360f565b6001600160a01b038116610b2f576000604051631e4fbdf760e01b8152600401610b269190615009565b61366e61360f565b6000613678612316565b805460ff1916905550565b60008060008060008060006136b989896040516020016136a591815260200190565b604051602081830303815290604052613f8d565b935093509350935060405160308152602080820152602060408201528460608201528360808201526001609082015260008051602061587083398151915260b082015260208160d0836005600019fa61371157600080fd5b805197505060405160308152602080820152836050820152602060408201528260708201526001609082015260008051602061587083398151915260b082015260208160d0836005600019fa61376657600080fd5b51969996985060019081161496505050505050565b60008060008060008061379088888a8a6140d6565b90925090506137a182828a8a6140d6565b90925090506137df82826000805160206158d18339815191527e9713b03af0fed4cd2cafadeed8fdf4a74fa084e52d1852e4a2bd0685c315d2614147565b90999098509650505050505050565b600080600080600085600003613830576138078761417b565b909350905080156138215782600094509450505050612ae6565b60008394509450505050612ae6565b6000805160206158708339815191528788099250600080516020615870833981519152868709915060008051602061587083398151915282840892506138758361417b565b90935090508061388e5760008094509450505050612ae6565b60008051602061587083398151915283880891506138ab82614212565b915060006138b88361417b565b9250905081613909576138da888560008051602061587083398151915261426c565b92506138e583614212565b92506138f08361417b565b9250905081613909576000809550955050505050612ae6565b80600080516020615870833981519152828308935061393684600080516020615870833981519152614290565b93506000600080516020615870833981519152858a09919a91995090975050505050505050565b60008080613979856000805160206158708339815191526154a4565b90506000613995856000805160206158708339815191526154a4565b9196919550909350505050565b60006139b0858585856139b9565b95945050505050565b60008060008060006139cd878789896140d6565b90945092506139de898981816140d6565b90925090506139ef82828b8b6140d6565b9092509050613a00848484846142e1565b9094509250613a3e84846000805160206158d18339815191527e9713b03af0fed4cd2cafadeed8fdf4a74fa084e52d1852e4a2bd0685c315d26142e1565b909450925083158015613a4f575082155b9998505050505050505050565b613a64614b8c565b613a6c614b8c565b613a74614b8c565b613a7c614b8c565b84516020860151604087015160608801516080890151613ab4946744e992b44a6909f1949093909290918b60055b6020020151614323565b80516020820151604083015160608401516080850151949750613ae09460029493929190896005613aaa565b9150613b4c8360005b602002015184600160200201518560026020020151866003602002015187600460200201518860056020020151886000602002015189600160200201518a600260200201518b600360200201518c600460200201518d60055b60200201516143a6565b9150613b578261462f565b9150613b628361462f565b9050613b6d8161462f565b9050613b7a836000613ae9565b9250613be08360005b6020020151846001602002015185600260200201518660036020020151876004602002015188600560200201518760006020020151886001602002015189600260200201518a600360200201518b600460200201518c6005613b42565b9250613beb8561462f565b9050613bf68161462f565b9050613c018161462f565b90506139b0836000613b83565b600080600080600080613c218888614747565b9092509050613c328c8c84846140d6565b9096509450613c438a8a84846140d6565b969d959c509a50949850929650505050505050565b60008151835114613c6857600080fd5b82516000613c77826006615463565b90506000816001600160401b03811115613c9357613c93614db9565b604051908082528060200260200182016040528015613cbc578160200160208202803683370190505b50905060005b83811015613eed57868181518110613cdc57613cdc6155a5565b60200260200101516000015182826006613cf69190615463565b613d01906000615592565b81518110613d1157613d116155a5565b602002602001018181525050868181518110613d2f57613d2f6155a5565b60200260200101516020015182826006613d499190615463565b613d54906001615592565b81518110613d6457613d646155a5565b602002602001018181525050858181518110613d8257613d826155a5565b6020908102919091010151515182613d9b836006615463565b613da6906002615592565b81518110613db657613db66155a5565b602002602001018181525050858181518110613dd457613dd46155a5565b60209081029190910181015151015182613def836006615463565b613dfa906003615592565b81518110613e0a57613e0a6155a5565b602002602001018181525050858181518110613e2857613e286155a5565b602002602001015160200151600060028110613e4657613e466155a5565b602002015182613e57836006615463565b613e62906004615592565b81518110613e7257613e726155a5565b602002602001018181525050858181518110613e9057613e906155a5565b602002602001015160200151600160028110613eae57613eae6155a5565b602002015182613ebf836006615463565b613eca906005615592565b81518110613eda57613eda6155a5565b6020908102919091010152600101613cc2565b50613ef6614baa565b60006020826020860260208601600060086107d05a03f1905080613f505760405162461bcd60e51b8152602060048201526011602482015270496e76616c6964205369676e617475726560781b6044820152606401610b26565b505115159695505050505050565b6060613f6c838360006147d2565b9392505050565b6000613f7d612da5565b54600160401b900460ff16919050565b60008060008060ff85511115613fa257600080fd5b600060405160005b6088811015613fc157600082820152602001613faa565b506088602060005b8a51811015613fea578a820151848401526020928301929182019101613fc9565b505060898951019050608081830153600201602060005b89518110156140225789820151848401526020928301929182019101614001565b5050608b88518a5101019050875181830153508751875101608c018120915050604051818152600160208201536021602060005b89518110156140775789820151848401526020928301929182019101614056565b505050865187516021018201538651602201812095508582188152600260208201538651602201812094508482188152600360208201538651602201812093508382188152600460208201539551602201909520939692955090935050565b60008061411460008051602061587083398151915285880960008051602061587083398151915285880960008051602061587083398151915261426c565b60008051602061587083398151915280868809600080516020615870833981519152868a09089150915094509492505050565b6000806000805160206158708339815191528487086000805160206158708339815191528487089150915094509492505050565b600080600060405160208152602080820152602060408201528460608201527f0c19139cb84c680a6e14116da060561765e05aa45a1c72a34f082305b61f3f52608082015260008051602061587083398151915260a082015260208160c08360056107d05a03fa905193509050600080516020615870833981519152838009841491508061420c5760009250600091505b50915091565b6000600182161515614225600284615490565b915080156142665760008051602061587083398151915260026142576000805160206158708339815191526001615592565b6142619190615490565b830891505b50919050565b6000818061427c5761427c61547a565b61428684846154a4565b8508949350505050565b60008060405160208152602080820152602060408201528460608201526002840360808201528360a082015260208160c08360056107d05a03fa905192509050806142da57600080fd5b5092915050565b6000806142fd868560008051602061587083398151915261426c565b614316868560008051602061587083398151915261426c565b9150915094509492505050565b61432b614b8c565b871561439b57600188161561436c578051602082015160408301516060840151608085015160a08601516143699594939291908d8d8d8d8d8d6143a6565b90505b61437a87878787878761486f565b949b50929950909750955093509150614394600289615490565b975061432b565b979650505050505050565b6143ae614b8c565b881580156143ba575087155b156143fc578686868686868660005b60a0890192909252608088019290925260608701929092526040860192909252602085810193909352909102015261461f565b82158015614408575081155b1561441b578c8c8c8c8c8c8660006143c9565b61442785858b8b6140d6565b90955093506144388b8b85856140d6565b6060830152604082015261444e87878b8b6140d6565b909750955061445f8d8d85856140d6565b60a0830152608082018190528714801561447c575060a081015186145b156144c1576040810151851480156144975750606081015184145b156144b2576144aa8d8d8d8d8d8d61486f565b8660006143c9565b600160008181808086816143c9565b6144cd898985856140d6565b90935091506144ed858583600260200201518460035b60200201516142e1565b909d509b50614507878783600460200201518460056144e3565b909b5099506145188b8b81816140d6565b9099509750614538898983600460200201518460055b60200201516140d6565b909550935061454989898d8d6140d6565b909950975061455a898985856140d6565b60a083015260808201526145708d8d81816140d6565b9097509550614581878785856140d6565b909750955061459287878b8b6142e1565b90975095506145a3858560026149de565b90935091506145b4878785856142e1565b90975095506145c58b8b89896140d6565b602083015281526145d8858589896142e1565b909b5099506145e98d8d8d8d6140d6565b909b5099506146038989836002602002015184600361452e565b909d509b506146148b8b8f8f6142e1565b606083015260408201525b9c9b505050505050505050505050565b614637614b8c565b815161464b908360015b6020020151614a11565b60208301528152604082015161466390836003614641565b60608301526040820152608082015161467e90836005614641565b60a08301526080820152805160208201516146db91907f2fb347984f7911f74c0bec3cf559b143b78cc310c2c3330c99e39557176f553d7f16c9e55061ebae204ba4cc8bd75a079432ae2a1d0b7c9dce1665d51c640fcba26140d6565b602083015281526040810151606082015161473891907f063cf305489af5dcdc5ec698b6e2f9b9dbaae0eda9c95998dc54014671a0135a7f07c03cbcac41049a0704b5a7ec796f2b21807dc98fa25bd282d37f632623b0e36140d6565b60608301526040820152919050565b600080806147886000805160206158708339815191528087880960008051602061587083398151915287880908600080516020615870833981519152614290565b90506000805160206158708339815191528186096000805160206158708339815191528286096147c6906000805160206158708339815191526154a4565b92509250509250929050565b6060814710156147f7573060405163cd78605960e01b8152600401610b269190615009565b600080856001600160a01b031684866040516148139190615513565b60006040518083038185875af1925050503d8060008114614850576040519150601f19603f3d011682016040523d82523d6000602084013e614855565b606091505b5091509150614865868383614a38565b9695505050505050565b6000806000806000806148848c8c60036149de565b909650945061489586868e8e6140d6565b90965094506148a68a8a8a8a6140d6565b90985096506148b78c8c8c8c6140d6565b90945092506148c884848a8a6140d6565b90945092506148d9868681816140d6565b909c509a506148ea848460086149de565b90925090506148fb8c8c84846142e1565b909c509a5061490c888881816140d6565b909250905061491d848460046149de565b909450925061492e84848e8e6142e1565b909450925061493f848488886140d6565b90945092506149508a8a60086149de565b909650945061496186868c8c6140d6565b9096509450614972868684846140d6565b9096509450614983848488886142e1565b90945092506149948c8c60026149de565b90965094506149a586868a8a6140d6565b90965094506149b6888884846140d6565b90925090506149c7828260086149de565b809250819350505096509650965096509650969050565b60008060008051602061587083398151915283860960008051602061587083398151915284860991509150935093915050565b60008083614a2d846000805160206158708339815191526154a4565b915091509250929050565b606082614a4d57614a4882614a8b565b613f6c565b8151158015614a6457506001600160a01b0384163b155b15614a845783604051639996b31560e01b8152600401610b269190615009565b5080613f6c565b805115614a9b5780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b60408051610140810182526000808252602082018190529181019190915260608101614ade614b0e565b81526020016000815260200160008152602001600081526020016000815260200160608152602001600081525090565b604051806040016040528060008152602001600081525090565b60405180608001604052806004906020820280368337509192915050565b6040518060400160405280614b59614bc8565b8152602001614b66614bc8565b905290565b508054600082556003029060005260206000209081019061085c9190614be6565b6040518060c001604052806006906020820280368337509192915050565b60405180602001604052806001906020820280368337509192915050565b60405180604001604052806002906020820280368337509192915050565b5b80821115614c195780546001600160a01b03199081168255600182018054909116905560006002820155600301614be7565b5090565b80356001600160401b0381168114614c3457600080fd5b919050565b600060208284031215614c4b57600080fd5b613f6c82614c1d565b80518252602090810151910152565b805180516001600160a01b03908116845260209182015116818401520151604082015260600190565b600081518084526020840193506020830160005b82811015614cc457614cb3868351614c63565b955060209190910190600101614ca0565b5093949350505050565b60208152614ce86020820183516001600160401b03169052565b60006020830151614d0460408401826001600160401b03169052565b5060408301516001600160a01b0381166060840152506060830151614d2c6080840182614c54565b50608083015160c083015260a083015160e083015260c083015161010083015260e0830151610120830152610100830151610160610140840152614d74610180840182614c8c565b90506101208401516101608401528091505092915050565b600060208284031215614d9e57600080fd5b5035919050565b6001600160401b0391909116815260200190565b634e487b7160e01b600052604160045260246000fd5b604080519081016001600160401b0381118282101715614df157614df1614db9565b60405290565b604051608081016001600160401b0381118282101715614df157614df1614db9565b604051601f8201601f191681016001600160401b0381118282101715614e4157614e41614db9565b604052919050565b600060208284031215614e5b57600080fd5b81356001600160401b03811115614e7157600080fd5b8201601f81018413614e8257600080fd5b80356001600160401b03811115614e9b57614e9b614db9565b614eae601f8201601f1916602001614e19565b818152856020838501011115614ec357600080fd5b81602084016020830137600091810160200191909152949350505050565b6001600160a01b038116811461085c57600080fd5b60006080828403121561426657600080fd5b60006001600160401b03821115614f2157614f21614db9565b5060051b60200190565b600082601f830112614f3c57600080fd5b8135614f4f614f4a82614f08565b614e19565b8082825260208201915060208360051b860101925085831115614f7157600080fd5b602085015b83811015614f9557614f8781614c1d565b835260209283019201614f76565b5095945050505050565b60008060008060e08587031215614fb557600080fd5b8435614fc081614ee1565b935060208501359250614fd68660408701614ef6565b915060c08501356001600160401b03811115614ff157600080fd5b614ffd87828801614f2b565b91505092959194509250565b6001600160a01b0391909116815260200190565b604081016124418284614c54565b60008060008084860361010081121561504357600080fd5b604081121561505157600080fd5b50849350604084013592506150698660608601614ef6565b915060e08501356001600160401b03811115614ff157600080fd5b6040808252835190820181905260009060208501906060840190835b818110156150c75783516001600160401b03168352602093840193909201916001016150a0565b50508381036020808601919091528551808352918101925085019060005b8181101561510e576150f8848451614c54565b60409390930192602092909201916001016150e5565b50919695505050505050565b60006040828403121561512c57600080fd5b615134614dcf565b823581526020928301359281019290925250919050565b60006080828403121561515d57600080fd5b615165614df7565b8235815260208084013590820152604080840135908201526060928301359281019290925250919050565b600082601f8301126151a157600080fd5b81356151af614f4a82614f08565b808282526020820191506020606084028601019250858311156151d157600080fd5b602085015b83811015614f955780870360608112156151ef57600080fd5b6151f7614dcf565b604082121561520557600080fd5b61520d614dcf565b9150823561521a81614ee1565b8252602083013561522a81614ee1565b602083810191909152918152604083013581830152845292909201916060016151d6565b60008060008084860361016081121561526657600080fd5b615270878761511a565b945061527f876040880161514b565b9350608060bf198201121561529357600080fd5b5061529c614df7565b60c0860135815260e08601356020820152610100860135604082015261012086013561ffff811681146152ce57600080fd5b606082015291506101408501356001600160401b038111156152ef57600080fd5b614ffd87828801615190565b6000806020838503121561530e57600080fd5b82356001600160401b0381111561532457600080fd5b8301601f8101851361533557600080fd5b80356001600160401b0381111561534b57600080fd5b8560208260051b840101111561536057600080fd5b6020919091019590945092505050565b60006020828403121561538257600080fd5b8135613f6c81614ee1565b918252602082015260400190565b600080600080600080600060e0888a0312156153b657600080fd5b87356153c181614ee1565b965060208801356153d181614ee1565b96999698505050506040850135946060810135946080820135945060a0820135935060c0909101359150565b600080600080610100858703121561541457600080fd5b61541e868661511a565b935061542d866040870161514b565b925060c085013561543d81614ee1565b9396929550929360e00135925050565b634e487b7160e01b600052601160045260246000fd5b80820281158282048414176124415761244161544d565b634e487b7160e01b600052601260045260246000fd5b60008261549f5761549f61547a565b500490565b818103818111156124415761244161544d565b6000608082840312156154c957600080fd5b613f6c838361514b565b6000604082840312156154e557600080fd5b613f6c838361511a565b60005b8381101561550a5781810151838201526020016154f2565b50506000910152565b600082516155258184602087016154ef565b9190910192915050565b6001600160401b039390931683526020830191909152604082015260600190565b6001600160401b038316815260608101613f6c602083018480358252602090810135910152565b93845260208401929092526040830152606082015260800190565b808201808211156124415761244161544d565b634e487b7160e01b600052603260045260246000fd5b6001600160a01b0385168152600061010082016155db6020840187614c54565b8451606084015260208501516080840152604085015160a084015261ffff60608601511660c084015261010060e08401528084518083526101208501915060208601925060005b8181101561564657615635838551614c63565b602094909401939250600101615622565b509098975050505050505050565b6001600160a01b038316815260608101613f6c6020830184614c54565b60008235607e1983360301811261552557600080fd5b6000808335601e1984360301811261569e57600080fd5b8301803591506001600160401b038211156156b857600080fd5b6020019150606081023603821315612ae657600080fd5b80546001600160a01b0319166001600160a01b0392909216919091179055565b81356156fa81614ee1565b61570481836156cf565b50602082013561571381614ee1565b61572081600184016156cf565b50604082013560028201555050565b6001600160401b0381811683821601908111156124415761244161544d565b6001600160a01b038416815260208101839052608081016132a46040830184614c54565b6000600182016157845761578461544d565b5060010190565b60006001600160401b0382166002600160401b031981016157ae576157ae61544d565b60010192915050565b6001600160401b039290921682526001600160a01b0316602082015260400190565b600084516157eb8184602089016154ef565b919091019283525060601b6001600160601b0319166020820152603401919050565b600060ff821660ff81036157ae576157ae61544d565b634e487b7160e01b600052600160045260246000fd5b6000826158485761584861547a565b500690565b60006020828403121561585f57600080fd5b81518015158114613f6c57600080fdfe30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd476e0956cda88cad152e89927e53611735b61a5c762d1428573c6931b0a5efcb01424c535f5349475f545259414e44494e4352454d454e545f4c49515549444154452b149d40ceb8aaae81be18991be06ac3b5b4c5e559dbefa33267e6dc24a138e5a164736f6c634300081a000a", - "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106103015760003560e01c8063040f985314610306578063095f14041461032f5780630962ef79146103465780630bd1b4191461035b5780630caca63b1461036457806317f33a7d146103775780631f744b541461039e578063245fc183146103a657806334fde376146103b9578063372500ab146103c15780633f4ba83a146103c957806343039d90146103d1578063538d2709146103e4578063544736e6146103ed57806356d399e81461040a57806358e93308146104135780635c975abb1461041c578063623b94221461042457806362f1fbc21461042d57806365ca819d146104375780636b348ab41461046b578063715018a61461047457806379ba50971461047c5780637a6d4065146104845780637c89d2f0146104975780638328b610146104bc5780638456cb59146104cf57806384cd9b57146104d757806386e76685146104ea57806386fa5063146104f3578063879d0f0c146104fc5780638a2209e6146105045780638a399481146105195780638da5cb5b146105225780639156ac801461052a57806395055ffd146105335780639592d424146105465780639c80ebee1461054f5780639cebc4741461042d5780639e3b372d14610557578063a44285a514610560578063ab0122ae14610569578063abf2c5031461057c578063ae6c606314610592578063b13aaebd1461059b578063b687f85c146105a4578063bc7efecc146105ac578063be9a6555146105bf578063c0ebedab146105c7578063c4f56d9a146105f0578063d5a8125414610603578063d5ca793f1461060c578063d655422f1461061f578063d84f0bf814610632578063d9054b091461063b578063d995b00c1461064e578063da1d543514610657578063dd644d7414610660578063e30c397814610669578063eb82031214610671578063edbf4ac2146106a6578063ee94e412146106b9578063f2fde38b146106c2578063f324c8eb146106d5578063f88d658b146106e8578063f907f5fc146106fb578063ffd9a86d1461070e575b600080fd5b610319610314366004614c39565b610721565b6040516103269190614cce565b60405180910390f35b610338600d5481565b604051908152602001610326565b610359610354366004614d8c565b610852565b005b610338600f5481565b610359610372366004614d8c565b61085f565b60015461039190600160a01b90046001600160401b031681565b6040516103269190614da5565b6103596108c4565b6103596103b4366004614d8c565b610994565b6103386109f2565b610359610a29565b610359610a5c565b6103596103df366004614d8c565b610a6e565b61033860075481565b6000546103fa9060ff1681565b6040519015158152602001610326565b610338600b5481565b610338600e5481565b6103fa610acc565b61033860155481565b61033862278d0081565b610391610445366004614e49565b80516020818301810180516012825292820191909301209152546001600160401b031681565b610338600a5481565b610359610ae1565b610359610af3565b610359610492366004614f9f565b610b38565b6000546104af9061010090046001600160a01b031681565b6040516103269190615009565b6103596104ca366004614d8c565b610cd6565b610359610d33565b6103596104e5366004614d8c565b610d43565b61033860045481565b610338600c5481565b610338601481565b61050c610da1565b604051610326919061501d565b61033860035481565b6104af610dc4565b61033860195481565b61035961054136600461502b565b610ddf565b61033860025481565b610391600081565b61033860095481565b61033860175481565b610359610577366004614c39565b610f8c565b610584611055565b604051610326929190615084565b61033860065481565b61033860185481565b610338600481565b6103596105ba36600461524e565b6111e0565b610359611509565b6103916105d5366004614d8c565b601b602052600090815260409020546001600160401b031681565b6103596105fe36600461502b565b611520565b61033860055481565b61035961061a3660046152fb565b611923565b61035961062d366004614d8c565b611bb6565b61033860085481565b610359610649366004614c39565b611bf3565b610338611c2081565b61033860165481565b610338601a5481565b6104af611c05565b61069861067f366004615370565b6011602052600090815260409020805460019091015482565b60405161032692919061538d565b6103596106b436600461539b565b611c10565b610338610e1081565b6103596106d0366004615370565b611f6a565b6103596106e3366004614d8c565b611fdb565b6103596106f6366004614d8c565b612039565b6001546104af906001600160a01b031681565b61035961071c3660046153fd565b612097565b610729614ab4565b6001600160401b03808316600090815260106020908152604080832081516101408101835281548087168252600160401b90049095168584015260018101546001600160a01b0316858301528151808301835260028201548152600382015481850152606086015260048101546080860152600581015460a0860152600681015460c0860152600781015460e0860152600881018054835181860281018601909452808452919461010087019491929184015b82821015610839576000848152602090819020604080516080810182526003860290920180546001600160a01b039081169284019283526001808301549091166060850152918352600201548284015290835290920191016107dc565b5050505081526020016009820154815250509050919050565b61085c33826120a9565b50565b6108676121ec565b6000811161088857604051630ef6cf4560e41b815260040160405180910390fd5b600f8190556040518181527f20d448c79534efb2c0adc259142770a7979eea09d1e5850fc1b02403337e11a8906020015b60405180910390a150565b6000600281905580526010602052600080516020615890833981519152546001600160401b03165b6001600160401b0381161561085c576001600160401b0381166000908152601060205260408120600254909103610932576002810154601355600381015460145561097a565b604080518082018252601354815260145460208083019190915282518084019093526002840154835260038401549083015261096d9161221e565b8051601355602001516014555b546002805460010190556001600160401b031690506108ec565b61099c6121ec565b600081116109bd57604051630ef6cf4560e41b815260040160405180910390fd5b60188190556040518181527fa83e79dc52a438d552d0631ce4fbfe7dec7656d10398a2849f352a6ab4f0163b906020016108b9565b60008060646002546002610a069190615463565b610a109190615490565b905060148111610a21576014610a23565b805b91505090565b336000908152601160205260408120600181015490549091610a4b83836154a4565b9050610a5733826120a9565b505050565b610a646121ec565b610a6c6122ca565b565b610a766121ec565b60008111610a9757604051630ef6cf4560e41b815260040160405180910390fd5b60048190556040518181527f0ac8ee09138dfaf5e3ebe4cb4fd42dd1a0695535a530171223fb5066f52e0e3b906020016108b9565b600080610ad7612316565b5460ff1692915050565b610ae96121ec565b610a6c600061233a565b3380610afd611c05565b6001600160a01b031614610b2f578060405163118cdaa760e01b8152600401610b269190615009565b60405180910390fd5b61085c8161233a565b610b40612361565b60005460ff16610b635760405163348b55eb60e21b815260040160405180910390fd5b8051600354811115610ba85780600254610b7d91906154a4565b600354600254610b8d91906154a4565b604051635eee4a3560e01b8152600401610b2692919061538d565b6001600160a01b038516610bcf5760405163e99d5ac560e01b815260040160405180910390fd5b6001600160a01b0385166000908152601160205260409020548411610c07576040516333938e6360e01b815260040160405180910390fd5b6007546040805160208101929092526001600160601b0319606088901b16908201526054810185905260009060740160405160208183030381529060405290506000610c5582600a54612387565b9050610c7084610c6a368890038801886154b7565b83612447565b50506001600160a01b0385166000818152601160205260409081902080549087905590519091907f95390641529563dbfb446535fa996c5ac3be00f90f5705b3abda59a4467b797f90610cc6908890859061538d565b60405180910390a2505050505050565b610cde6121ec565b60008111610cff57604051630ef6cf4560e41b815260040160405180910390fd5b600b8190556040518181527e6b7a1ea14ff2794527a64af37d55a2040e351f8b4c1adcdc9aea80d64e0429906020016108b9565b610d3b6121ec565b610a6c612580565b610d4b6121ec565b60008111610d6c576040516352513c5960e01b815260040160405180910390fd5b600d8190556040518181527f7c6ad2fef3b5f9e109dad55b811c13b9c880062367b00074d9286d7e7300b35f906020016108b9565b610da9614b0e565b50604080518082019091526013548152601454602082015290565b600080610dcf6125c7565b546001600160a01b031692915050565b610de7612361565b60005460ff16610e0a5760405163348b55eb60e21b815260040160405180910390fd5b8051600354811115610e245780600254610b7d91906154a4565b6000610e3d610e38368890038801886154d3565b6125eb565b90506000601282604051610e519190615513565b908152604051908190036020019020546001600160401b03169050610e7586612614565b15610e99578086426040516337030b8b60e01b8152600401610b269392919061552f565b6001600160401b0381166000908152601060205260409020600201548735141580610ee557506001600160401b0381166000908152601060209081526040909120600301549088013514155b15610f0757808760405163c43283b560e01b8152600401610b26929190615550565b600854604051600091610f26918a35906020808d0135918c9101615577565b60405160208183030381529060405290506000610f4582600a54612387565b9050610f5a86610c6a368a90038a018a6154b7565b50506001600160401b038116600090815260106020526040902060070154610f8390829061262c565b50505050505050565b610f94612361565b60005460ff16610fb75760405163348b55eb60e21b815260040160405180910390fd5b6001600160401b03811660009081526010602052604081206005015490819003610ff65781604051630cf4827360e31b8152600401610b269190614da5565b600061100562278d0083615592565b90508042101561102e57828142604051637a54a45360e11b8152600401610b269392919061552f565b6001600160401b038316600090815260106020526040902060070154610a5790849061262c565b6060806002546001600160401b0381111561107257611072614db9565b60405190808252806020026020018201604052801561109b578160200160208202803683370190505b5091506002546001600160401b038111156110b8576110b8614db9565b6040519080825280602002602001820160405280156110f157816020015b6110de614b0e565b8152602001906001900390816110d65790505b5060008080526010602052600080516020615890833981519152549192506001600160401b03909116905b6001600160401b038216156111da576001600160401b0380831660009081526010602052604090208551909184918791851690811061115d5761115d6155a5565b60200260200101906001600160401b031690816001600160401b031681525050806002016040518060400160405290816000820154815260200160018201548152505084836001600160401b0316815181106111bb576111bb6155a5565b6020908102919091010152546001600160401b0316915060010161111c565b50509091565b6111e8612361565b60005460ff1661120b5760405163348b55eb60e21b815260040160405180910390fd5b8051600c548111156112305760405163226c2d8360e21b815260040160405180910390fd5b80156112a4576000805b8281101561127557838181518110611254576112546155a5565b6020026020010151602001518261126b9190615592565b915060010161123a565b50600b54811461129e57600b54816040516367d22bd960e01b8152600401610b2692919061538d565b50611339565b60408051600180825281830190925290816020015b60408051608081018252600091810182815260608201839052815260208101919091528152602001906001900390816112b957505060408051608081018252339181018281526060820192909252908152600b5460208201528151919350908390600090611329576113296155a5565b6020026020010181905250815190505b60006012611346876125eb565b6040516113539190615513565b908152604051908190036020019020546001600160401b03169050801561138f578060405163459c639360e01b8152600401610b269190614da5565b6000836000815181106113a4576113a46155a5565b6020026020010151600001516000015190506113c687878388600001516126cf565b6000806113d78988600001516127b9565b600b5460078201556001810180546001600160a01b0319166001600160a01b038716179055909250905060005b8581101561148b5781600801878281518110611422576114226155a5565b60209081029190910181015182546001808201855560009485529383902082518051600390930290910180546001600160a01b03199081166001600160a01b03948516178255918501518187018054909316931692909217905591015160029091015501611404565b50611494612aed565b816001600160401b03167f533c16cb4158fe7c77021b90e9290bbefa457dd392bd8abc1eab59747834fd5b848b8a8a6040516114d394939291906155bb565b60405180910390a26114fe600060019054906101000a90046001600160a01b03163330600b54612b19565b505050505050505050565b6115116121ec565b6000805460ff19166001179055565b611528612361565b60005460ff1661154b5760405163348b55eb60e21b815260040160405180910390fd5b80516003548111156115655780600254610b7d91906154a4565b6000611579610e38368890038801886154d3565b9050600060128260405161158d9190615513565b908152604051908190036020019020546001600160401b031690506115b186612614565b156115d5578086426040516337030b8b60e01b8152600401610b269392919061552f565b6001600160401b03808216600090815260106020908152604080832081516101408101835281548087168252600160401b90049095168584015260018101546001600160a01b0316858301528151808301835260028201548152600382015481850152606086015260048101546080860152600581015460a0860152600681015460c0860152600781015460e086015260088101805483518186028101860190945280845294959491936101008601939290879084015b828210156116e9576000848152602090819020604080516080810182526003860290920180546001600160a01b0390811692840192835260018083015490911660608501529183526002015482840152908352909201910161168c565b505050908252506009919091015460209091015260608101515190915088351415806117215750806060015160200151886020013514155b1561174357818860405163c43283b560e01b8152600401610b26929190615550565b611c2081608001516117559190615592565b42101561177e57608081015160405163c666e87d60e01b8152610b26918491429060040161552f565b60095460405160009161179d918b35906020808e0135918d9101615577565b604051602081830303815290604052905060006117bc82600a54612387565b90506117d187610c6a368b90038b018b6154b7565b5050816001600160401b03167f0bfb12191b00293af29126b1c5489f8daeb4a4af82db2960b7f8353c3105cd7c82604001518360600151604051611816929190615654565b60405180910390a26000600f54600d54600e546118339190615592565b61183d9190615592565b905060008260e001519050600082600d54836118599190615463565b6118639190615490565b90506000600e54836118759190615463565b156118a457836001600e5461188a91906154a4565b6118949190615490565b61189f906001615592565b6118a7565b60005b90506118c786826118b885876154a4565b6118c291906154a4565b61262c565b600d54156118eb576000546118eb9061010090046001600160a01b03163384612b80565b600e541561191557600054600154611915916001600160a01b036101009091048116911683612b80565b505050505050505050505050565b61192b6121ec565b60005460ff161561194f57604051630a35d5c360e31b815260040160405180910390fd5b8060005b81811015611bad573684848381811061196e5761196e6155a5565b90506020028101906119809190615671565b905060006119916060830183615687565b9050116119b157604051631a10033d60e11b815260040160405180910390fd5b600c546119c16060830183615687565b905011156119e25760405163226c2d8360e21b815260040160405180910390fd5b600080611a016119f7368590038501856154d3565b84604001356127b9565b600b5460078201559092509050611a1b6060840184615687565b6000818110611a2c57611a2c6155a5565b611a429260206060909202019081019150615370565b6001820180546001600160a01b0319166001600160a01b0392909216919091179055600080611a746060860186615687565b9050905060005b81811015611b225736611a916060880188615687565b83818110611aa157611aa16155a5565b9050606002019050806040013584611ab99190615592565b93506000611aca6020830183615370565b6001600160a01b031603611af15760405163e99d5ac560e01b815260040160405180910390fd5b60088501805460018101825560009182526020909120829160030201611b1782826156ef565b505050600101611a7b565b50600b548214611b4b57600b54826040516367d22bd960e01b8152600401610b2692919061538d565b604080518635815260208088013590820152868201358183015290516001600160401b038616917f34be2fa283e1d5eb453459898f160448257aa1ef0f7dd2c3a3b55c45d1af768f919081900360600190a26001860195505050505050611953565b50610a57612aed565b611bbe6121ec565b600e8190556040518181527f4751fdab53c6c42462ab92ce837e339f8efc4c64fa76dadd99cc95baa68a4c00906020016108b9565b611bfb612361565b61085c8133612bb1565b600080610dcf612d81565b6000611c1a612da5565b805490915060ff600160401b82041615906001600160401b0316600081158015611c415750825b90506000826001600160401b03166001148015611c5d5750303b155b905081158015611c6b575080155b15611c895760405163f92ee8a960e01b815260040160405180910390fd5b84546001600160401b03191660011785558315611cb257845460ff60401b1916600160401b1785555b6001861015611cd457604051630ef6cf4560e41b815260040160405180910390fd5b6001881015611cf6576040516352513c5960e01b815260040160405180910390fd5b6000805460ff19168155600281905560035561012c60045560408051808201909152601b81527a0424c535f5349475f545259414e44494e4352454d454e545f504f5602c1b6020820152611d4990612dc9565b60065560408051808201909152601e81527f424c535f5349475f545259414e44494e4352454d454e545f52455741524400006020820152611d8990612dc9565b60075560408051808201909152601c81527b109314d7d4d251d7d51496505391125390d4915351539517d156125560221b6020820152611dc890612dc9565b600881905550611def6040518060600160405280602181526020016158b060219139612dc9565b600955604080518082019091526019815278424c535f5349475f484153485f544f5f4649454c445f54414760381b6020820152611e2b90612dc9565b600a5561025860055566038d7ea4c6800060175561a8c060185560006019819055601a8190558054610100600160a81b0319166101006001600160a01b038f811691909102919091178255600180546001600160a01b031916918e16919091178155600b8c9055600c8b9055600d8a9055600e899055600f889055611eb0919061572f565b600180546001600160401b0392909216600160a01b02600160a01b600160e01b031990921691909117905560008052601060205260008051602061589083398151915280546001600160801b0319169055611f0a33612dfd565b611f12612e0e565b831561191557845460ff60401b191685556040517fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d290611f5490600190614da5565b60405180910390a1505050505050505050505050565b611f726121ec565b6000611f7c612d81565b80546001600160a01b0319166001600160a01b0384169081178255909150611fa2610dc4565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b611fe36121ec565b6000811161200457604051630ef6cf4560e41b815260040160405180910390fd5b60178190556040518181527fe604909b918af52702fa14cd9b5a8870e5bd66da3163365671e583f705fd5463906020016108b9565b6120416121ec565b6000811161206257604051630ef6cf4560e41b815260040160405180910390fd5b60058190556040518181527ffbbc3d0a51e101ce4a7fda20e6e4eb0e230bf7de27ee2e3683fc8539e3766395906020016108b9565b6120a3848484846126cf565b50505050565b6001600160a01b03821660009081526011602052604081206001810154905490916120d483836154a4565b9050808411156120f75760405163363cc6b360e21b815260040160405180910390fd5b6000601854426121079190615490565b9050601a5481111561211e57601a81905560006019555b84601960008282546121309190615592565b9091555050601754601954111561215a57604051638c10944b60e01b815260040160405180910390fd5b6001600160a01b03861660009081526011602052604081206001018054879290612185908490615592565b90915550506040518581526001600160a01b038716907ffc30cddea38e2bf4d6ea7d3f9ed3b6ad7f176419f4963bd81318067a4aee73fe9060200160405180910390a26000546121e49061010090046001600160a01b03168787612b80565b505050505050565b336121f5610dc4565b6001600160a01b031614610a6c573360405163118cdaa760e01b8152600401610b269190615009565b612226614b0e565b61222e614b28565b835181526020808501518183015283516040808401919091529084015160608301526000908360808460066107d05a03fa9050806122c25760405162461bcd60e51b815260206004820152602b60248201527f43616c6c20746f20707265636f6d70696c656420636f6e747261637420666f7260448201526a081859190819985a5b195960aa1b6064820152608401610b26565b505092915050565b6122d2612e1e565b60006122dc612316565b805460ff1916815590507f5db9ee0a495bf2e6ff9c91a7834c1ba4fdd244a5e8aa4e537bd38aeae4b073aa335b6040516108b99190615009565b7fcd5ed15c6e187e77e9aee88184c21f4f2182ab5827cb3b7e07fbedcd63f0330090565b6000612344612d81565b80546001600160a01b0319168155905061235d82612e43565b5050565b612369610acc565b15610a6c5760405163d93c066560e01b815260040160405180910390fd5b61238f614b46565b600061239b8484612e9f565b905060008060008061240885600001516001600281106123bd576123bd6155a5565b602002015186516000602002015187602001516001600281106123e2576123e26155a5565b602002015188602001516000600281106123fe576123fe6155a5565b6020020151613070565b60408051608081018252808201948552606081019590955292845282518084019093528252602082810191909152820152955050505050505b92915050565b61244f614b0e565b835160005b818110156124d5576000868281518110612470576124706155a5565b602002602001015190506124ca8460106000846001600160401b03166001600160401b031681526020019081526020016000206002016040518060400160405290816000820154815260200160018201548152505061221e565b935050600101612454565b5060408051808201909152601354815260145460208201526124ff906124fa84613100565b61221e565b604080516080810182526020808801518284019081528851606080850191909152908352835180850185529089015181529288015183820152810191909152909250600061254c84613100565b905061256161255961318a565b8383886131ab565b610f835780604051634fb09a2160e11b8152600401610b26919061501d565b612588612361565b6000612592612316565b805460ff1916600117815590507f62e78cea01bee320cd4e420270b5ea74000d11b0c9f74754ebdbfc544b05a2586123093390565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930090565b6060816040516020016125fe919061501d565b6040516020818303038152906040529050919050565b6000600554826126249190615592565b421192915050565b6001600160401b0382166000908152601060209081526040918290206001810154835180850190945260028201548452600390910154918301919091526001600160a01b03169061267c846132ac565b612684612aed565b836001600160401b03167f24c4411329b949fad2eba6f79a8ba090a08eac1af4b56c3a2f5a282ed45e233e8385846040516126c19392919061574e565b60405180910390a250505050565b600060065485600001518660200151858560405160200161272095949392919094855260208501939093526040840191909152606090811b6001600160601b03191690830152607482015260940190565b6040516020818303038152906040529050600061273f82600a54612387565b60408051608081018252602080890151828401908152895160608085019190915290835283518085018552908a01518152928901518382015281019190915290915061279c61278c61318a565b826127968a613100565b856131ab565b610f835760405163cf006ab760e01b815260040160405180910390fd5b8151600090819015806127ce57506020840151155b156127ec5760405163139b722760e31b815260040160405180910390fd5b8260000361280d57604051634eb8f11f60e01b815260040160405180910390fd5b6000612818856125eb565b905060006001600160401b03166012826040516128359190615513565b908152604051908190036020019020546001600160401b031614612895576012816040516128639190615513565b9081526040519081900360200181205463459c639360e01b8252610b26916001600160401b0390911690600401614da5565b6000848152601b60205260409020546001600160401b0316156128e5576000848152601b602052604090819020549051630cb4c72160e21b8152610b26916001600160401b031690600401614da5565b60005460ff161561294a57436015541015612904574360155560006016555b6016805490600061291483615772565b919050555060006129236109f2565b90508060165411156129485760405163689f6e7560e11b815260040160405180910390fd5b505b60018054600160a01b90046001600160401b031690601461296a8361578b565b91906101000a8154816001600160401b0302191690836001600160401b03160217905550925060026000815461299f90615772565b909155506001600160401b038381166000818152601060209081526040808320600080516020615890833981519152805482546001600160801b031916600160401b918290049098168082029890981783558154600160401b600160801b03191690870217815586855282852080546001600160401b031990811688179091558c5160028401558c8501516003840155426004840155600983018c90558b8652601b9094529382902080549093169094179091555191945091908590601290612a69908690615513565b90815260405190819003602001902080546001600160401b03929092166001600160401b0319909216919091179055600254600103612ab45786516013556020870151601455612ae2565b6040805180820190915260135481526014546020820152612ad5908861221e565b8051601355602001516014555b5050505b9250929050565b60006003600254612afe9190615490565b90506004548111612b0f5780612b13565b6004545b60035550565b6040516001600160a01b0384811660248301528381166044830152606482018390526120a39186918216906323b872dd906084015b604051602081830303815290604052915060e01b6020820180516001600160e01b0383818316178352505050506135b5565b6040516001600160a01b03838116602483015260448201839052610a5791859182169063a9059cbb90606401612b4e565b60005460ff16612bd45760405163348b55eb60e21b815260040160405180910390fd5b6001600160401b03821660009081526010602052604081206008810154829190825b81811015612c67576000836008018281548110612c1557612c156155a5565b6000918252602090912060039091020180549091506001600160a01b03808916911603612c5e576007840154600282015460019750612c55906004615463565b10945050612c67565b50600101612bf6565b5083612c8a578585604051635bf2837760e11b8152600401610b269291906157b7565b8160050154600003612ce157828015612cb4575062278d008260040154612cb19190615592565b42105b15612cd6578585604051633826feb560e01b8152600401610b269291906157b7565b426005830155612d20565b6000610e108360060154612cf59190615592565b905080421015612d1e57868142604051637a54a45360e11b8152600401610b269392919061552f565b505b426006830155604080516001600160a01b0387168152600284015460208201526003840154918101919091526001600160401b038716907f8600c0145511b317ae0189933fa71eb57a32d74891804c7993ef74ec63a5e80490606001610cc6565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0090565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0090565b6000814630604051602001612de0939291906157d9565b604051602081830303815290604052805190602001209050919050565b612e0561360f565b61085c81613634565b612e1661360f565b610a6c613666565b612e26610acc565b610a6c57604051638dfc202b60e01b815260040160405180910390fd5b6000612e4d6125c7565b80546001600160a01b038481166001600160a01b031983168117845560405193945091169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a3505050565b612ea7614b46565b600080600080600087516001612ebd9190615592565b6001600160401b03811115612ed457612ed4614db9565b6040519080825280601f01601f191660200182016040528015612efe576020820181803683370190505b50885190915060005b81811015612f5d57898181518110612f2157612f216155a5565b602001015160f81c60f81b838281518110612f3e57612f3e6155a5565b60200101906001600160f81b031916908160001a905350600101612f07565b5060005b8060f81b8360018551612f7491906154a4565b81518110612f8457612f846155a5565b60200101906001600160f81b031916908160001a9053506000612fa7848b613683565b91995097509050600080612fbb8a8a61377b565b91509150600080612fcc84846137ee565b9150915081600014158015612fe057508015155b1561301d578415612ffb57612ff5828261395d565b90925090505b9098509650878761300e8c8c84846139a2565b1561301d575050505050613035565b5050505050808061302d9061580d565b915050612f61565b505060408051608081018252808201958652606081019690965293855250825180840190935282526020808301919091528201529392505050565b600080600080613082888888886139b9565b61308e5761308e615823565b6040805160c081018252898152602081018990529081018790526060810186905260016080820152600060a08201526130c681613a5c565b8051602082015160408301516060840151608085015160a08601519596506130ed95613c0e565b929c919b50995090975095505050505050565b613108614b0e565b815115801561311957506020820151155b15613137575050604080518082019091526000808252602082015290565b604051806040016040528083600001518152602001600080516020615870833981519152846020015161316a9190615839565b613182906000805160206158708339815191526154a4565b905292915050565b613192614b0e565b5060408051808201909152600181526002602082015290565b60408051600280825260608201909252600091829190816020015b6131ce614b0e565b8152602001906001900390816131c65750506040805160028082526060820190925291925060009190602082015b613204614b46565b8152602001906001900390816131fc579050509050868260008151811061322d5761322d6155a5565b6020026020010181905250848260018151811061324c5761324c6155a5565b6020026020010181905250858160008151811061326b5761326b6155a5565b6020026020010181905250838160018151811061328a5761328a6155a5565b602002602001018190525061329f8282613c58565b925050505b949350505050565b6000600254116132cf576040516363cd35d560e11b815260040160405180910390fd5b6001600160401b0381166132f657604051631d58f8cb60e11b815260040160405180910390fd5b6001600160401b03808216600090815260106020908152604080832081516101408101835281548087168252600160401b90049095168584015260018101546001600160a01b0316858301528151808301835260028201548152600382015481850152606086015260048101546080860152600581015460a0860152600681015460c0860152600781015460e086015260088101805483518186028101860190945280845294959491936101008601939290879084015b8282101561340a576000848152602090819020604080516080810182526003860290920180546001600160a01b039081169284019283526001808301549091166060850152918352600201548284015290835290920191016133ad565b5050509082525060099190910154602091820152818101805183516001600160401b0390811660009081526010855260408082208054600160401b600160801b031916600160401b9585169590950294909417909355855193518216815282902080546001600160401b0319169390911692909217909155805180820190915260135481526014549181019190915260608201519192506134ae916124fa90613100565b80516013556020015160145560608101516000906134cb906125eb565b6101208301516000908152601b60205260409081902080546001600160401b031916905551909150601290613501908390615513565b908152604080516020928190038301902080546001600160401b03191690556001600160401b03851660009081526010909252812080546001600160801b03191681556001810180546001600160a01b03191690556002810182905560038101829055600481018290556005810182905560068101829055600781018290559061358e6008830182614b6b565b600982016000905550506001600260008282546135ab91906154a4565b9091555050505050565b60006135ca6001600160a01b03841683613f5e565b905080516000141580156135ef5750808060200190518101906135ed919061584d565b155b15610a575782604051635274afe760e01b8152600401610b269190615009565b613617613f73565b610a6c57604051631afcd79f60e31b815260040160405180910390fd5b61363c61360f565b6001600160a01b038116610b2f576000604051631e4fbdf760e01b8152600401610b269190615009565b61366e61360f565b6000613678612316565b805460ff1916905550565b60008060008060008060006136b989896040516020016136a591815260200190565b604051602081830303815290604052613f8d565b935093509350935060405160308152602080820152602060408201528460608201528360808201526001609082015260008051602061587083398151915260b082015260208160d0836005600019fa61371157600080fd5b805197505060405160308152602080820152836050820152602060408201528260708201526001609082015260008051602061587083398151915260b082015260208160d0836005600019fa61376657600080fd5b51969996985060019081161496505050505050565b60008060008060008061379088888a8a6140d6565b90925090506137a182828a8a6140d6565b90925090506137df82826000805160206158d18339815191527e9713b03af0fed4cd2cafadeed8fdf4a74fa084e52d1852e4a2bd0685c315d2614147565b90999098509650505050505050565b600080600080600085600003613830576138078761417b565b909350905080156138215782600094509450505050612ae6565b60008394509450505050612ae6565b6000805160206158708339815191528788099250600080516020615870833981519152868709915060008051602061587083398151915282840892506138758361417b565b90935090508061388e5760008094509450505050612ae6565b60008051602061587083398151915283880891506138ab82614212565b915060006138b88361417b565b9250905081613909576138da888560008051602061587083398151915261426c565b92506138e583614212565b92506138f08361417b565b9250905081613909576000809550955050505050612ae6565b80600080516020615870833981519152828308935061393684600080516020615870833981519152614290565b93506000600080516020615870833981519152858a09919a91995090975050505050505050565b60008080613979856000805160206158708339815191526154a4565b90506000613995856000805160206158708339815191526154a4565b9196919550909350505050565b60006139b0858585856139b9565b95945050505050565b60008060008060006139cd878789896140d6565b90945092506139de898981816140d6565b90925090506139ef82828b8b6140d6565b9092509050613a00848484846142e1565b9094509250613a3e84846000805160206158d18339815191527e9713b03af0fed4cd2cafadeed8fdf4a74fa084e52d1852e4a2bd0685c315d26142e1565b909450925083158015613a4f575082155b9998505050505050505050565b613a64614b8c565b613a6c614b8c565b613a74614b8c565b613a7c614b8c565b84516020860151604087015160608801516080890151613ab4946744e992b44a6909f1949093909290918b60055b6020020151614323565b80516020820151604083015160608401516080850151949750613ae09460029493929190896005613aaa565b9150613b4c8360005b602002015184600160200201518560026020020151866003602002015187600460200201518860056020020151886000602002015189600160200201518a600260200201518b600360200201518c600460200201518d60055b60200201516143a6565b9150613b578261462f565b9150613b628361462f565b9050613b6d8161462f565b9050613b7a836000613ae9565b9250613be08360005b6020020151846001602002015185600260200201518660036020020151876004602002015188600560200201518760006020020151886001602002015189600260200201518a600360200201518b600460200201518c6005613b42565b9250613beb8561462f565b9050613bf68161462f565b9050613c018161462f565b90506139b0836000613b83565b600080600080600080613c218888614747565b9092509050613c328c8c84846140d6565b9096509450613c438a8a84846140d6565b969d959c509a50949850929650505050505050565b60008151835114613c6857600080fd5b82516000613c77826006615463565b90506000816001600160401b03811115613c9357613c93614db9565b604051908082528060200260200182016040528015613cbc578160200160208202803683370190505b50905060005b83811015613eed57868181518110613cdc57613cdc6155a5565b60200260200101516000015182826006613cf69190615463565b613d01906000615592565b81518110613d1157613d116155a5565b602002602001018181525050868181518110613d2f57613d2f6155a5565b60200260200101516020015182826006613d499190615463565b613d54906001615592565b81518110613d6457613d646155a5565b602002602001018181525050858181518110613d8257613d826155a5565b6020908102919091010151515182613d9b836006615463565b613da6906002615592565b81518110613db657613db66155a5565b602002602001018181525050858181518110613dd457613dd46155a5565b60209081029190910181015151015182613def836006615463565b613dfa906003615592565b81518110613e0a57613e0a6155a5565b602002602001018181525050858181518110613e2857613e286155a5565b602002602001015160200151600060028110613e4657613e466155a5565b602002015182613e57836006615463565b613e62906004615592565b81518110613e7257613e726155a5565b602002602001018181525050858181518110613e9057613e906155a5565b602002602001015160200151600160028110613eae57613eae6155a5565b602002015182613ebf836006615463565b613eca906005615592565b81518110613eda57613eda6155a5565b6020908102919091010152600101613cc2565b50613ef6614baa565b60006020826020860260208601600060086107d05a03f1905080613f505760405162461bcd60e51b8152602060048201526011602482015270496e76616c6964205369676e617475726560781b6044820152606401610b26565b505115159695505050505050565b6060613f6c838360006147d2565b9392505050565b6000613f7d612da5565b54600160401b900460ff16919050565b60008060008060ff85511115613fa257600080fd5b600060405160005b6088811015613fc157600082820152602001613faa565b506088602060005b8a51811015613fea578a820151848401526020928301929182019101613fc9565b505060898951019050608081830153600201602060005b89518110156140225789820151848401526020928301929182019101614001565b5050608b88518a5101019050875181830153508751875101608c018120915050604051818152600160208201536021602060005b89518110156140775789820151848401526020928301929182019101614056565b505050865187516021018201538651602201812095508582188152600260208201538651602201812094508482188152600360208201538651602201812093508382188152600460208201539551602201909520939692955090935050565b60008061411460008051602061587083398151915285880960008051602061587083398151915285880960008051602061587083398151915261426c565b60008051602061587083398151915280868809600080516020615870833981519152868a09089150915094509492505050565b6000806000805160206158708339815191528487086000805160206158708339815191528487089150915094509492505050565b600080600060405160208152602080820152602060408201528460608201527f0c19139cb84c680a6e14116da060561765e05aa45a1c72a34f082305b61f3f52608082015260008051602061587083398151915260a082015260208160c08360056107d05a03fa905193509050600080516020615870833981519152838009841491508061420c5760009250600091505b50915091565b6000600182161515614225600284615490565b915080156142665760008051602061587083398151915260026142576000805160206158708339815191526001615592565b6142619190615490565b830891505b50919050565b6000818061427c5761427c61547a565b61428684846154a4565b8508949350505050565b60008060405160208152602080820152602060408201528460608201526002840360808201528360a082015260208160c08360056107d05a03fa905192509050806142da57600080fd5b5092915050565b6000806142fd868560008051602061587083398151915261426c565b614316868560008051602061587083398151915261426c565b9150915094509492505050565b61432b614b8c565b871561439b57600188161561436c578051602082015160408301516060840151608085015160a08601516143699594939291908d8d8d8d8d8d6143a6565b90505b61437a87878787878761486f565b949b50929950909750955093509150614394600289615490565b975061432b565b979650505050505050565b6143ae614b8c565b881580156143ba575087155b156143fc578686868686868660005b60a0890192909252608088019290925260608701929092526040860192909252602085810193909352909102015261461f565b82158015614408575081155b1561441b578c8c8c8c8c8c8660006143c9565b61442785858b8b6140d6565b90955093506144388b8b85856140d6565b6060830152604082015261444e87878b8b6140d6565b909750955061445f8d8d85856140d6565b60a0830152608082018190528714801561447c575060a081015186145b156144c1576040810151851480156144975750606081015184145b156144b2576144aa8d8d8d8d8d8d61486f565b8660006143c9565b600160008181808086816143c9565b6144cd898985856140d6565b90935091506144ed858583600260200201518460035b60200201516142e1565b909d509b50614507878783600460200201518460056144e3565b909b5099506145188b8b81816140d6565b9099509750614538898983600460200201518460055b60200201516140d6565b909550935061454989898d8d6140d6565b909950975061455a898985856140d6565b60a083015260808201526145708d8d81816140d6565b9097509550614581878785856140d6565b909750955061459287878b8b6142e1565b90975095506145a3858560026149de565b90935091506145b4878785856142e1565b90975095506145c58b8b89896140d6565b602083015281526145d8858589896142e1565b909b5099506145e98d8d8d8d6140d6565b909b5099506146038989836002602002015184600361452e565b909d509b506146148b8b8f8f6142e1565b606083015260408201525b9c9b505050505050505050505050565b614637614b8c565b815161464b908360015b6020020151614a11565b60208301528152604082015161466390836003614641565b60608301526040820152608082015161467e90836005614641565b60a08301526080820152805160208201516146db91907f2fb347984f7911f74c0bec3cf559b143b78cc310c2c3330c99e39557176f553d7f16c9e55061ebae204ba4cc8bd75a079432ae2a1d0b7c9dce1665d51c640fcba26140d6565b602083015281526040810151606082015161473891907f063cf305489af5dcdc5ec698b6e2f9b9dbaae0eda9c95998dc54014671a0135a7f07c03cbcac41049a0704b5a7ec796f2b21807dc98fa25bd282d37f632623b0e36140d6565b60608301526040820152919050565b600080806147886000805160206158708339815191528087880960008051602061587083398151915287880908600080516020615870833981519152614290565b90506000805160206158708339815191528186096000805160206158708339815191528286096147c6906000805160206158708339815191526154a4565b92509250509250929050565b6060814710156147f7573060405163cd78605960e01b8152600401610b269190615009565b600080856001600160a01b031684866040516148139190615513565b60006040518083038185875af1925050503d8060008114614850576040519150601f19603f3d011682016040523d82523d6000602084013e614855565b606091505b5091509150614865868383614a38565b9695505050505050565b6000806000806000806148848c8c60036149de565b909650945061489586868e8e6140d6565b90965094506148a68a8a8a8a6140d6565b90985096506148b78c8c8c8c6140d6565b90945092506148c884848a8a6140d6565b90945092506148d9868681816140d6565b909c509a506148ea848460086149de565b90925090506148fb8c8c84846142e1565b909c509a5061490c888881816140d6565b909250905061491d848460046149de565b909450925061492e84848e8e6142e1565b909450925061493f848488886140d6565b90945092506149508a8a60086149de565b909650945061496186868c8c6140d6565b9096509450614972868684846140d6565b9096509450614983848488886142e1565b90945092506149948c8c60026149de565b90965094506149a586868a8a6140d6565b90965094506149b6888884846140d6565b90925090506149c7828260086149de565b809250819350505096509650965096509650969050565b60008060008051602061587083398151915283860960008051602061587083398151915284860991509150935093915050565b60008083614a2d846000805160206158708339815191526154a4565b915091509250929050565b606082614a4d57614a4882614a8b565b613f6c565b8151158015614a6457506001600160a01b0384163b155b15614a845783604051639996b31560e01b8152600401610b269190615009565b5080613f6c565b805115614a9b5780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b60408051610140810182526000808252602082018190529181019190915260608101614ade614b0e565b81526020016000815260200160008152602001600081526020016000815260200160608152602001600081525090565b604051806040016040528060008152602001600081525090565b60405180608001604052806004906020820280368337509192915050565b6040518060400160405280614b59614bc8565b8152602001614b66614bc8565b905290565b508054600082556003029060005260206000209081019061085c9190614be6565b6040518060c001604052806006906020820280368337509192915050565b60405180602001604052806001906020820280368337509192915050565b60405180604001604052806002906020820280368337509192915050565b5b80821115614c195780546001600160a01b03199081168255600182018054909116905560006002820155600301614be7565b5090565b80356001600160401b0381168114614c3457600080fd5b919050565b600060208284031215614c4b57600080fd5b613f6c82614c1d565b80518252602090810151910152565b805180516001600160a01b03908116845260209182015116818401520151604082015260600190565b600081518084526020840193506020830160005b82811015614cc457614cb3868351614c63565b955060209190910190600101614ca0565b5093949350505050565b60208152614ce86020820183516001600160401b03169052565b60006020830151614d0460408401826001600160401b03169052565b5060408301516001600160a01b0381166060840152506060830151614d2c6080840182614c54565b50608083015160c083015260a083015160e083015260c083015161010083015260e0830151610120830152610100830151610160610140840152614d74610180840182614c8c565b90506101208401516101608401528091505092915050565b600060208284031215614d9e57600080fd5b5035919050565b6001600160401b0391909116815260200190565b634e487b7160e01b600052604160045260246000fd5b604080519081016001600160401b0381118282101715614df157614df1614db9565b60405290565b604051608081016001600160401b0381118282101715614df157614df1614db9565b604051601f8201601f191681016001600160401b0381118282101715614e4157614e41614db9565b604052919050565b600060208284031215614e5b57600080fd5b81356001600160401b03811115614e7157600080fd5b8201601f81018413614e8257600080fd5b80356001600160401b03811115614e9b57614e9b614db9565b614eae601f8201601f1916602001614e19565b818152856020838501011115614ec357600080fd5b81602084016020830137600091810160200191909152949350505050565b6001600160a01b038116811461085c57600080fd5b60006080828403121561426657600080fd5b60006001600160401b03821115614f2157614f21614db9565b5060051b60200190565b600082601f830112614f3c57600080fd5b8135614f4f614f4a82614f08565b614e19565b8082825260208201915060208360051b860101925085831115614f7157600080fd5b602085015b83811015614f9557614f8781614c1d565b835260209283019201614f76565b5095945050505050565b60008060008060e08587031215614fb557600080fd5b8435614fc081614ee1565b935060208501359250614fd68660408701614ef6565b915060c08501356001600160401b03811115614ff157600080fd5b614ffd87828801614f2b565b91505092959194509250565b6001600160a01b0391909116815260200190565b604081016124418284614c54565b60008060008084860361010081121561504357600080fd5b604081121561505157600080fd5b50849350604084013592506150698660608601614ef6565b915060e08501356001600160401b03811115614ff157600080fd5b6040808252835190820181905260009060208501906060840190835b818110156150c75783516001600160401b03168352602093840193909201916001016150a0565b50508381036020808601919091528551808352918101925085019060005b8181101561510e576150f8848451614c54565b60409390930192602092909201916001016150e5565b50919695505050505050565b60006040828403121561512c57600080fd5b615134614dcf565b823581526020928301359281019290925250919050565b60006080828403121561515d57600080fd5b615165614df7565b8235815260208084013590820152604080840135908201526060928301359281019290925250919050565b600082601f8301126151a157600080fd5b81356151af614f4a82614f08565b808282526020820191506020606084028601019250858311156151d157600080fd5b602085015b83811015614f955780870360608112156151ef57600080fd5b6151f7614dcf565b604082121561520557600080fd5b61520d614dcf565b9150823561521a81614ee1565b8252602083013561522a81614ee1565b602083810191909152918152604083013581830152845292909201916060016151d6565b60008060008084860361016081121561526657600080fd5b615270878761511a565b945061527f876040880161514b565b9350608060bf198201121561529357600080fd5b5061529c614df7565b60c0860135815260e08601356020820152610100860135604082015261012086013561ffff811681146152ce57600080fd5b606082015291506101408501356001600160401b038111156152ef57600080fd5b614ffd87828801615190565b6000806020838503121561530e57600080fd5b82356001600160401b0381111561532457600080fd5b8301601f8101851361533557600080fd5b80356001600160401b0381111561534b57600080fd5b8560208260051b840101111561536057600080fd5b6020919091019590945092505050565b60006020828403121561538257600080fd5b8135613f6c81614ee1565b918252602082015260400190565b600080600080600080600060e0888a0312156153b657600080fd5b87356153c181614ee1565b965060208801356153d181614ee1565b96999698505050506040850135946060810135946080820135945060a0820135935060c0909101359150565b600080600080610100858703121561541457600080fd5b61541e868661511a565b935061542d866040870161514b565b925060c085013561543d81614ee1565b9396929550929360e00135925050565b634e487b7160e01b600052601160045260246000fd5b80820281158282048414176124415761244161544d565b634e487b7160e01b600052601260045260246000fd5b60008261549f5761549f61547a565b500490565b818103818111156124415761244161544d565b6000608082840312156154c957600080fd5b613f6c838361514b565b6000604082840312156154e557600080fd5b613f6c838361511a565b60005b8381101561550a5781810151838201526020016154f2565b50506000910152565b600082516155258184602087016154ef565b9190910192915050565b6001600160401b039390931683526020830191909152604082015260600190565b6001600160401b038316815260608101613f6c602083018480358252602090810135910152565b93845260208401929092526040830152606082015260800190565b808201808211156124415761244161544d565b634e487b7160e01b600052603260045260246000fd5b6001600160a01b0385168152600061010082016155db6020840187614c54565b8451606084015260208501516080840152604085015160a084015261ffff60608601511660c084015261010060e08401528084518083526101208501915060208601925060005b8181101561564657615635838551614c63565b602094909401939250600101615622565b509098975050505050505050565b6001600160a01b038316815260608101613f6c6020830184614c54565b60008235607e1983360301811261552557600080fd5b6000808335601e1984360301811261569e57600080fd5b8301803591506001600160401b038211156156b857600080fd5b6020019150606081023603821315612ae657600080fd5b80546001600160a01b0319166001600160a01b0392909216919091179055565b81356156fa81614ee1565b61570481836156cf565b50602082013561571381614ee1565b61572081600184016156cf565b50604082013560028201555050565b6001600160401b0381811683821601908111156124415761244161544d565b6001600160a01b038416815260208101839052608081016132a46040830184614c54565b6000600182016157845761578461544d565b5060010190565b60006001600160401b0382166002600160401b031981016157ae576157ae61544d565b60010192915050565b6001600160401b039290921682526001600160a01b0316602082015260400190565b600084516157eb8184602089016154ef565b919091019283525060601b6001600160601b0319166020820152603401919050565b600060ff821660ff81036157ae576157ae61544d565b634e487b7160e01b600052600160045260246000fd5b6000826158485761584861547a565b500690565b60006020828403121561585f57600080fd5b81518015158114613f6c57600080fdfe30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd476e0956cda88cad152e89927e53611735b61a5c762d1428573c6931b0a5efcb01424c535f5349475f545259414e44494e4352454d454e545f4c49515549444154452b149d40ceb8aaae81be18991be06ac3b5b4c5e559dbefa33267e6dc24a138e5a164736f6c634300081a000a", + "bytecode": "0x6080604052348015600f57600080fd5b506157e98061001f6000396000f3fe608060405234801561001057600080fd5b50600436106102e05760003560e01c8063040f9853146102e5578063095f14041461030e5780630962ef79146103255780630bd1b4191461033a57806317f33a7d146103435780631f4c49301461036a5780631f744b541461037d578063245fc1831461038557806334fde37614610398578063372500ab146103a05780633f4ba83a146103a857806343039d90146103b0578063538d2709146103c3578063544736e6146103cc57806356d399e8146103e957806358e93308146103f25780635c975abb146103fb578063623b94221461040357806362f1fbc21461040c57806365ca819d146104165780636b348ab41461044a578063715018a61461045357806379ba50971461045b5780637a6d4065146104635780637c89d2f0146104765780638328b6101461049b5780638456cb59146104ae57806386e76685146104b657806386fa5063146104bf578063879d0f0c146104c85780638a2209e6146104d05780638a399481146104e55780638da5cb5b146104ee5780639156ac80146104f657806395055ffd146104ff5780639592d424146105125780639c80ebee1461051b5780639cebc4741461040c5780639e3b372d14610523578063a44285a51461052c578063ab0122ae14610535578063abf2c50314610548578063ae6c60631461055e578063b13aaebd14610567578063b687f85c14610570578063bc7efecc14610578578063be9a65551461058b578063c0ebedab14610593578063c4f56d9a146105bc578063d5a81254146105cf578063d84f0bf8146105d8578063d9054b09146105e1578063da1d5435146105f4578063dd644d74146105fd578063e30c397814610606578063e58598131461060e578063eb82031214610621578063edbf4ac214610656578063ee94e41214610669578063f2fde38b14610672578063f324c8eb14610685578063f88d658b14610698578063f907f5fc146106ab578063ffd9a86d146106be575b600080fd5b6102f86102f3366004614afd565b6106d1565b6040516103059190614b92565b60405180910390f35b610317600d5481565b604051908152602001610305565b610338610333366004614c50565b610802565b005b610317600f5481565b60015461035d90600160a01b90046001600160401b031681565b6040516103059190614c69565b610338610378366004614c7d565b61080f565b610338610aba565b610338610393366004614c50565b610b8a565b610317610bef565b610338610c26565b610338610c54565b6103386103be366004614c50565b610c66565b61031760075481565b6000546103d99060ff1681565b6040519015158152602001610305565b610317600b5481565b610317600e5481565b6103d9610cc4565b61031760155481565b61031762278d0081565b61035d610424366004614d82565b80516020818301810180516012825292820191909301209152546001600160401b031681565b610317600a5481565b610338610cd9565b610338610ceb565b610338610471366004614ed8565b610d27565b60005461048e9061010090046001600160a01b031681565b6040516103059190614f42565b6103386104a9366004614c50565b610ec5565b610338610f22565b61031760045481565b610317600c5481565b610317601481565b6104d8610f32565b6040516103059190614f56565b61031760035481565b61048e610f55565b61031760195481565b61033861050d366004614f64565b610f70565b61031760025481565b61035d600081565b61031760095481565b61031760175481565b610338610543366004614afd565b610ff9565b6105506110c2565b604051610305929190614fbd565b61031760065481565b61031760185481565b610317600481565b610338610586366004615187565b61124d565b610338611576565b61035d6105a1366004614c50565b601b602052600090815260409020546001600160401b031681565b6103386105ca366004614f64565b61158d565b61031760055481565b61031760085481565b6103386105ef366004614afd565b611742565b61031760165481565b610317601a5481565b61048e611754565b61033861061c366004615234565b61175f565b61064861062f366004615260565b6011602052600090815260409020805460019091015482565b60405161030592919061527d565b61033861066436600461528b565b611812565b610317610e1081565b610338610680366004615260565b611b80565b610338610693366004614c50565b611bf1565b6103386106a6366004614c50565b611c4f565b60015461048e906001600160a01b031681565b6103386106cc3660046152ed565b611cad565b6106d9614978565b6001600160401b03808316600090815260106020908152604080832081516101408101835281548087168252600160401b90049095168584015260018101546001600160a01b0316858301528151808301835260028201548152600382015481850152606086015260048101546080860152600581015460a0860152600681015460c0860152600781015460e0860152600881018054835181860281018601909452808452919461010087019491929184015b828210156107e9576000848152602090819020604080516080810182526003860290920180546001600160a01b0390811692840192835260018083015490911660608501529183526002015482840152908352909201910161078c565b5050505081526020016009820154815250509050919050565b61080c3382611cbf565b50565b610817611dfa565b60005460ff161561083b57604051630a35d5c360e31b815260040160405180910390fd5b8060005b81811015610aac573684848381811061085a5761085a61533d565b905060200281019061086c9190615353565b9050600061087d6080830183615373565b90501161089d57604051631a10033d60e11b815260040160405180910390fd5b600c546108ad6080830183615373565b905011156108ce5760405163226c2d8360e21b815260040160405180910390fd5b6000806108ed6108e3368590038501856153bb565b8460400135611e2c565b600b54600782015590925090506109076080840184615373565b60008181106109185761091861533d565b61092e9260206060909202019081019150615260565b6001820180546001600160a01b0319166001600160a01b03929092169190911790556060830135600482015560008061096a6080860186615373565b9050905060005b81811015610a1857366109876080880188615373565b838181106109975761099761533d565b90506060020190508060400135846109af91906153ed565b935060006109c06020830183615260565b6001600160a01b0316036109e75760405163e99d5ac560e01b815260040160405180910390fd5b60088501805460018101825560009182526020909120829160030201610a0d8282615420565b505050600101610971565b50600b548214610a4a57600b54826040516367d22bd960e01b8152600401610a4192919061527d565b60405180910390fd5b604080518635815260208088013590820152868201358183015290516001600160401b038616917f34be2fa283e1d5eb453459898f160448257aa1ef0f7dd2c3a3b55c45d1af768f919081900360600190a2600186019550505050505061083f565b50610ab5612160565b505050565b600060028190558052601060205260008051602061577c833981519152546001600160401b03165b6001600160401b0381161561080c576001600160401b0381166000908152601060205260408120600254909103610b285760028101546013556003810154601455610b70565b6040805180820182526013548152601454602080830191909152825180840190935260028401548352600384015490830152610b639161218c565b8051601355602001516014555b546002805460010190556001600160401b03169050610ae2565b610b92611dfa565b60008111610bb357604051630ef6cf4560e41b815260040160405180910390fd5b60188190556040518181527fa83e79dc52a438d552d0631ce4fbfe7dec7656d10398a2849f352a6ab4f0163b906020015b60405180910390a150565b60008060646002546002610c039190615460565b610c0d919061548d565b905060148111610c1e576014610c20565b805b91505090565b336000908152601160205260408120600181015490549091610c4883836154a1565b9050610ab53382611cbf565b610c5c611dfa565b610c64612238565b565b610c6e611dfa565b60008111610c8f57604051630ef6cf4560e41b815260040160405180910390fd5b60048190556040518181527f0ac8ee09138dfaf5e3ebe4cb4fd42dd1a0695535a530171223fb5066f52e0e3b90602001610be4565b600080610ccf612284565b5460ff1692915050565b610ce1611dfa565b610c6460006122a8565b3380610cf5611754565b6001600160a01b031614610d1e578060405163118cdaa760e01b8152600401610a419190614f42565b61080c816122a8565b610d2f6122cf565b60005460ff16610d525760405163348b55eb60e21b815260040160405180910390fd5b8051600354811115610d975780600254610d6c91906154a1565b600354600254610d7c91906154a1565b604051635eee4a3560e01b8152600401610a4192919061527d565b6001600160a01b038516610dbe5760405163e99d5ac560e01b815260040160405180910390fd5b6001600160a01b0385166000908152601160205260409020548411610df6576040516333938e6360e01b815260040160405180910390fd5b6007546040805160208101929092526001600160601b0319606088901b16908201526054810185905260009060740160405160208183030381529060405290506000610e4482600a546122f5565b9050610e5f84610e59368890038801886154b4565b836123b5565b50506001600160a01b0385166000818152601160205260409081902080549087905590519091907f95390641529563dbfb446535fa996c5ac3be00f90f5705b3abda59a4467b797f90610eb5908890859061527d565b60405180910390a2505050505050565b610ecd611dfa565b60008111610eee57604051630ef6cf4560e41b815260040160405180910390fd5b600b8190556040518181527e6b7a1ea14ff2794527a64af37d55a2040e351f8b4c1adcdc9aea80d64e042990602001610be4565b610f2a611dfa565b610c646124f7565b610f3a6149d2565b50604080518082019091526013548152601454602082015290565b600080610f6061253e565b546001600160a01b031692915050565b610f786122cf565b60005460ff16610f9b5760405163348b55eb60e21b815260040160405180910390fd5b8051600354811115610fb55780600254610d6c91906154a1565b6000610fc686868660085487612562565b506001600160401b038116600090815260106020526040902060070154909150610ff190829061280f565b505050505050565b6110016122cf565b60005460ff166110245760405163348b55eb60e21b815260040160405180910390fd5b6001600160401b038116600090815260106020526040812060050154908190036110635781604051630cf4827360e31b8152600401610a419190614c69565b600061107262278d00836153ed565b90508042101561109b57828142604051637a54a45360e11b8152600401610a41939291906154d0565b6001600160401b038316600090815260106020526040902060070154610ab590849061280f565b6060806002546001600160401b038111156110df576110df614cf2565b604051908082528060200260200182016040528015611108578160200160208202803683370190505b5091506002546001600160401b0381111561112557611125614cf2565b60405190808252806020026020018201604052801561115e57816020015b61114b6149d2565b8152602001906001900390816111435790505b506000808052601060205260008051602061577c833981519152549192506001600160401b03909116905b6001600160401b03821615611247576001600160401b038083166000908152601060205260409020855190918491879185169081106111ca576111ca61533d565b60200260200101906001600160401b031690816001600160401b031681525050806002016040518060400160405290816000820154815260200160018201548152505084836001600160401b0316815181106112285761122861533d565b6020908102919091010152546001600160401b03169150600101611189565b50509091565b6112556122cf565b60005460ff166112785760405163348b55eb60e21b815260040160405180910390fd5b8051600c5481111561129d5760405163226c2d8360e21b815260040160405180910390fd5b8015611311576000805b828110156112e2578381815181106112c1576112c161533d565b602002602001015160200151826112d891906153ed565b91506001016112a7565b50600b54811461130b57600b54816040516367d22bd960e01b8152600401610a4192919061527d565b506113a6565b60408051600180825281830190925290816020015b604080516080810182526000918101828152606082018390528152602081019190915281526020019060019003908161132657505060408051608081018252339181018281526060820192909252908152600b54602082015281519193509083906000906113965761139661533d565b6020026020010181905250815190505b600060126113b3876128b2565b6040516113c09190615515565b908152604051908190036020019020546001600160401b0316905080156113fc578060405163459c639360e01b8152600401610a419190614c69565b6000836000815181106114115761141161533d565b60200260200101516000015160000151905061143387878388600001516128db565b600080611444898860000151611e2c565b600b5460078201556001810180546001600160a01b0319166001600160a01b038716179055909250905060005b858110156114f8578160080187828151811061148f5761148f61533d565b60209081029190910181015182546001808201855560009485529383902082518051600390930290910180546001600160a01b03199081166001600160a01b03948516178255918501518187018054909316931692909217905591015160029091015501611471565b50611501612160565b816001600160401b03167f533c16cb4158fe7c77021b90e9290bbefa457dd392bd8abc1eab59747834fd5b848b8a8a6040516115409493929190615527565b60405180910390a261156b600060019054906101000a90046001600160a01b03163330600b546129c5565b505050505050505050565b61157e611dfa565b6000805460ff19166001179055565b6115956122cf565b60005460ff166115b85760405163348b55eb60e21b815260040160405180910390fd5b80516003548111156115d25780600254610d6c91906154a1565b6000806115e487878760095488612562565b91509150816001600160401b03167f0bfb12191b00293af29126b1c5489f8daeb4a4af82db2960b7f8353c3105cd7c8260400151836060015160405161162b9291906155c0565b60405180910390a26000600f54600d54600e5461164891906153ed565b61165291906153ed565b905060008260e001519050600082600d548361166e9190615460565b611678919061548d565b90506000600e548361168a9190615460565b156116c457836001600e54856116a09190615460565b6116aa91906154a1565b6116b4919061548d565b6116bf9060016153ed565b6116c7565b60005b90506116e786826116d885876154a1565b6116e291906154a1565b61280f565b600d541561170b5760005461170b9061010090046001600160a01b03163384612a2c565b600e541561173557600054600154611735916001600160a01b036101009091048116911683612a2c565b5050505050505050505050565b61174a6122cf565b61080c8133612a5d565b600080610f60612c2d565b611767611dfa565b6000811161178857604051630ef6cf4560e41b815260040160405180910390fd5b8061179383856153ed565b61179e906003615460565b11156117bd57604051630b06449d60e41b815260040160405180910390fd5b600d839055600e829055600f81905560408051848152602081018490529081018290527f0f69a2a87c90cdbb1a6a46bbb4870f2edfbd516244285ab6ffed6460ac2c6a839060600160405180910390a1505050565b600061181c612c51565b805490915060ff600160401b82041615906001600160401b03166000811580156118435750825b90506000826001600160401b0316600114801561185f5750303b155b90508115801561186d575080155b1561188b5760405163f92ee8a960e01b815260040160405180910390fd5b84546001600160401b031916600117855583156118b457845460ff60401b1916600160401b1785555b60018610156118d657604051630ef6cf4560e41b815260040160405180910390fd5b856118e1888a6153ed565b6118ec906003615460565b111561190b57604051630b06449d60e41b815260040160405180910390fd5b6000805460ff19168155600281905560035561012c60045560408051808201909152601b81527a0424c535f5349475f545259414e44494e4352454d454e545f504f5602c1b602082015261195e90612c75565b60065560408051808201909152601e81527f424c535f5349475f545259414e44494e4352454d454e545f5245574152440000602082015261199e90612c75565b60075560408051808201909152601c81527b109314d7d4d251d7d51496505391125390d4915351539517d156125560221b60208201526119dd90612c75565b600881905550611a0460405180606001604052806021815260200161579c60219139612c75565b600955604080518082019091526019815278424c535f5349475f484153485f544f5f4649454c445f54414760381b6020820152611a4090612c75565b600a5561025860055566038d7ea4c6800060175561a8c060185560006019819055601a8190558054610100600160a81b0319166101006001600160a01b038f811691909102919091178255600180546001600160a01b031916918e16919091178155600b8c9055600c8b9055600d8a9055600e899055600f889055611ac591906155dd565b600180546001600160401b0392909216600160a01b02600160a01b600160e01b031990921691909117905560008052601060205260008051602061577c83398151915280546001600160801b0319169055611b1f33612ca9565b611b27612cba565b8315611b7257845460ff60401b191685556040517fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d290611b6990600190614c69565b60405180910390a15b505050505050505050505050565b611b88611dfa565b6000611b92612c2d565b80546001600160a01b0319166001600160a01b0384169081178255909150611bb8610f55565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b611bf9611dfa565b60008111611c1a57604051630ef6cf4560e41b815260040160405180910390fd5b60178190556040518181527fe604909b918af52702fa14cd9b5a8870e5bd66da3163365671e583f705fd546390602001610be4565b611c57611dfa565b60008111611c7857604051630ef6cf4560e41b815260040160405180910390fd5b60058190556040518181527ffbbc3d0a51e101ce4a7fda20e6e4eb0e230bf7de27ee2e3683fc8539e376639590602001610be4565b611cb9848484846128db565b50505050565b6001600160a01b0382166000908152601160205260408120600181015490549091611cea83836154a1565b905080841115611d0d5760405163363cc6b360e21b815260040160405180910390fd5b600060185442611d1d919061548d565b9050601a54811115611d3457601a81905560006019555b8460196000828254611d4691906153ed565b90915550506017546019541115611d7057604051638c10944b60e01b815260040160405180910390fd5b6001600160a01b03861660009081526011602052604081206001018054879290611d9b9084906153ed565b90915550506040518581526001600160a01b038716907ffc30cddea38e2bf4d6ea7d3f9ed3b6ad7f176419f4963bd81318067a4aee73fe9060200160405180910390a2600054610ff19061010090046001600160a01b03168787612a2c565b33611e03610f55565b6001600160a01b031614610c64573360405163118cdaa760e01b8152600401610a419190614f42565b815160009081901580611e4157506020840151155b15611e5f5760405163139b722760e31b815260040160405180910390fd5b82600003611e8057604051634eb8f11f60e01b815260040160405180910390fd5b6000611e8b856128b2565b905060006001600160401b0316601282604051611ea89190615515565b908152604051908190036020019020546001600160401b031614611f0857601281604051611ed69190615515565b9081526040519081900360200181205463459c639360e01b8252610a41916001600160401b0390911690600401614c69565b6000848152601b60205260409020546001600160401b031615611f58576000848152601b602052604090819020549051630cb4c72160e21b8152610a41916001600160401b031690600401614c69565b60005460ff1615611fbd57436015541015611f77574360155560006016555b60168054906000611f87836155fc565b91905055506000611f96610bef565b9050806016541115611fbb5760405163689f6e7560e11b815260040160405180910390fd5b505b60018054600160a01b90046001600160401b0316906014611fdd83615615565b91906101000a8154816001600160401b0302191690836001600160401b031602179055509250600260008154612012906155fc565b909155506001600160401b03838116600081815260106020908152604080832060008051602061577c833981519152805482546001600160801b031916600160401b918290049098168082029890981783558154600160401b600160801b03191690870217815586855282852080546001600160401b031990811688179091558c5160028401558c8501516003840155426004840155600983018c90558b8652601b90945293829020805490931690941790915551919450919085906012906120dc908690615515565b90815260405190819003602001902080546001600160401b03929092166001600160401b03199092169190911790556002546001036121275786516013556020870151601455612155565b6040805180820190915260135481526014546020820152612148908861218c565b8051601355602001516014555b5050505b9250929050565b60006003600254612171919061548d565b905060045481116121825780612186565b6004545b60035550565b6121946149d2565b61219c6149ec565b835181526020808501518183015283516040808401919091529084015160608301526000908360808460066107d05a03fa9050806122305760405162461bcd60e51b815260206004820152602b60248201527f43616c6c20746f20707265636f6d70696c656420636f6e747261637420666f7260448201526a081859190819985a5b195960aa1b6064820152608401610a41565b505092915050565b612240612cca565b600061224a612284565b805460ff1916815590507f5db9ee0a495bf2e6ff9c91a7834c1ba4fdd244a5e8aa4e537bd38aeae4b073aa335b604051610be49190614f42565b7fcd5ed15c6e187e77e9aee88184c21f4f2182ab5827cb3b7e07fbedcd63f0330090565b60006122b2612c2d565b80546001600160a01b031916815590506122cb82612cef565b5050565b6122d7610cc4565b15610c645760405163d93c066560e01b815260040160405180910390fd5b6122fd614a0a565b60006123098484612d4b565b9050600080600080612376856000015160016002811061232b5761232b61533d565b602002015186516000602002015187602001516001600281106123505761235061533d565b6020020151886020015160006002811061236c5761236c61533d565b6020020151612f1c565b60408051608081018252808201948552606081019590955292845282518084019093528252602082810191909152820152955050505050505b92915050565b6123bd6149d2565b835160005b818110156124435760008682815181106123de576123de61533d565b602002602001015190506124388460106000846001600160401b03166001600160401b031681526020019081526020016000206002016040518060400160405290816000820154815260200160018201548152505061218c565b9350506001016123c2565b50604080518082019091526013548152601454602082015261246d9061246884612fac565b61218c565b60408051608081018252602080880151828401908152885160608085019190915290835283518085018552908901518152928801518382015281019190915290925060006124ba84612fac565b90506124cf6124c7613036565b838388613057565b6124ee5783604051634fb09a2160e11b8152600401610a419190614f56565b50505050505050565b6124ff6122cf565b6000612509612284565b805460ff1916600117815590507f62e78cea01bee320cd4e420270b5ea74000d11b0c9f74754ebdbfc544b05a2586122773390565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930090565b600061256c614978565b6000612585612580368a90038a018a6153bb565b6128b2565b90506012816040516125979190615515565b908152604051908190036020019020546001600160401b0316925060008390036125d65787604051631b8ac14360e01b8152600401610a419190615641565b6125df87613158565b15612603578287426040516337030b8b60e01b8152600401610a41939291906154d0565b6001600160401b03808416600090815260106020908152604080832081516101408101835281548087168252600160401b90049095168584015260018101546001600160a01b0316858301528151808301835260028201548152600382015481850152606086015260048101546080860152600581015460a0860152600681015460c0860152600781015460e0860152600881018054835181860281018601909452808452919461010087019491929184015b82821015612713576000848152602090819020604080516080810182526003860290920180546001600160a01b039081169284019283526001808301549091166060850152918352600201548284015290835290920191016126b6565b5050509082525060099190910154602090910152606081015151909250883514158061274b5750816060015160200151886020013514155b1561276d57828860405163c43283b560e01b8152600401610a41929190615658565b608082015161277f90611c20906153ed565b4210156127a8576080820151604051633acb74ef60e11b8152610a4191859142906004016154d0565b6040805160208082018890528a35828401528a0135606082015260808082018a90528251808303909101815260a0909101909152600a546000906127ed9083906122f5565b905061280286610e59368b90038b018b6154b4565b5050509550959350505050565b6001600160401b0382166000908152601060209081526040918290206001810154835180850190945260028201548452600390910154918301919091526001600160a01b03169061285f84613170565b612867612160565b836001600160401b03167f24c4411329b949fad2eba6f79a8ba090a08eac1af4b56c3a2f5a282ed45e233e8385846040516128a49392919061567f565b60405180910390a250505050565b6060816040516020016128c59190614f56565b6040516020818303038152906040529050919050565b600060065485600001518660200151858560405160200161292c95949392919094855260208501939093526040840191909152606090811b6001600160601b03191690830152607482015260940190565b6040516020818303038152906040529050600061294b82600a546122f5565b60408051608081018252602080890151828401908152895160608085019190915290835283518085018552908a0151815292890151838201528101919091529091506129a8612998613036565b826129a28a612fac565b85613057565b6124ee5760405163cf006ab760e01b815260040160405180910390fd5b6040516001600160a01b038481166024830152838116604483015260648201839052611cb99186918216906323b872dd906084015b604051602081830303815290604052915060e01b6020820180516001600160e01b038381831617835250505050613479565b6040516001600160a01b03838116602483015260448201839052610ab591859182169063a9059cbb906064016129fa565b60005460ff16612a805760405163348b55eb60e21b815260040160405180910390fd5b6001600160401b03821660009081526010602052604081206008810154829190825b81811015612b13576000836008018281548110612ac157612ac161533d565b6000918252602090912060039091020180549091506001600160a01b03808916911603612b0a576007840154600282015460019750612b01906004615460565b10945050612b13565b50600101612aa2565b5083612b36578585604051635bf2837760e11b8152600401610a419291906156a3565b8160050154600003612b8d57828015612b60575062278d008260040154612b5d91906153ed565b42105b15612b82578585604051633826feb560e01b8152600401610a419291906156a3565b426005830155612bcc565b6000610e108360060154612ba191906153ed565b905080421015612bca57868142604051637a54a45360e11b8152600401610a41939291906154d0565b505b426006830155604080516001600160a01b0387168152600284015460208201526003840154918101919091526001600160401b038716907f8600c0145511b317ae0189933fa71eb57a32d74891804c7993ef74ec63a5e80490606001610eb5565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0090565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0090565b6000814630604051602001612c8c939291906156c5565b604051602081830303815290604052805190602001209050919050565b612cb16134d3565b61080c816134f8565b612cc26134d3565b610c6461352a565b612cd2610cc4565b610c6457604051638dfc202b60e01b815260040160405180910390fd5b6000612cf961253e565b80546001600160a01b038481166001600160a01b031983168117845560405193945091169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a3505050565b612d53614a0a565b600080600080600087516001612d6991906153ed565b6001600160401b03811115612d8057612d80614cf2565b6040519080825280601f01601f191660200182016040528015612daa576020820181803683370190505b50885190915060005b81811015612e0957898181518110612dcd57612dcd61533d565b602001015160f81c60f81b838281518110612dea57612dea61533d565b60200101906001600160f81b031916908160001a905350600101612db3565b5060005b8060f81b8360018551612e2091906154a1565b81518110612e3057612e3061533d565b60200101906001600160f81b031916908160001a9053506000612e53848b613547565b91995097509050600080612e678a8a61363f565b91509150600080612e7884846136b2565b9150915081600014158015612e8c57508015155b15612ec9578415612ea757612ea18282613821565b90925090505b90985096508787612eba8c8c8484613866565b15612ec9575050505050612ee1565b50505050508080612ed9906156f9565b915050612e0d565b505060408051608081018252808201958652606081019690965293855250825180840190935282526020808301919091528201529392505050565b600080600080612f2e8888888861387d565b612f3a57612f3a61570f565b6040805160c081018252898152602081018990529081018790526060810186905260016080820152600060a0820152612f7281613920565b8051602082015160408301516060840151608085015160a0860151959650612f9995613ad2565b929c919b50995090975095505050505050565b612fb46149d2565b8151158015612fc557506020820151155b15612fe3575050604080518082019091526000808252602082015290565b60405180604001604052808360000151815260200160008051602061575c83398151915284602001516130169190615725565b61302e9060008051602061575c8339815191526154a1565b905292915050565b61303e6149d2565b5060408051808201909152600181526002602082015290565b60408051600280825260608201909252600091829190816020015b61307a6149d2565b8152602001906001900390816130725750506040805160028082526060820190925291925060009190602082015b6130b0614a0a565b8152602001906001900390816130a857905050905086826000815181106130d9576130d961533d565b602002602001018190525084826001815181106130f8576130f861533d565b602002602001018190525085816000815181106131175761311761533d565b602002602001018190525083816001815181106131365761313661533d565b602002602001018190525061314b8282613b1c565b925050505b949350505050565b60006005548261316891906153ed565b421192915050565b600060025411613193576040516363cd35d560e11b815260040160405180910390fd5b6001600160401b0381166131ba57604051631d58f8cb60e11b815260040160405180910390fd5b6001600160401b03808216600090815260106020908152604080832081516101408101835281548087168252600160401b90049095168584015260018101546001600160a01b0316858301528151808301835260028201548152600382015481850152606086015260048101546080860152600581015460a0860152600681015460c0860152600781015460e086015260088101805483518186028101860190945280845294959491936101008601939290879084015b828210156132ce576000848152602090819020604080516080810182526003860290920180546001600160a01b03908116928401928352600180830154909116606085015291835260020154828401529083529092019101613271565b5050509082525060099190910154602091820152818101805183516001600160401b0390811660009081526010855260408082208054600160401b600160801b031916600160401b9585169590950294909417909355855193518216815282902080546001600160401b0319169390911692909217909155805180820190915260135481526014549181019190915260608201519192506133729161246890612fac565b805160135560200151601455606081015160009061338f906128b2565b6101208301516000908152601b60205260409081902080546001600160401b0319169055519091506012906133c5908390615515565b908152604080516020928190038301902080546001600160401b03191690556001600160401b03851660009081526010909252812080546001600160801b03191681556001810180546001600160a01b0319169055600281018290556003810182905560048101829055600581018290556006810182905560078101829055906134526008830182614a2f565b6009820160009055505060016002600082825461346f91906154a1565b9091555050505050565b600061348e6001600160a01b03841683613e22565b905080516000141580156134b35750808060200190518101906134b19190615739565b155b15610ab55782604051635274afe760e01b8152600401610a419190614f42565b6134db613e37565b610c6457604051631afcd79f60e31b815260040160405180910390fd5b6135006134d3565b6001600160a01b038116610d1e576000604051631e4fbdf760e01b8152600401610a419190614f42565b6135326134d3565b600061353c612284565b805460ff1916905550565b600080600080600080600061357d898960405160200161356991815260200190565b604051602081830303815290604052613e51565b935093509350935060405160308152602080820152602060408201528460608201528360808201526001609082015260008051602061575c83398151915260b082015260208160d0836005600019fa6135d557600080fd5b805197505060405160308152602080820152836050820152602060408201528260708201526001609082015260008051602061575c83398151915260b082015260208160d0836005600019fa61362a57600080fd5b51969996985060019081161496505050505050565b60008060008060008061365488888a8a613f9a565b909250905061366582828a8a613f9a565b90925090506136a382826000805160206157bd8339815191527e9713b03af0fed4cd2cafadeed8fdf4a74fa084e52d1852e4a2bd0685c315d261400b565b90999098509650505050505050565b6000806000806000856000036136f4576136cb8761403f565b909350905080156136e55782600094509450505050612159565b60008394509450505050612159565b60008051602061575c833981519152878809925060008051602061575c833981519152868709915060008051602061575c83398151915282840892506137398361403f565b9093509050806137525760008094509450505050612159565b60008051602061575c833981519152838808915061376f826140d6565b9150600061377c8361403f565b92509050816137cd5761379e888560008051602061575c833981519152614130565b92506137a9836140d6565b92506137b48361403f565b92509050816137cd576000809550955050505050612159565b8060008051602061575c83398151915282830893506137fa8460008051602061575c833981519152614154565b9350600060008051602061575c833981519152858a09919a91995090975050505050505050565b6000808061383d8560008051602061575c8339815191526154a1565b905060006138598560008051602061575c8339815191526154a1565b9196919550909350505050565b60006138748585858561387d565b95945050505050565b600080600080600061389187878989613f9a565b90945092506138a289898181613f9a565b90925090506138b382828b8b613f9a565b90925090506138c4848484846141a5565b909450925061390284846000805160206157bd8339815191527e9713b03af0fed4cd2cafadeed8fdf4a74fa084e52d1852e4a2bd0685c315d26141a5565b909450925083158015613913575082155b9998505050505050505050565b613928614a50565b613930614a50565b613938614a50565b613940614a50565b84516020860151604087015160608801516080890151613978946744e992b44a6909f1949093909290918b60055b60200201516141e7565b805160208201516040830151606084015160808501519497506139a4946002949392919089600561396e565b9150613a108360005b602002015184600160200201518560026020020151866003602002015187600460200201518860056020020151886000602002015189600160200201518a600260200201518b600360200201518c600460200201518d60055b602002015161426a565b9150613a1b826144f3565b9150613a26836144f3565b9050613a31816144f3565b9050613a3e8360006139ad565b9250613aa48360005b6020020151846001602002015185600260200201518660036020020151876004602002015188600560200201518760006020020151886001602002015189600260200201518a600360200201518b600460200201518c6005613a06565b9250613aaf856144f3565b9050613aba816144f3565b9050613ac5816144f3565b9050613874836000613a47565b600080600080600080613ae5888861460b565b9092509050613af68c8c8484613f9a565b9096509450613b078a8a8484613f9a565b969d959c509a50949850929650505050505050565b60008151835114613b2c57600080fd5b82516000613b3b826006615460565b90506000816001600160401b03811115613b5757613b57614cf2565b604051908082528060200260200182016040528015613b80578160200160208202803683370190505b50905060005b83811015613db157868181518110613ba057613ba061533d565b60200260200101516000015182826006613bba9190615460565b613bc59060006153ed565b81518110613bd557613bd561533d565b602002602001018181525050868181518110613bf357613bf361533d565b60200260200101516020015182826006613c0d9190615460565b613c189060016153ed565b81518110613c2857613c2861533d565b602002602001018181525050858181518110613c4657613c4661533d565b6020908102919091010151515182613c5f836006615460565b613c6a9060026153ed565b81518110613c7a57613c7a61533d565b602002602001018181525050858181518110613c9857613c9861533d565b60209081029190910181015151015182613cb3836006615460565b613cbe9060036153ed565b81518110613cce57613cce61533d565b602002602001018181525050858181518110613cec57613cec61533d565b602002602001015160200151600060028110613d0a57613d0a61533d565b602002015182613d1b836006615460565b613d269060046153ed565b81518110613d3657613d3661533d565b602002602001018181525050858181518110613d5457613d5461533d565b602002602001015160200151600160028110613d7257613d7261533d565b602002015182613d83836006615460565b613d8e9060056153ed565b81518110613d9e57613d9e61533d565b6020908102919091010152600101613b86565b50613dba614a6e565b60006020826020860260208601600060086107d05a03f1905080613e145760405162461bcd60e51b8152602060048201526011602482015270496e76616c6964205369676e617475726560781b6044820152606401610a41565b505115159695505050505050565b6060613e3083836000614696565b9392505050565b6000613e41612c51565b54600160401b900460ff16919050565b60008060008060ff85511115613e6657600080fd5b600060405160005b6088811015613e8557600082820152602001613e6e565b506088602060005b8a51811015613eae578a820151848401526020928301929182019101613e8d565b505060898951019050608081830153600201602060005b8951811015613ee65789820151848401526020928301929182019101613ec5565b5050608b88518a5101019050875181830153508751875101608c018120915050604051818152600160208201536021602060005b8951811015613f3b5789820151848401526020928301929182019101613f1a565b505050865187516021018201538651602201812095508582188152600260208201538651602201812094508482188152600360208201538651602201812093508382188152600460208201539551602201909520939692955090935050565b600080613fd860008051602061575c83398151915285880960008051602061575c83398151915285880960008051602061575c833981519152614130565b60008051602061575c8339815191528086880960008051602061575c833981519152868a09089150915094509492505050565b60008060008051602061575c83398151915284870860008051602061575c8339815191528487089150915094509492505050565b600080600060405160208152602080820152602060408201528460608201527f0c19139cb84c680a6e14116da060561765e05aa45a1c72a34f082305b61f3f52608082015260008051602061575c83398151915260a082015260208160c08360056107d05a03fa90519350905060008051602061575c83398151915283800984149150806140d05760009250600091505b50915091565b60006001821615156140e960028461548d565b9150801561412a5760008051602061575c833981519152600261411b60008051602061575c83398151915260016153ed565b614125919061548d565b830891505b50919050565b6000818061414057614140615477565b61414a84846154a1565b8508949350505050565b60008060405160208152602080820152602060408201528460608201526002840360808201528360a082015260208160c08360056107d05a03fa9051925090508061419e57600080fd5b5092915050565b6000806141c1868560008051602061575c833981519152614130565b6141da868560008051602061575c833981519152614130565b9150915094509492505050565b6141ef614a50565b871561425f576001881615614230578051602082015160408301516060840151608085015160a086015161422d9594939291908d8d8d8d8d8d61426a565b90505b61423e878787878787614733565b949b5092995090975095509350915061425860028961548d565b97506141ef565b979650505050505050565b614272614a50565b8815801561427e575087155b156142c0578686868686868660005b60a089019290925260808801929092526060870192909252604086019290925260208581019390935290910201526144e3565b821580156142cc575081155b156142df578c8c8c8c8c8c86600061428d565b6142eb85858b8b613f9a565b90955093506142fc8b8b8585613f9a565b6060830152604082015261431287878b8b613f9a565b90975095506143238d8d8585613f9a565b60a08301526080820181905287148015614340575060a081015186145b156143855760408101518514801561435b5750606081015184145b156143765761436e8d8d8d8d8d8d614733565b86600061428d565b6001600081818080868161428d565b61439189898585613f9a565b90935091506143b1858583600260200201518460035b60200201516141a5565b909d509b506143cb878783600460200201518460056143a7565b909b5099506143dc8b8b8181613f9a565b90995097506143fc898983600460200201518460055b6020020151613f9a565b909550935061440d89898d8d613f9a565b909950975061441e89898585613f9a565b60a083015260808201526144348d8d8181613f9a565b909750955061444587878585613f9a565b909750955061445687878b8b6141a5565b9097509550614467858560026148a2565b9093509150614478878785856141a5565b90975095506144898b8b8989613f9a565b6020830152815261449c858589896141a5565b909b5099506144ad8d8d8d8d613f9a565b909b5099506144c7898983600260200201518460036143f2565b909d509b506144d88b8b8f8f6141a5565b606083015260408201525b9c9b505050505050505050505050565b6144fb614a50565b815161450f908360015b60200201516148d5565b60208301528152604082015161452790836003614505565b60608301526040820152608082015161454290836005614505565b60a083015260808201528051602082015161459f91907f2fb347984f7911f74c0bec3cf559b143b78cc310c2c3330c99e39557176f553d7f16c9e55061ebae204ba4cc8bd75a079432ae2a1d0b7c9dce1665d51c640fcba2613f9a565b60208301528152604081015160608201516145fc91907f063cf305489af5dcdc5ec698b6e2f9b9dbaae0eda9c95998dc54014671a0135a7f07c03cbcac41049a0704b5a7ec796f2b21807dc98fa25bd282d37f632623b0e3613f9a565b60608301526040820152919050565b6000808061464c60008051602061575c8339815191528087880960008051602061575c8339815191528788090860008051602061575c833981519152614154565b905060008051602061575c83398151915281860960008051602061575c83398151915282860961468a9060008051602061575c8339815191526154a1565b92509250509250929050565b6060814710156146bb573060405163cd78605960e01b8152600401610a419190614f42565b600080856001600160a01b031684866040516146d79190615515565b60006040518083038185875af1925050503d8060008114614714576040519150601f19603f3d011682016040523d82523d6000602084013e614719565b606091505b50915091506147298683836148fc565b9695505050505050565b6000806000806000806147488c8c60036148a2565b909650945061475986868e8e613f9a565b909650945061476a8a8a8a8a613f9a565b909850965061477b8c8c8c8c613f9a565b909450925061478c84848a8a613f9a565b909450925061479d86868181613f9a565b909c509a506147ae848460086148a2565b90925090506147bf8c8c84846141a5565b909c509a506147d088888181613f9a565b90925090506147e1848460046148a2565b90945092506147f284848e8e6141a5565b909450925061480384848888613f9a565b90945092506148148a8a60086148a2565b909650945061482586868c8c613f9a565b909650945061483686868484613f9a565b9096509450614847848488886141a5565b90945092506148588c8c60026148a2565b909650945061486986868a8a613f9a565b909650945061487a88888484613f9a565b909250905061488b828260086148a2565b809250819350505096509650965096509650969050565b60008060008051602061575c83398151915283860960008051602061575c83398151915284860991509150935093915050565b600080836148f18460008051602061575c8339815191526154a1565b915091509250929050565b6060826149115761490c8261494f565b613e30565b815115801561492857506001600160a01b0384163b155b156149485783604051639996b31560e01b8152600401610a419190614f42565b5080613e30565b80511561495f5780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b604080516101408101825260008082526020820181905291810191909152606081016149a26149d2565b81526020016000815260200160008152602001600081526020016000815260200160608152602001600081525090565b604051806040016040528060008152602001600081525090565b60405180608001604052806004906020820280368337509192915050565b6040518060400160405280614a1d614a8c565b8152602001614a2a614a8c565b905290565b508054600082556003029060005260206000209081019061080c9190614aaa565b6040518060c001604052806006906020820280368337509192915050565b60405180602001604052806001906020820280368337509192915050565b60405180604001604052806002906020820280368337509192915050565b5b80821115614add5780546001600160a01b03199081168255600182018054909116905560006002820155600301614aab565b5090565b80356001600160401b0381168114614af857600080fd5b919050565b600060208284031215614b0f57600080fd5b613e3082614ae1565b80518252602090810151910152565b805180516001600160a01b03908116845260209182015116818401520151604082015260600190565b600081518084526020840193506020830160005b82811015614b8857614b77868351614b27565b955060209190910190600101614b64565b5093949350505050565b60208152614bac6020820183516001600160401b03169052565b60006020830151614bc860408401826001600160401b03169052565b5060408301516001600160a01b0381166060840152506060830151614bf06080840182614b18565b50608083015160c083015260a083015160e083015260c083015161010083015260e0830151610120830152610100830151610160610140840152614c38610180840182614b50565b90506101208401516101608401528091505092915050565b600060208284031215614c6257600080fd5b5035919050565b6001600160401b0391909116815260200190565b60008060208385031215614c9057600080fd5b82356001600160401b03811115614ca657600080fd5b8301601f81018513614cb757600080fd5b80356001600160401b03811115614ccd57600080fd5b8560208260051b8401011115614ce257600080fd5b6020919091019590945092505050565b634e487b7160e01b600052604160045260246000fd5b604080519081016001600160401b0381118282101715614d2a57614d2a614cf2565b60405290565b604051608081016001600160401b0381118282101715614d2a57614d2a614cf2565b604051601f8201601f191681016001600160401b0381118282101715614d7a57614d7a614cf2565b604052919050565b600060208284031215614d9457600080fd5b81356001600160401b03811115614daa57600080fd5b8201601f81018413614dbb57600080fd5b80356001600160401b03811115614dd457614dd4614cf2565b614de7601f8201601f1916602001614d52565b818152856020838501011115614dfc57600080fd5b81602084016020830137600091810160200191909152949350505050565b6001600160a01b038116811461080c57600080fd5b60006080828403121561412a57600080fd5b60006001600160401b03821115614e5a57614e5a614cf2565b5060051b60200190565b600082601f830112614e7557600080fd5b8135614e88614e8382614e41565b614d52565b8082825260208201915060208360051b860101925085831115614eaa57600080fd5b602085015b83811015614ece57614ec081614ae1565b835260209283019201614eaf565b5095945050505050565b60008060008060e08587031215614eee57600080fd5b8435614ef981614e1a565b935060208501359250614f0f8660408701614e2f565b915060c08501356001600160401b03811115614f2a57600080fd5b614f3687828801614e64565b91505092959194509250565b6001600160a01b0391909116815260200190565b604081016123af8284614b18565b600080600080848603610100811215614f7c57600080fd5b6040811215614f8a57600080fd5b5084935060408401359250614fa28660608601614e2f565b915060e08501356001600160401b03811115614f2a57600080fd5b6040808252835190820181905260009060208501906060840190835b818110156150005783516001600160401b0316835260209384019390920191600101614fd9565b50508381036020808601919091528551808352918101925085019060005b8181101561504757615031848451614b18565b604093909301926020929092019160010161501e565b50919695505050505050565b60006040828403121561506557600080fd5b61506d614d08565b823581526020928301359281019290925250919050565b60006080828403121561509657600080fd5b61509e614d30565b8235815260208084013590820152604080840135908201526060928301359281019290925250919050565b600082601f8301126150da57600080fd5b81356150e8614e8382614e41565b8082825260208201915060206060840286010192508583111561510a57600080fd5b602085015b83811015614ece57808703606081121561512857600080fd5b615130614d08565b604082121561513e57600080fd5b615146614d08565b9150823561515381614e1a565b8252602083013561516381614e1a565b6020838101919091529181526040830135818301528452929092019160600161510f565b60008060008084860361016081121561519f57600080fd5b6151a98787615053565b94506151b88760408801615084565b9350608060bf19820112156151cc57600080fd5b506151d5614d30565b60c0860135815260e08601356020820152610100860135604082015261012086013561ffff8116811461520757600080fd5b606082015291506101408501356001600160401b0381111561522857600080fd5b614f36878288016150c9565b60008060006060848603121561524957600080fd5b505081359360208301359350604090920135919050565b60006020828403121561527257600080fd5b8135613e3081614e1a565b918252602082015260400190565b600080600080600080600060e0888a0312156152a657600080fd5b87356152b181614e1a565b965060208801356152c181614e1a565b96999698505050506040850135946060810135946080820135945060a0820135935060c0909101359150565b600080600080610100858703121561530457600080fd5b61530e8686615053565b935061531d8660408701615084565b925060c085013561532d81614e1a565b9396929550929360e00135925050565b634e487b7160e01b600052603260045260246000fd5b60008235609e1983360301811261536957600080fd5b9190910192915050565b6000808335601e1984360301811261538a57600080fd5b8301803591506001600160401b038211156153a457600080fd5b602001915060608102360382131561215957600080fd5b6000604082840312156153cd57600080fd5b613e308383615053565b634e487b7160e01b600052601160045260246000fd5b808201808211156123af576123af6153d7565b80546001600160a01b0319166001600160a01b0392909216919091179055565b813561542b81614e1a565b6154358183615400565b50602082013561544481614e1a565b6154518160018401615400565b50604082013560028201555050565b80820281158282048414176123af576123af6153d7565b634e487b7160e01b600052601260045260246000fd5b60008261549c5761549c615477565b500490565b818103818111156123af576123af6153d7565b6000608082840312156154c657600080fd5b613e308383615084565b6001600160401b039390931683526020830191909152604082015260600190565b60005b8381101561550c5781810151838201526020016154f4565b50506000910152565b600082516153698184602087016154f1565b6001600160a01b0385168152600061010082016155476020840187614b18565b8451606084015260208501516080840152604085015160a084015261ffff60608601511660c084015261010060e08401528084518083526101208501915060208601925060005b818110156155b2576155a1838551614b27565b60209490940193925060010161558e565b509098975050505050505050565b6001600160a01b038316815260608101613e306020830184614b18565b6001600160401b0381811683821601908111156123af576123af6153d7565b60006001820161560e5761560e6153d7565b5060010190565b60006001600160401b0382166002600160401b03198101615638576156386153d7565b60010192915050565b8135815260208083013590820152604081016123af565b6001600160401b038316815260608101613e30602083018480358252602090810135910152565b6001600160a01b038416815260208101839052608081016131506040830184614b18565b6001600160401b039290921682526001600160a01b0316602082015260400190565b600084516156d78184602089016154f1565b919091019283525060601b6001600160601b0319166020820152603401919050565b600060ff821660ff8103615638576156386153d7565b634e487b7160e01b600052600160045260246000fd5b60008261573457615734615477565b500690565b60006020828403121561574b57600080fd5b81518015158114613e3057600080fdfe30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd476e0956cda88cad152e89927e53611735b61a5c762d1428573c6931b0a5efcb01424c535f5349475f545259414e44494e4352454d454e545f4c49515549444154452b149d40ceb8aaae81be18991be06ac3b5b4c5e559dbefa33267e6dc24a138e5a164736f6c634300081a000a", + "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106102e05760003560e01c8063040f9853146102e5578063095f14041461030e5780630962ef79146103255780630bd1b4191461033a57806317f33a7d146103435780631f4c49301461036a5780631f744b541461037d578063245fc1831461038557806334fde37614610398578063372500ab146103a05780633f4ba83a146103a857806343039d90146103b0578063538d2709146103c3578063544736e6146103cc57806356d399e8146103e957806358e93308146103f25780635c975abb146103fb578063623b94221461040357806362f1fbc21461040c57806365ca819d146104165780636b348ab41461044a578063715018a61461045357806379ba50971461045b5780637a6d4065146104635780637c89d2f0146104765780638328b6101461049b5780638456cb59146104ae57806386e76685146104b657806386fa5063146104bf578063879d0f0c146104c85780638a2209e6146104d05780638a399481146104e55780638da5cb5b146104ee5780639156ac80146104f657806395055ffd146104ff5780639592d424146105125780639c80ebee1461051b5780639cebc4741461040c5780639e3b372d14610523578063a44285a51461052c578063ab0122ae14610535578063abf2c50314610548578063ae6c60631461055e578063b13aaebd14610567578063b687f85c14610570578063bc7efecc14610578578063be9a65551461058b578063c0ebedab14610593578063c4f56d9a146105bc578063d5a81254146105cf578063d84f0bf8146105d8578063d9054b09146105e1578063da1d5435146105f4578063dd644d74146105fd578063e30c397814610606578063e58598131461060e578063eb82031214610621578063edbf4ac214610656578063ee94e41214610669578063f2fde38b14610672578063f324c8eb14610685578063f88d658b14610698578063f907f5fc146106ab578063ffd9a86d146106be575b600080fd5b6102f86102f3366004614afd565b6106d1565b6040516103059190614b92565b60405180910390f35b610317600d5481565b604051908152602001610305565b610338610333366004614c50565b610802565b005b610317600f5481565b60015461035d90600160a01b90046001600160401b031681565b6040516103059190614c69565b610338610378366004614c7d565b61080f565b610338610aba565b610338610393366004614c50565b610b8a565b610317610bef565b610338610c26565b610338610c54565b6103386103be366004614c50565b610c66565b61031760075481565b6000546103d99060ff1681565b6040519015158152602001610305565b610317600b5481565b610317600e5481565b6103d9610cc4565b61031760155481565b61031762278d0081565b61035d610424366004614d82565b80516020818301810180516012825292820191909301209152546001600160401b031681565b610317600a5481565b610338610cd9565b610338610ceb565b610338610471366004614ed8565b610d27565b60005461048e9061010090046001600160a01b031681565b6040516103059190614f42565b6103386104a9366004614c50565b610ec5565b610338610f22565b61031760045481565b610317600c5481565b610317601481565b6104d8610f32565b6040516103059190614f56565b61031760035481565b61048e610f55565b61031760195481565b61033861050d366004614f64565b610f70565b61031760025481565b61035d600081565b61031760095481565b61031760175481565b610338610543366004614afd565b610ff9565b6105506110c2565b604051610305929190614fbd565b61031760065481565b61031760185481565b610317600481565b610338610586366004615187565b61124d565b610338611576565b61035d6105a1366004614c50565b601b602052600090815260409020546001600160401b031681565b6103386105ca366004614f64565b61158d565b61031760055481565b61031760085481565b6103386105ef366004614afd565b611742565b61031760165481565b610317601a5481565b61048e611754565b61033861061c366004615234565b61175f565b61064861062f366004615260565b6011602052600090815260409020805460019091015482565b60405161030592919061527d565b61033861066436600461528b565b611812565b610317610e1081565b610338610680366004615260565b611b80565b610338610693366004614c50565b611bf1565b6103386106a6366004614c50565b611c4f565b60015461048e906001600160a01b031681565b6103386106cc3660046152ed565b611cad565b6106d9614978565b6001600160401b03808316600090815260106020908152604080832081516101408101835281548087168252600160401b90049095168584015260018101546001600160a01b0316858301528151808301835260028201548152600382015481850152606086015260048101546080860152600581015460a0860152600681015460c0860152600781015460e0860152600881018054835181860281018601909452808452919461010087019491929184015b828210156107e9576000848152602090819020604080516080810182526003860290920180546001600160a01b0390811692840192835260018083015490911660608501529183526002015482840152908352909201910161078c565b5050505081526020016009820154815250509050919050565b61080c3382611cbf565b50565b610817611dfa565b60005460ff161561083b57604051630a35d5c360e31b815260040160405180910390fd5b8060005b81811015610aac573684848381811061085a5761085a61533d565b905060200281019061086c9190615353565b9050600061087d6080830183615373565b90501161089d57604051631a10033d60e11b815260040160405180910390fd5b600c546108ad6080830183615373565b905011156108ce5760405163226c2d8360e21b815260040160405180910390fd5b6000806108ed6108e3368590038501856153bb565b8460400135611e2c565b600b54600782015590925090506109076080840184615373565b60008181106109185761091861533d565b61092e9260206060909202019081019150615260565b6001820180546001600160a01b0319166001600160a01b03929092169190911790556060830135600482015560008061096a6080860186615373565b9050905060005b81811015610a1857366109876080880188615373565b838181106109975761099761533d565b90506060020190508060400135846109af91906153ed565b935060006109c06020830183615260565b6001600160a01b0316036109e75760405163e99d5ac560e01b815260040160405180910390fd5b60088501805460018101825560009182526020909120829160030201610a0d8282615420565b505050600101610971565b50600b548214610a4a57600b54826040516367d22bd960e01b8152600401610a4192919061527d565b60405180910390fd5b604080518635815260208088013590820152868201358183015290516001600160401b038616917f34be2fa283e1d5eb453459898f160448257aa1ef0f7dd2c3a3b55c45d1af768f919081900360600190a2600186019550505050505061083f565b50610ab5612160565b505050565b600060028190558052601060205260008051602061577c833981519152546001600160401b03165b6001600160401b0381161561080c576001600160401b0381166000908152601060205260408120600254909103610b285760028101546013556003810154601455610b70565b6040805180820182526013548152601454602080830191909152825180840190935260028401548352600384015490830152610b639161218c565b8051601355602001516014555b546002805460010190556001600160401b03169050610ae2565b610b92611dfa565b60008111610bb357604051630ef6cf4560e41b815260040160405180910390fd5b60188190556040518181527fa83e79dc52a438d552d0631ce4fbfe7dec7656d10398a2849f352a6ab4f0163b906020015b60405180910390a150565b60008060646002546002610c039190615460565b610c0d919061548d565b905060148111610c1e576014610c20565b805b91505090565b336000908152601160205260408120600181015490549091610c4883836154a1565b9050610ab53382611cbf565b610c5c611dfa565b610c64612238565b565b610c6e611dfa565b60008111610c8f57604051630ef6cf4560e41b815260040160405180910390fd5b60048190556040518181527f0ac8ee09138dfaf5e3ebe4cb4fd42dd1a0695535a530171223fb5066f52e0e3b90602001610be4565b600080610ccf612284565b5460ff1692915050565b610ce1611dfa565b610c6460006122a8565b3380610cf5611754565b6001600160a01b031614610d1e578060405163118cdaa760e01b8152600401610a419190614f42565b61080c816122a8565b610d2f6122cf565b60005460ff16610d525760405163348b55eb60e21b815260040160405180910390fd5b8051600354811115610d975780600254610d6c91906154a1565b600354600254610d7c91906154a1565b604051635eee4a3560e01b8152600401610a4192919061527d565b6001600160a01b038516610dbe5760405163e99d5ac560e01b815260040160405180910390fd5b6001600160a01b0385166000908152601160205260409020548411610df6576040516333938e6360e01b815260040160405180910390fd5b6007546040805160208101929092526001600160601b0319606088901b16908201526054810185905260009060740160405160208183030381529060405290506000610e4482600a546122f5565b9050610e5f84610e59368890038801886154b4565b836123b5565b50506001600160a01b0385166000818152601160205260409081902080549087905590519091907f95390641529563dbfb446535fa996c5ac3be00f90f5705b3abda59a4467b797f90610eb5908890859061527d565b60405180910390a2505050505050565b610ecd611dfa565b60008111610eee57604051630ef6cf4560e41b815260040160405180910390fd5b600b8190556040518181527e6b7a1ea14ff2794527a64af37d55a2040e351f8b4c1adcdc9aea80d64e042990602001610be4565b610f2a611dfa565b610c646124f7565b610f3a6149d2565b50604080518082019091526013548152601454602082015290565b600080610f6061253e565b546001600160a01b031692915050565b610f786122cf565b60005460ff16610f9b5760405163348b55eb60e21b815260040160405180910390fd5b8051600354811115610fb55780600254610d6c91906154a1565b6000610fc686868660085487612562565b506001600160401b038116600090815260106020526040902060070154909150610ff190829061280f565b505050505050565b6110016122cf565b60005460ff166110245760405163348b55eb60e21b815260040160405180910390fd5b6001600160401b038116600090815260106020526040812060050154908190036110635781604051630cf4827360e31b8152600401610a419190614c69565b600061107262278d00836153ed565b90508042101561109b57828142604051637a54a45360e11b8152600401610a41939291906154d0565b6001600160401b038316600090815260106020526040902060070154610ab590849061280f565b6060806002546001600160401b038111156110df576110df614cf2565b604051908082528060200260200182016040528015611108578160200160208202803683370190505b5091506002546001600160401b0381111561112557611125614cf2565b60405190808252806020026020018201604052801561115e57816020015b61114b6149d2565b8152602001906001900390816111435790505b506000808052601060205260008051602061577c833981519152549192506001600160401b03909116905b6001600160401b03821615611247576001600160401b038083166000908152601060205260409020855190918491879185169081106111ca576111ca61533d565b60200260200101906001600160401b031690816001600160401b031681525050806002016040518060400160405290816000820154815260200160018201548152505084836001600160401b0316815181106112285761122861533d565b6020908102919091010152546001600160401b03169150600101611189565b50509091565b6112556122cf565b60005460ff166112785760405163348b55eb60e21b815260040160405180910390fd5b8051600c5481111561129d5760405163226c2d8360e21b815260040160405180910390fd5b8015611311576000805b828110156112e2578381815181106112c1576112c161533d565b602002602001015160200151826112d891906153ed565b91506001016112a7565b50600b54811461130b57600b54816040516367d22bd960e01b8152600401610a4192919061527d565b506113a6565b60408051600180825281830190925290816020015b604080516080810182526000918101828152606082018390528152602081019190915281526020019060019003908161132657505060408051608081018252339181018281526060820192909252908152600b54602082015281519193509083906000906113965761139661533d565b6020026020010181905250815190505b600060126113b3876128b2565b6040516113c09190615515565b908152604051908190036020019020546001600160401b0316905080156113fc578060405163459c639360e01b8152600401610a419190614c69565b6000836000815181106114115761141161533d565b60200260200101516000015160000151905061143387878388600001516128db565b600080611444898860000151611e2c565b600b5460078201556001810180546001600160a01b0319166001600160a01b038716179055909250905060005b858110156114f8578160080187828151811061148f5761148f61533d565b60209081029190910181015182546001808201855560009485529383902082518051600390930290910180546001600160a01b03199081166001600160a01b03948516178255918501518187018054909316931692909217905591015160029091015501611471565b50611501612160565b816001600160401b03167f533c16cb4158fe7c77021b90e9290bbefa457dd392bd8abc1eab59747834fd5b848b8a8a6040516115409493929190615527565b60405180910390a261156b600060019054906101000a90046001600160a01b03163330600b546129c5565b505050505050505050565b61157e611dfa565b6000805460ff19166001179055565b6115956122cf565b60005460ff166115b85760405163348b55eb60e21b815260040160405180910390fd5b80516003548111156115d25780600254610d6c91906154a1565b6000806115e487878760095488612562565b91509150816001600160401b03167f0bfb12191b00293af29126b1c5489f8daeb4a4af82db2960b7f8353c3105cd7c8260400151836060015160405161162b9291906155c0565b60405180910390a26000600f54600d54600e5461164891906153ed565b61165291906153ed565b905060008260e001519050600082600d548361166e9190615460565b611678919061548d565b90506000600e548361168a9190615460565b156116c457836001600e54856116a09190615460565b6116aa91906154a1565b6116b4919061548d565b6116bf9060016153ed565b6116c7565b60005b90506116e786826116d885876154a1565b6116e291906154a1565b61280f565b600d541561170b5760005461170b9061010090046001600160a01b03163384612a2c565b600e541561173557600054600154611735916001600160a01b036101009091048116911683612a2c565b5050505050505050505050565b61174a6122cf565b61080c8133612a5d565b600080610f60612c2d565b611767611dfa565b6000811161178857604051630ef6cf4560e41b815260040160405180910390fd5b8061179383856153ed565b61179e906003615460565b11156117bd57604051630b06449d60e41b815260040160405180910390fd5b600d839055600e829055600f81905560408051848152602081018490529081018290527f0f69a2a87c90cdbb1a6a46bbb4870f2edfbd516244285ab6ffed6460ac2c6a839060600160405180910390a1505050565b600061181c612c51565b805490915060ff600160401b82041615906001600160401b03166000811580156118435750825b90506000826001600160401b0316600114801561185f5750303b155b90508115801561186d575080155b1561188b5760405163f92ee8a960e01b815260040160405180910390fd5b84546001600160401b031916600117855583156118b457845460ff60401b1916600160401b1785555b60018610156118d657604051630ef6cf4560e41b815260040160405180910390fd5b856118e1888a6153ed565b6118ec906003615460565b111561190b57604051630b06449d60e41b815260040160405180910390fd5b6000805460ff19168155600281905560035561012c60045560408051808201909152601b81527a0424c535f5349475f545259414e44494e4352454d454e545f504f5602c1b602082015261195e90612c75565b60065560408051808201909152601e81527f424c535f5349475f545259414e44494e4352454d454e545f5245574152440000602082015261199e90612c75565b60075560408051808201909152601c81527b109314d7d4d251d7d51496505391125390d4915351539517d156125560221b60208201526119dd90612c75565b600881905550611a0460405180606001604052806021815260200161579c60219139612c75565b600955604080518082019091526019815278424c535f5349475f484153485f544f5f4649454c445f54414760381b6020820152611a4090612c75565b600a5561025860055566038d7ea4c6800060175561a8c060185560006019819055601a8190558054610100600160a81b0319166101006001600160a01b038f811691909102919091178255600180546001600160a01b031916918e16919091178155600b8c9055600c8b9055600d8a9055600e899055600f889055611ac591906155dd565b600180546001600160401b0392909216600160a01b02600160a01b600160e01b031990921691909117905560008052601060205260008051602061577c83398151915280546001600160801b0319169055611b1f33612ca9565b611b27612cba565b8315611b7257845460ff60401b191685556040517fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d290611b6990600190614c69565b60405180910390a15b505050505050505050505050565b611b88611dfa565b6000611b92612c2d565b80546001600160a01b0319166001600160a01b0384169081178255909150611bb8610f55565b6001600160a01b03167f38d16b8cac22d99fc7c124b9cd0de2d3fa1faef420bfe791d8c362d765e2270060405160405180910390a35050565b611bf9611dfa565b60008111611c1a57604051630ef6cf4560e41b815260040160405180910390fd5b60178190556040518181527fe604909b918af52702fa14cd9b5a8870e5bd66da3163365671e583f705fd546390602001610be4565b611c57611dfa565b60008111611c7857604051630ef6cf4560e41b815260040160405180910390fd5b60058190556040518181527ffbbc3d0a51e101ce4a7fda20e6e4eb0e230bf7de27ee2e3683fc8539e376639590602001610be4565b611cb9848484846128db565b50505050565b6001600160a01b0382166000908152601160205260408120600181015490549091611cea83836154a1565b905080841115611d0d5760405163363cc6b360e21b815260040160405180910390fd5b600060185442611d1d919061548d565b9050601a54811115611d3457601a81905560006019555b8460196000828254611d4691906153ed565b90915550506017546019541115611d7057604051638c10944b60e01b815260040160405180910390fd5b6001600160a01b03861660009081526011602052604081206001018054879290611d9b9084906153ed565b90915550506040518581526001600160a01b038716907ffc30cddea38e2bf4d6ea7d3f9ed3b6ad7f176419f4963bd81318067a4aee73fe9060200160405180910390a2600054610ff19061010090046001600160a01b03168787612a2c565b33611e03610f55565b6001600160a01b031614610c64573360405163118cdaa760e01b8152600401610a419190614f42565b815160009081901580611e4157506020840151155b15611e5f5760405163139b722760e31b815260040160405180910390fd5b82600003611e8057604051634eb8f11f60e01b815260040160405180910390fd5b6000611e8b856128b2565b905060006001600160401b0316601282604051611ea89190615515565b908152604051908190036020019020546001600160401b031614611f0857601281604051611ed69190615515565b9081526040519081900360200181205463459c639360e01b8252610a41916001600160401b0390911690600401614c69565b6000848152601b60205260409020546001600160401b031615611f58576000848152601b602052604090819020549051630cb4c72160e21b8152610a41916001600160401b031690600401614c69565b60005460ff1615611fbd57436015541015611f77574360155560006016555b60168054906000611f87836155fc565b91905055506000611f96610bef565b9050806016541115611fbb5760405163689f6e7560e11b815260040160405180910390fd5b505b60018054600160a01b90046001600160401b0316906014611fdd83615615565b91906101000a8154816001600160401b0302191690836001600160401b031602179055509250600260008154612012906155fc565b909155506001600160401b03838116600081815260106020908152604080832060008051602061577c833981519152805482546001600160801b031916600160401b918290049098168082029890981783558154600160401b600160801b03191690870217815586855282852080546001600160401b031990811688179091558c5160028401558c8501516003840155426004840155600983018c90558b8652601b90945293829020805490931690941790915551919450919085906012906120dc908690615515565b90815260405190819003602001902080546001600160401b03929092166001600160401b03199092169190911790556002546001036121275786516013556020870151601455612155565b6040805180820190915260135481526014546020820152612148908861218c565b8051601355602001516014555b5050505b9250929050565b60006003600254612171919061548d565b905060045481116121825780612186565b6004545b60035550565b6121946149d2565b61219c6149ec565b835181526020808501518183015283516040808401919091529084015160608301526000908360808460066107d05a03fa9050806122305760405162461bcd60e51b815260206004820152602b60248201527f43616c6c20746f20707265636f6d70696c656420636f6e747261637420666f7260448201526a081859190819985a5b195960aa1b6064820152608401610a41565b505092915050565b612240612cca565b600061224a612284565b805460ff1916815590507f5db9ee0a495bf2e6ff9c91a7834c1ba4fdd244a5e8aa4e537bd38aeae4b073aa335b604051610be49190614f42565b7fcd5ed15c6e187e77e9aee88184c21f4f2182ab5827cb3b7e07fbedcd63f0330090565b60006122b2612c2d565b80546001600160a01b031916815590506122cb82612cef565b5050565b6122d7610cc4565b15610c645760405163d93c066560e01b815260040160405180910390fd5b6122fd614a0a565b60006123098484612d4b565b9050600080600080612376856000015160016002811061232b5761232b61533d565b602002015186516000602002015187602001516001600281106123505761235061533d565b6020020151886020015160006002811061236c5761236c61533d565b6020020151612f1c565b60408051608081018252808201948552606081019590955292845282518084019093528252602082810191909152820152955050505050505b92915050565b6123bd6149d2565b835160005b818110156124435760008682815181106123de576123de61533d565b602002602001015190506124388460106000846001600160401b03166001600160401b031681526020019081526020016000206002016040518060400160405290816000820154815260200160018201548152505061218c565b9350506001016123c2565b50604080518082019091526013548152601454602082015261246d9061246884612fac565b61218c565b60408051608081018252602080880151828401908152885160608085019190915290835283518085018552908901518152928801518382015281019190915290925060006124ba84612fac565b90506124cf6124c7613036565b838388613057565b6124ee5783604051634fb09a2160e11b8152600401610a419190614f56565b50505050505050565b6124ff6122cf565b6000612509612284565b805460ff1916600117815590507f62e78cea01bee320cd4e420270b5ea74000d11b0c9f74754ebdbfc544b05a2586122773390565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930090565b600061256c614978565b6000612585612580368a90038a018a6153bb565b6128b2565b90506012816040516125979190615515565b908152604051908190036020019020546001600160401b0316925060008390036125d65787604051631b8ac14360e01b8152600401610a419190615641565b6125df87613158565b15612603578287426040516337030b8b60e01b8152600401610a41939291906154d0565b6001600160401b03808416600090815260106020908152604080832081516101408101835281548087168252600160401b90049095168584015260018101546001600160a01b0316858301528151808301835260028201548152600382015481850152606086015260048101546080860152600581015460a0860152600681015460c0860152600781015460e0860152600881018054835181860281018601909452808452919461010087019491929184015b82821015612713576000848152602090819020604080516080810182526003860290920180546001600160a01b039081169284019283526001808301549091166060850152918352600201548284015290835290920191016126b6565b5050509082525060099190910154602090910152606081015151909250883514158061274b5750816060015160200151886020013514155b1561276d57828860405163c43283b560e01b8152600401610a41929190615658565b608082015161277f90611c20906153ed565b4210156127a8576080820151604051633acb74ef60e11b8152610a4191859142906004016154d0565b6040805160208082018890528a35828401528a0135606082015260808082018a90528251808303909101815260a0909101909152600a546000906127ed9083906122f5565b905061280286610e59368b90038b018b6154b4565b5050509550959350505050565b6001600160401b0382166000908152601060209081526040918290206001810154835180850190945260028201548452600390910154918301919091526001600160a01b03169061285f84613170565b612867612160565b836001600160401b03167f24c4411329b949fad2eba6f79a8ba090a08eac1af4b56c3a2f5a282ed45e233e8385846040516128a49392919061567f565b60405180910390a250505050565b6060816040516020016128c59190614f56565b6040516020818303038152906040529050919050565b600060065485600001518660200151858560405160200161292c95949392919094855260208501939093526040840191909152606090811b6001600160601b03191690830152607482015260940190565b6040516020818303038152906040529050600061294b82600a546122f5565b60408051608081018252602080890151828401908152895160608085019190915290835283518085018552908a0151815292890151838201528101919091529091506129a8612998613036565b826129a28a612fac565b85613057565b6124ee5760405163cf006ab760e01b815260040160405180910390fd5b6040516001600160a01b038481166024830152838116604483015260648201839052611cb99186918216906323b872dd906084015b604051602081830303815290604052915060e01b6020820180516001600160e01b038381831617835250505050613479565b6040516001600160a01b03838116602483015260448201839052610ab591859182169063a9059cbb906064016129fa565b60005460ff16612a805760405163348b55eb60e21b815260040160405180910390fd5b6001600160401b03821660009081526010602052604081206008810154829190825b81811015612b13576000836008018281548110612ac157612ac161533d565b6000918252602090912060039091020180549091506001600160a01b03808916911603612b0a576007840154600282015460019750612b01906004615460565b10945050612b13565b50600101612aa2565b5083612b36578585604051635bf2837760e11b8152600401610a419291906156a3565b8160050154600003612b8d57828015612b60575062278d008260040154612b5d91906153ed565b42105b15612b82578585604051633826feb560e01b8152600401610a419291906156a3565b426005830155612bcc565b6000610e108360060154612ba191906153ed565b905080421015612bca57868142604051637a54a45360e11b8152600401610a41939291906154d0565b505b426006830155604080516001600160a01b0387168152600284015460208201526003840154918101919091526001600160401b038716907f8600c0145511b317ae0189933fa71eb57a32d74891804c7993ef74ec63a5e80490606001610eb5565b7f237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c0090565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0090565b6000814630604051602001612c8c939291906156c5565b604051602081830303815290604052805190602001209050919050565b612cb16134d3565b61080c816134f8565b612cc26134d3565b610c6461352a565b612cd2610cc4565b610c6457604051638dfc202b60e01b815260040160405180910390fd5b6000612cf961253e565b80546001600160a01b038481166001600160a01b031983168117845560405193945091169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a3505050565b612d53614a0a565b600080600080600087516001612d6991906153ed565b6001600160401b03811115612d8057612d80614cf2565b6040519080825280601f01601f191660200182016040528015612daa576020820181803683370190505b50885190915060005b81811015612e0957898181518110612dcd57612dcd61533d565b602001015160f81c60f81b838281518110612dea57612dea61533d565b60200101906001600160f81b031916908160001a905350600101612db3565b5060005b8060f81b8360018551612e2091906154a1565b81518110612e3057612e3061533d565b60200101906001600160f81b031916908160001a9053506000612e53848b613547565b91995097509050600080612e678a8a61363f565b91509150600080612e7884846136b2565b9150915081600014158015612e8c57508015155b15612ec9578415612ea757612ea18282613821565b90925090505b90985096508787612eba8c8c8484613866565b15612ec9575050505050612ee1565b50505050508080612ed9906156f9565b915050612e0d565b505060408051608081018252808201958652606081019690965293855250825180840190935282526020808301919091528201529392505050565b600080600080612f2e8888888861387d565b612f3a57612f3a61570f565b6040805160c081018252898152602081018990529081018790526060810186905260016080820152600060a0820152612f7281613920565b8051602082015160408301516060840151608085015160a0860151959650612f9995613ad2565b929c919b50995090975095505050505050565b612fb46149d2565b8151158015612fc557506020820151155b15612fe3575050604080518082019091526000808252602082015290565b60405180604001604052808360000151815260200160008051602061575c83398151915284602001516130169190615725565b61302e9060008051602061575c8339815191526154a1565b905292915050565b61303e6149d2565b5060408051808201909152600181526002602082015290565b60408051600280825260608201909252600091829190816020015b61307a6149d2565b8152602001906001900390816130725750506040805160028082526060820190925291925060009190602082015b6130b0614a0a565b8152602001906001900390816130a857905050905086826000815181106130d9576130d961533d565b602002602001018190525084826001815181106130f8576130f861533d565b602002602001018190525085816000815181106131175761311761533d565b602002602001018190525083816001815181106131365761313661533d565b602002602001018190525061314b8282613b1c565b925050505b949350505050565b60006005548261316891906153ed565b421192915050565b600060025411613193576040516363cd35d560e11b815260040160405180910390fd5b6001600160401b0381166131ba57604051631d58f8cb60e11b815260040160405180910390fd5b6001600160401b03808216600090815260106020908152604080832081516101408101835281548087168252600160401b90049095168584015260018101546001600160a01b0316858301528151808301835260028201548152600382015481850152606086015260048101546080860152600581015460a0860152600681015460c0860152600781015460e086015260088101805483518186028101860190945280845294959491936101008601939290879084015b828210156132ce576000848152602090819020604080516080810182526003860290920180546001600160a01b03908116928401928352600180830154909116606085015291835260020154828401529083529092019101613271565b5050509082525060099190910154602091820152818101805183516001600160401b0390811660009081526010855260408082208054600160401b600160801b031916600160401b9585169590950294909417909355855193518216815282902080546001600160401b0319169390911692909217909155805180820190915260135481526014549181019190915260608201519192506133729161246890612fac565b805160135560200151601455606081015160009061338f906128b2565b6101208301516000908152601b60205260409081902080546001600160401b0319169055519091506012906133c5908390615515565b908152604080516020928190038301902080546001600160401b03191690556001600160401b03851660009081526010909252812080546001600160801b03191681556001810180546001600160a01b0319169055600281018290556003810182905560048101829055600581018290556006810182905560078101829055906134526008830182614a2f565b6009820160009055505060016002600082825461346f91906154a1565b9091555050505050565b600061348e6001600160a01b03841683613e22565b905080516000141580156134b35750808060200190518101906134b19190615739565b155b15610ab55782604051635274afe760e01b8152600401610a419190614f42565b6134db613e37565b610c6457604051631afcd79f60e31b815260040160405180910390fd5b6135006134d3565b6001600160a01b038116610d1e576000604051631e4fbdf760e01b8152600401610a419190614f42565b6135326134d3565b600061353c612284565b805460ff1916905550565b600080600080600080600061357d898960405160200161356991815260200190565b604051602081830303815290604052613e51565b935093509350935060405160308152602080820152602060408201528460608201528360808201526001609082015260008051602061575c83398151915260b082015260208160d0836005600019fa6135d557600080fd5b805197505060405160308152602080820152836050820152602060408201528260708201526001609082015260008051602061575c83398151915260b082015260208160d0836005600019fa61362a57600080fd5b51969996985060019081161496505050505050565b60008060008060008061365488888a8a613f9a565b909250905061366582828a8a613f9a565b90925090506136a382826000805160206157bd8339815191527e9713b03af0fed4cd2cafadeed8fdf4a74fa084e52d1852e4a2bd0685c315d261400b565b90999098509650505050505050565b6000806000806000856000036136f4576136cb8761403f565b909350905080156136e55782600094509450505050612159565b60008394509450505050612159565b60008051602061575c833981519152878809925060008051602061575c833981519152868709915060008051602061575c83398151915282840892506137398361403f565b9093509050806137525760008094509450505050612159565b60008051602061575c833981519152838808915061376f826140d6565b9150600061377c8361403f565b92509050816137cd5761379e888560008051602061575c833981519152614130565b92506137a9836140d6565b92506137b48361403f565b92509050816137cd576000809550955050505050612159565b8060008051602061575c83398151915282830893506137fa8460008051602061575c833981519152614154565b9350600060008051602061575c833981519152858a09919a91995090975050505050505050565b6000808061383d8560008051602061575c8339815191526154a1565b905060006138598560008051602061575c8339815191526154a1565b9196919550909350505050565b60006138748585858561387d565b95945050505050565b600080600080600061389187878989613f9a565b90945092506138a289898181613f9a565b90925090506138b382828b8b613f9a565b90925090506138c4848484846141a5565b909450925061390284846000805160206157bd8339815191527e9713b03af0fed4cd2cafadeed8fdf4a74fa084e52d1852e4a2bd0685c315d26141a5565b909450925083158015613913575082155b9998505050505050505050565b613928614a50565b613930614a50565b613938614a50565b613940614a50565b84516020860151604087015160608801516080890151613978946744e992b44a6909f1949093909290918b60055b60200201516141e7565b805160208201516040830151606084015160808501519497506139a4946002949392919089600561396e565b9150613a108360005b602002015184600160200201518560026020020151866003602002015187600460200201518860056020020151886000602002015189600160200201518a600260200201518b600360200201518c600460200201518d60055b602002015161426a565b9150613a1b826144f3565b9150613a26836144f3565b9050613a31816144f3565b9050613a3e8360006139ad565b9250613aa48360005b6020020151846001602002015185600260200201518660036020020151876004602002015188600560200201518760006020020151886001602002015189600260200201518a600360200201518b600460200201518c6005613a06565b9250613aaf856144f3565b9050613aba816144f3565b9050613ac5816144f3565b9050613874836000613a47565b600080600080600080613ae5888861460b565b9092509050613af68c8c8484613f9a565b9096509450613b078a8a8484613f9a565b969d959c509a50949850929650505050505050565b60008151835114613b2c57600080fd5b82516000613b3b826006615460565b90506000816001600160401b03811115613b5757613b57614cf2565b604051908082528060200260200182016040528015613b80578160200160208202803683370190505b50905060005b83811015613db157868181518110613ba057613ba061533d565b60200260200101516000015182826006613bba9190615460565b613bc59060006153ed565b81518110613bd557613bd561533d565b602002602001018181525050868181518110613bf357613bf361533d565b60200260200101516020015182826006613c0d9190615460565b613c189060016153ed565b81518110613c2857613c2861533d565b602002602001018181525050858181518110613c4657613c4661533d565b6020908102919091010151515182613c5f836006615460565b613c6a9060026153ed565b81518110613c7a57613c7a61533d565b602002602001018181525050858181518110613c9857613c9861533d565b60209081029190910181015151015182613cb3836006615460565b613cbe9060036153ed565b81518110613cce57613cce61533d565b602002602001018181525050858181518110613cec57613cec61533d565b602002602001015160200151600060028110613d0a57613d0a61533d565b602002015182613d1b836006615460565b613d269060046153ed565b81518110613d3657613d3661533d565b602002602001018181525050858181518110613d5457613d5461533d565b602002602001015160200151600160028110613d7257613d7261533d565b602002015182613d83836006615460565b613d8e9060056153ed565b81518110613d9e57613d9e61533d565b6020908102919091010152600101613b86565b50613dba614a6e565b60006020826020860260208601600060086107d05a03f1905080613e145760405162461bcd60e51b8152602060048201526011602482015270496e76616c6964205369676e617475726560781b6044820152606401610a41565b505115159695505050505050565b6060613e3083836000614696565b9392505050565b6000613e41612c51565b54600160401b900460ff16919050565b60008060008060ff85511115613e6657600080fd5b600060405160005b6088811015613e8557600082820152602001613e6e565b506088602060005b8a51811015613eae578a820151848401526020928301929182019101613e8d565b505060898951019050608081830153600201602060005b8951811015613ee65789820151848401526020928301929182019101613ec5565b5050608b88518a5101019050875181830153508751875101608c018120915050604051818152600160208201536021602060005b8951811015613f3b5789820151848401526020928301929182019101613f1a565b505050865187516021018201538651602201812095508582188152600260208201538651602201812094508482188152600360208201538651602201812093508382188152600460208201539551602201909520939692955090935050565b600080613fd860008051602061575c83398151915285880960008051602061575c83398151915285880960008051602061575c833981519152614130565b60008051602061575c8339815191528086880960008051602061575c833981519152868a09089150915094509492505050565b60008060008051602061575c83398151915284870860008051602061575c8339815191528487089150915094509492505050565b600080600060405160208152602080820152602060408201528460608201527f0c19139cb84c680a6e14116da060561765e05aa45a1c72a34f082305b61f3f52608082015260008051602061575c83398151915260a082015260208160c08360056107d05a03fa90519350905060008051602061575c83398151915283800984149150806140d05760009250600091505b50915091565b60006001821615156140e960028461548d565b9150801561412a5760008051602061575c833981519152600261411b60008051602061575c83398151915260016153ed565b614125919061548d565b830891505b50919050565b6000818061414057614140615477565b61414a84846154a1565b8508949350505050565b60008060405160208152602080820152602060408201528460608201526002840360808201528360a082015260208160c08360056107d05a03fa9051925090508061419e57600080fd5b5092915050565b6000806141c1868560008051602061575c833981519152614130565b6141da868560008051602061575c833981519152614130565b9150915094509492505050565b6141ef614a50565b871561425f576001881615614230578051602082015160408301516060840151608085015160a086015161422d9594939291908d8d8d8d8d8d61426a565b90505b61423e878787878787614733565b949b5092995090975095509350915061425860028961548d565b97506141ef565b979650505050505050565b614272614a50565b8815801561427e575087155b156142c0578686868686868660005b60a089019290925260808801929092526060870192909252604086019290925260208581019390935290910201526144e3565b821580156142cc575081155b156142df578c8c8c8c8c8c86600061428d565b6142eb85858b8b613f9a565b90955093506142fc8b8b8585613f9a565b6060830152604082015261431287878b8b613f9a565b90975095506143238d8d8585613f9a565b60a08301526080820181905287148015614340575060a081015186145b156143855760408101518514801561435b5750606081015184145b156143765761436e8d8d8d8d8d8d614733565b86600061428d565b6001600081818080868161428d565b61439189898585613f9a565b90935091506143b1858583600260200201518460035b60200201516141a5565b909d509b506143cb878783600460200201518460056143a7565b909b5099506143dc8b8b8181613f9a565b90995097506143fc898983600460200201518460055b6020020151613f9a565b909550935061440d89898d8d613f9a565b909950975061441e89898585613f9a565b60a083015260808201526144348d8d8181613f9a565b909750955061444587878585613f9a565b909750955061445687878b8b6141a5565b9097509550614467858560026148a2565b9093509150614478878785856141a5565b90975095506144898b8b8989613f9a565b6020830152815261449c858589896141a5565b909b5099506144ad8d8d8d8d613f9a565b909b5099506144c7898983600260200201518460036143f2565b909d509b506144d88b8b8f8f6141a5565b606083015260408201525b9c9b505050505050505050505050565b6144fb614a50565b815161450f908360015b60200201516148d5565b60208301528152604082015161452790836003614505565b60608301526040820152608082015161454290836005614505565b60a083015260808201528051602082015161459f91907f2fb347984f7911f74c0bec3cf559b143b78cc310c2c3330c99e39557176f553d7f16c9e55061ebae204ba4cc8bd75a079432ae2a1d0b7c9dce1665d51c640fcba2613f9a565b60208301528152604081015160608201516145fc91907f063cf305489af5dcdc5ec698b6e2f9b9dbaae0eda9c95998dc54014671a0135a7f07c03cbcac41049a0704b5a7ec796f2b21807dc98fa25bd282d37f632623b0e3613f9a565b60608301526040820152919050565b6000808061464c60008051602061575c8339815191528087880960008051602061575c8339815191528788090860008051602061575c833981519152614154565b905060008051602061575c83398151915281860960008051602061575c83398151915282860961468a9060008051602061575c8339815191526154a1565b92509250509250929050565b6060814710156146bb573060405163cd78605960e01b8152600401610a419190614f42565b600080856001600160a01b031684866040516146d79190615515565b60006040518083038185875af1925050503d8060008114614714576040519150601f19603f3d011682016040523d82523d6000602084013e614719565b606091505b50915091506147298683836148fc565b9695505050505050565b6000806000806000806147488c8c60036148a2565b909650945061475986868e8e613f9a565b909650945061476a8a8a8a8a613f9a565b909850965061477b8c8c8c8c613f9a565b909450925061478c84848a8a613f9a565b909450925061479d86868181613f9a565b909c509a506147ae848460086148a2565b90925090506147bf8c8c84846141a5565b909c509a506147d088888181613f9a565b90925090506147e1848460046148a2565b90945092506147f284848e8e6141a5565b909450925061480384848888613f9a565b90945092506148148a8a60086148a2565b909650945061482586868c8c613f9a565b909650945061483686868484613f9a565b9096509450614847848488886141a5565b90945092506148588c8c60026148a2565b909650945061486986868a8a613f9a565b909650945061487a88888484613f9a565b909250905061488b828260086148a2565b809250819350505096509650965096509650969050565b60008060008051602061575c83398151915283860960008051602061575c83398151915284860991509150935093915050565b600080836148f18460008051602061575c8339815191526154a1565b915091509250929050565b6060826149115761490c8261494f565b613e30565b815115801561492857506001600160a01b0384163b155b156149485783604051639996b31560e01b8152600401610a419190614f42565b5080613e30565b80511561495f5780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b604080516101408101825260008082526020820181905291810191909152606081016149a26149d2565b81526020016000815260200160008152602001600081526020016000815260200160608152602001600081525090565b604051806040016040528060008152602001600081525090565b60405180608001604052806004906020820280368337509192915050565b6040518060400160405280614a1d614a8c565b8152602001614a2a614a8c565b905290565b508054600082556003029060005260206000209081019061080c9190614aaa565b6040518060c001604052806006906020820280368337509192915050565b60405180602001604052806001906020820280368337509192915050565b60405180604001604052806002906020820280368337509192915050565b5b80821115614add5780546001600160a01b03199081168255600182018054909116905560006002820155600301614aab565b5090565b80356001600160401b0381168114614af857600080fd5b919050565b600060208284031215614b0f57600080fd5b613e3082614ae1565b80518252602090810151910152565b805180516001600160a01b03908116845260209182015116818401520151604082015260600190565b600081518084526020840193506020830160005b82811015614b8857614b77868351614b27565b955060209190910190600101614b64565b5093949350505050565b60208152614bac6020820183516001600160401b03169052565b60006020830151614bc860408401826001600160401b03169052565b5060408301516001600160a01b0381166060840152506060830151614bf06080840182614b18565b50608083015160c083015260a083015160e083015260c083015161010083015260e0830151610120830152610100830151610160610140840152614c38610180840182614b50565b90506101208401516101608401528091505092915050565b600060208284031215614c6257600080fd5b5035919050565b6001600160401b0391909116815260200190565b60008060208385031215614c9057600080fd5b82356001600160401b03811115614ca657600080fd5b8301601f81018513614cb757600080fd5b80356001600160401b03811115614ccd57600080fd5b8560208260051b8401011115614ce257600080fd5b6020919091019590945092505050565b634e487b7160e01b600052604160045260246000fd5b604080519081016001600160401b0381118282101715614d2a57614d2a614cf2565b60405290565b604051608081016001600160401b0381118282101715614d2a57614d2a614cf2565b604051601f8201601f191681016001600160401b0381118282101715614d7a57614d7a614cf2565b604052919050565b600060208284031215614d9457600080fd5b81356001600160401b03811115614daa57600080fd5b8201601f81018413614dbb57600080fd5b80356001600160401b03811115614dd457614dd4614cf2565b614de7601f8201601f1916602001614d52565b818152856020838501011115614dfc57600080fd5b81602084016020830137600091810160200191909152949350505050565b6001600160a01b038116811461080c57600080fd5b60006080828403121561412a57600080fd5b60006001600160401b03821115614e5a57614e5a614cf2565b5060051b60200190565b600082601f830112614e7557600080fd5b8135614e88614e8382614e41565b614d52565b8082825260208201915060208360051b860101925085831115614eaa57600080fd5b602085015b83811015614ece57614ec081614ae1565b835260209283019201614eaf565b5095945050505050565b60008060008060e08587031215614eee57600080fd5b8435614ef981614e1a565b935060208501359250614f0f8660408701614e2f565b915060c08501356001600160401b03811115614f2a57600080fd5b614f3687828801614e64565b91505092959194509250565b6001600160a01b0391909116815260200190565b604081016123af8284614b18565b600080600080848603610100811215614f7c57600080fd5b6040811215614f8a57600080fd5b5084935060408401359250614fa28660608601614e2f565b915060e08501356001600160401b03811115614f2a57600080fd5b6040808252835190820181905260009060208501906060840190835b818110156150005783516001600160401b0316835260209384019390920191600101614fd9565b50508381036020808601919091528551808352918101925085019060005b8181101561504757615031848451614b18565b604093909301926020929092019160010161501e565b50919695505050505050565b60006040828403121561506557600080fd5b61506d614d08565b823581526020928301359281019290925250919050565b60006080828403121561509657600080fd5b61509e614d30565b8235815260208084013590820152604080840135908201526060928301359281019290925250919050565b600082601f8301126150da57600080fd5b81356150e8614e8382614e41565b8082825260208201915060206060840286010192508583111561510a57600080fd5b602085015b83811015614ece57808703606081121561512857600080fd5b615130614d08565b604082121561513e57600080fd5b615146614d08565b9150823561515381614e1a565b8252602083013561516381614e1a565b6020838101919091529181526040830135818301528452929092019160600161510f565b60008060008084860361016081121561519f57600080fd5b6151a98787615053565b94506151b88760408801615084565b9350608060bf19820112156151cc57600080fd5b506151d5614d30565b60c0860135815260e08601356020820152610100860135604082015261012086013561ffff8116811461520757600080fd5b606082015291506101408501356001600160401b0381111561522857600080fd5b614f36878288016150c9565b60008060006060848603121561524957600080fd5b505081359360208301359350604090920135919050565b60006020828403121561527257600080fd5b8135613e3081614e1a565b918252602082015260400190565b600080600080600080600060e0888a0312156152a657600080fd5b87356152b181614e1a565b965060208801356152c181614e1a565b96999698505050506040850135946060810135946080820135945060a0820135935060c0909101359150565b600080600080610100858703121561530457600080fd5b61530e8686615053565b935061531d8660408701615084565b925060c085013561532d81614e1a565b9396929550929360e00135925050565b634e487b7160e01b600052603260045260246000fd5b60008235609e1983360301811261536957600080fd5b9190910192915050565b6000808335601e1984360301811261538a57600080fd5b8301803591506001600160401b038211156153a457600080fd5b602001915060608102360382131561215957600080fd5b6000604082840312156153cd57600080fd5b613e308383615053565b634e487b7160e01b600052601160045260246000fd5b808201808211156123af576123af6153d7565b80546001600160a01b0319166001600160a01b0392909216919091179055565b813561542b81614e1a565b6154358183615400565b50602082013561544481614e1a565b6154518160018401615400565b50604082013560028201555050565b80820281158282048414176123af576123af6153d7565b634e487b7160e01b600052601260045260246000fd5b60008261549c5761549c615477565b500490565b818103818111156123af576123af6153d7565b6000608082840312156154c657600080fd5b613e308383615084565b6001600160401b039390931683526020830191909152604082015260600190565b60005b8381101561550c5781810151838201526020016154f4565b50506000910152565b600082516153698184602087016154f1565b6001600160a01b0385168152600061010082016155476020840187614b18565b8451606084015260208501516080840152604085015160a084015261ffff60608601511660c084015261010060e08401528084518083526101208501915060208601925060005b818110156155b2576155a1838551614b27565b60209490940193925060010161558e565b509098975050505050505050565b6001600160a01b038316815260608101613e306020830184614b18565b6001600160401b0381811683821601908111156123af576123af6153d7565b60006001820161560e5761560e6153d7565b5060010190565b60006001600160401b0382166002600160401b03198101615638576156386153d7565b60010192915050565b8135815260208083013590820152604081016123af565b6001600160401b038316815260608101613e30602083018480358252602090810135910152565b6001600160a01b038416815260208101839052608081016131506040830184614b18565b6001600160401b039290921682526001600160a01b0316602082015260400190565b600084516156d78184602089016154f1565b919091019283525060601b6001600160601b0319166020820152603401919050565b600060ff821660ff8103615638576156386153d7565b634e487b7160e01b600052600160045260246000fd5b60008261573457615734615477565b500690565b60006020828403121561574b57600080fd5b81518015158114613e3057600080fdfe30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd476e0956cda88cad152e89927e53611735b61a5c762d1428573c6931b0a5efcb01424c535f5349475f545259414e44494e4352454d454e545f4c49515549444154452b149d40ceb8aaae81be18991be06ac3b5b4c5e559dbefa33267e6dc24a138e5a164736f6c634300081a000a", "linkReferences": {}, "deployedLinkReferences": {} } diff --git a/src/web3client/abis/Token.json b/src/web3client/abis/Token.json index 4e14626..dd75ff5 100644 --- a/src/web3client/abis/Token.json +++ b/src/web3client/abis/Token.json @@ -1,7 +1,7 @@ { "_format": "hh-sol-artifact-1", - "contractName": "Token", - "sourceName": "contracts/SENT.sol", + "contractName": "SESH", + "sourceName": "contracts/SESH.sol", "abi": [ { "inputs": [ @@ -538,8 +538,8 @@ "type": "function" } ], - "bytecode": "0x61016060405234801561001157600080fd5b5060405161155c38038061155c8339810160408190526100309161041d565b6040518060400160405280600781526020016629b2b9b9b4b7b760c91b81525080604051806040016040528060018152602001603160f81b8152506040518060400160405280600781526020016629b2b9b9b4b7b760c91b8152506040518060400160405280600481526020016314d1539560e21b81525081600390816100b791906104f9565b5060046100c482826104f9565b506100d491508390506005610248565b610120526100e3816006610248565b61014052815160208084019190912060e052815190820120610100524660a05261017060e05161010051604080517f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f60208201529081019290925260608201524660808201523060a082015260009060c00160405160208183030381529060405280519060200120905090565b60805250503060c05250806001600160a01b0381166101e45760405162461bcd60e51b815260206004820152602560248201527f5368617265643a205a65726f2d61646472657373206973206e6f74207065726d6044820152641a5d1d195960da1b60648201526084015b60405180910390fd5b82806000036102355760405162461bcd60e51b815260206004820152601b60248201527f5368617265643a2075696e7420696e70757420697320656d707479000000000060448201526064016101db565b61023f838561027b565b5050505061064a565b60006020835110156102645761025d836102b5565b9050610275565b8161026f84826104f9565b5060ff90505b92915050565b6001600160a01b0382166102a55760405163ec442f0560e01b8152600060048201526024016101db565b6102b1600083836102f3565b5050565b600080829050601f815111156102e0578260405163305a27a960e01b81526004016101db91906105b7565b80516102eb82610605565b179392505050565b6001600160a01b03831661031e5780600260008282546103139190610629565b909155506103909050565b6001600160a01b038316600090815260208190526040902054818110156103715760405163391434e360e21b81526001600160a01b038516600482015260248101829052604481018390526064016101db565b6001600160a01b03841660009081526020819052604090209082900390555b6001600160a01b0382166103ac576002805482900390556103cb565b6001600160a01b03821660009081526020819052604090208054820190555b816001600160a01b0316836001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8360405161041091815260200190565b60405180910390a3505050565b6000806040838503121561043057600080fd5b825160208401519092506001600160a01b038116811461044f57600080fd5b809150509250929050565b634e487b7160e01b600052604160045260246000fd5b600181811c9082168061048457607f821691505b6020821081036104a457634e487b7160e01b600052602260045260246000fd5b50919050565b601f8211156104f457806000526020600020601f840160051c810160208510156104d15750805b601f840160051c820191505b818110156104f157600081556001016104dd565b50505b505050565b81516001600160401b038111156105125761051261045a565b610526816105208454610470565b846104aa565b6020601f82116001811461055a57600083156105425750848201515b600019600385901b1c1916600184901b1784556104f1565b600084815260208120601f198516915b8281101561058a578785015182556020948501946001909201910161056a565b50848210156105a85786840151600019600387901b60f8161c191681555b50505050600190811b01905550565b602081526000825180602084015260005b818110156105e557602081860181015160408684010152016105c8565b506000604082850101526040601f19601f83011684010191505092915050565b805160208083015191908110156104a45760001960209190910360031b1b16919050565b8082018082111561027557634e487b7160e01b600052601160045260246000fd5b60805160a05160c05160e051610100516101205161014051610eb86106a460003960006106c80152600061069b015260006106430152600061061b01526000610576015260006105a0015260006105ca0152610eb86000f3fe608060405234801561001057600080fd5b50600436106100bf5760003560e01c806370a082311161007c57806370a08231146101415780637ecebe001461016a57806384b0196e1461017d57806395d89b4114610198578063a9059cbb146101a0578063d505accf146101b3578063dd62ed3e146101c857600080fd5b806306fdde03146100c4578063095ea7b3146100e257806318160ddd1461010557806323b872dd14610117578063313ce5671461012a5780633644e51514610139575b600080fd5b6100cc6101db565b6040516100d99190610c0f565b60405180910390f35b6100f56100f0366004610c45565b61026d565b60405190151581526020016100d9565b6002545b6040519081526020016100d9565b6100f5610125366004610c6f565b610287565b604051600981526020016100d9565b6101096102ab565b61010961014f366004610cac565b6001600160a01b031660009081526020819052604090205490565b610109610178366004610cac565b6102ba565b6101856102d8565b6040516100d99796959493929190610cc7565b6100cc61031e565b6100f56101ae366004610c45565b61032d565b6101c66101c1366004610d5f565b61033b565b005b6101096101d6366004610dd2565b61047a565b6060600380546101ea90610e05565b80601f016020809104026020016040519081016040528092919081815260200182805461021690610e05565b80156102635780601f1061023857610100808354040283529160200191610263565b820191906000526020600020905b81548152906001019060200180831161024657829003601f168201915b5050505050905090565b60003361027b8185856104a5565b60019150505b92915050565b6000336102958582856104b7565b6102a085858561050a565b506001949350505050565b60006102b5610569565b905090565b6001600160a01b038116600090815260076020526040812054610281565b6000606080600080600060606102ec610694565b6102f46106c1565b60408051600080825260208201909252600f60f81b9b939a50919850469750309650945092509050565b6060600480546101ea90610e05565b60003361027b81858561050a565b834211156103645760405163313c898160e11b8152600481018590526024015b60405180910390fd5b60007f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c98888886103b18c6001600160a01b0316600090815260076020526040902080546001810190915590565b6040805160208101969096526001600160a01b0394851690860152929091166060840152608083015260a082015260c0810186905260e001604051602081830303815290604052805190602001209050600061040c826106ee565b9050600061041c8287878761071b565b9050896001600160a01b0316816001600160a01b031614610463576040516325c0072360e11b81526001600160a01b0380831660048301528b16602482015260440161035b565b61046e8a8a8a6104a5565b50505050505050505050565b6001600160a01b03918216600090815260016020908152604080832093909416825291909152205490565b6104b28383836001610749565b505050565b60006104c3848461047a565b9050600019811461050457818110156104f557828183604051637dc7a0d960e11b815260040161035b93929190610e3f565b61050484848484036000610749565b50505050565b6001600160a01b038316610534576000604051634b637e8f60e11b815260040161035b9190610e60565b6001600160a01b03821661055e57600060405163ec442f0560e01b815260040161035b9190610e60565b6104b283838361081e565b6000306001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000161480156105c257507f000000000000000000000000000000000000000000000000000000000000000046145b156105ec57507f000000000000000000000000000000000000000000000000000000000000000090565b6102b5604080517f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f60208201527f0000000000000000000000000000000000000000000000000000000000000000918101919091527f000000000000000000000000000000000000000000000000000000000000000060608201524660808201523060a082015260009060c00160405160208183030381529060405280519060200120905090565b60606102b57f00000000000000000000000000000000000000000000000000000000000000006005610935565b60606102b57f00000000000000000000000000000000000000000000000000000000000000006006610935565b60006102816106fb610569565b8360405161190160f01b8152600281019290925260228201526042902090565b60008060008061072d888888886109e0565b92509250925061073d8282610aa5565b50909695505050505050565b6001600160a01b03841661077357600060405163e602df0560e01b815260040161035b9190610e60565b6001600160a01b03831661079d576000604051634a1406b160e11b815260040161035b9190610e60565b6001600160a01b038085166000908152600160209081526040808320938716835292905220829055801561050457826001600160a01b0316846001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9258460405161081091815260200190565b60405180910390a350505050565b6001600160a01b03831661084957806002600082825461083e9190610e74565b909155506108a89050565b6001600160a01b038316600090815260208190526040902054818110156108895783818360405163391434e360e21b815260040161035b93929190610e3f565b6001600160a01b03841660009081526020819052604090209082900390555b6001600160a01b0382166108c4576002805482900390556108e3565b6001600160a01b03821660009081526020819052604090208054820190555b816001600160a01b0316836001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8360405161092891815260200190565b60405180910390a3505050565b606060ff831461094f5761094883610b62565b9050610281565b81805461095b90610e05565b80601f016020809104026020016040519081016040528092919081815260200182805461098790610e05565b80156109d45780601f106109a9576101008083540402835291602001916109d4565b820191906000526020600020905b8154815290600101906020018083116109b757829003601f168201915b50505050509050610281565b600080806fa2a8918ca85bafe22016d0b997e4df60600160ff1b03841115610a115750600091506003905082610a9b565b604080516000808252602082018084528a905260ff891692820192909252606081018790526080810186905260019060a0016020604051602081039080840390855afa158015610a65573d6000803e3d6000fd5b5050604051601f1901519150506001600160a01b038116610a9157506000925060019150829050610a9b565b9250600091508190505b9450945094915050565b6000826003811115610ab957610ab9610e95565b03610ac2575050565b6001826003811115610ad657610ad6610e95565b03610af45760405163f645eedf60e01b815260040160405180910390fd5b6002826003811115610b0857610b08610e95565b03610b295760405163fce698f760e01b81526004810182905260240161035b565b6003826003811115610b3d57610b3d610e95565b03610b5e576040516335e2f38360e21b81526004810182905260240161035b565b5050565b60606000610b6f83610ba1565b604080516020808252818301909252919250600091906020820181803683375050509182525060208101929092525090565b600060ff8216601f81111561028157604051632cd44ac360e21b815260040160405180910390fd5b6000815180845260005b81811015610bef57602081850181015186830182015201610bd3565b506000602082860101526020601f19601f83011685010191505092915050565b602081526000610c226020830184610bc9565b9392505050565b80356001600160a01b0381168114610c4057600080fd5b919050565b60008060408385031215610c5857600080fd5b610c6183610c29565b946020939093013593505050565b600080600060608486031215610c8457600080fd5b610c8d84610c29565b9250610c9b60208501610c29565b929592945050506040919091013590565b600060208284031215610cbe57600080fd5b610c2282610c29565b60ff60f81b8816815260e060208201526000610ce660e0830189610bc9565b8281036040840152610cf88189610bc9565b606084018890526001600160a01b038716608085015260a0840186905283810360c08501528451808252602080870193509091019060005b81811015610d4e578351835260209384019390920191600101610d30565b50909b9a5050505050505050505050565b600080600080600080600060e0888a031215610d7a57600080fd5b610d8388610c29565b9650610d9160208901610c29565b95506040880135945060608801359350608088013560ff81168114610db557600080fd5b9699959850939692959460a0840135945060c09093013592915050565b60008060408385031215610de557600080fd5b610dee83610c29565b9150610dfc60208401610c29565b90509250929050565b600181811c90821680610e1957607f821691505b602082108103610e3957634e487b7160e01b600052602260045260246000fd5b50919050565b6001600160a01b039390931683526020830191909152604082015260600190565b6001600160a01b0391909116815260200190565b8082018082111561028157634e487b7160e01b600052601160045260246000fd5b634e487b7160e01b600052602160045260246000fdfea164736f6c634300081a000a", - "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100bf5760003560e01c806370a082311161007c57806370a08231146101415780637ecebe001461016a57806384b0196e1461017d57806395d89b4114610198578063a9059cbb146101a0578063d505accf146101b3578063dd62ed3e146101c857600080fd5b806306fdde03146100c4578063095ea7b3146100e257806318160ddd1461010557806323b872dd14610117578063313ce5671461012a5780633644e51514610139575b600080fd5b6100cc6101db565b6040516100d99190610c0f565b60405180910390f35b6100f56100f0366004610c45565b61026d565b60405190151581526020016100d9565b6002545b6040519081526020016100d9565b6100f5610125366004610c6f565b610287565b604051600981526020016100d9565b6101096102ab565b61010961014f366004610cac565b6001600160a01b031660009081526020819052604090205490565b610109610178366004610cac565b6102ba565b6101856102d8565b6040516100d99796959493929190610cc7565b6100cc61031e565b6100f56101ae366004610c45565b61032d565b6101c66101c1366004610d5f565b61033b565b005b6101096101d6366004610dd2565b61047a565b6060600380546101ea90610e05565b80601f016020809104026020016040519081016040528092919081815260200182805461021690610e05565b80156102635780601f1061023857610100808354040283529160200191610263565b820191906000526020600020905b81548152906001019060200180831161024657829003601f168201915b5050505050905090565b60003361027b8185856104a5565b60019150505b92915050565b6000336102958582856104b7565b6102a085858561050a565b506001949350505050565b60006102b5610569565b905090565b6001600160a01b038116600090815260076020526040812054610281565b6000606080600080600060606102ec610694565b6102f46106c1565b60408051600080825260208201909252600f60f81b9b939a50919850469750309650945092509050565b6060600480546101ea90610e05565b60003361027b81858561050a565b834211156103645760405163313c898160e11b8152600481018590526024015b60405180910390fd5b60007f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c98888886103b18c6001600160a01b0316600090815260076020526040902080546001810190915590565b6040805160208101969096526001600160a01b0394851690860152929091166060840152608083015260a082015260c0810186905260e001604051602081830303815290604052805190602001209050600061040c826106ee565b9050600061041c8287878761071b565b9050896001600160a01b0316816001600160a01b031614610463576040516325c0072360e11b81526001600160a01b0380831660048301528b16602482015260440161035b565b61046e8a8a8a6104a5565b50505050505050505050565b6001600160a01b03918216600090815260016020908152604080832093909416825291909152205490565b6104b28383836001610749565b505050565b60006104c3848461047a565b9050600019811461050457818110156104f557828183604051637dc7a0d960e11b815260040161035b93929190610e3f565b61050484848484036000610749565b50505050565b6001600160a01b038316610534576000604051634b637e8f60e11b815260040161035b9190610e60565b6001600160a01b03821661055e57600060405163ec442f0560e01b815260040161035b9190610e60565b6104b283838361081e565b6000306001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000161480156105c257507f000000000000000000000000000000000000000000000000000000000000000046145b156105ec57507f000000000000000000000000000000000000000000000000000000000000000090565b6102b5604080517f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f60208201527f0000000000000000000000000000000000000000000000000000000000000000918101919091527f000000000000000000000000000000000000000000000000000000000000000060608201524660808201523060a082015260009060c00160405160208183030381529060405280519060200120905090565b60606102b57f00000000000000000000000000000000000000000000000000000000000000006005610935565b60606102b57f00000000000000000000000000000000000000000000000000000000000000006006610935565b60006102816106fb610569565b8360405161190160f01b8152600281019290925260228201526042902090565b60008060008061072d888888886109e0565b92509250925061073d8282610aa5565b50909695505050505050565b6001600160a01b03841661077357600060405163e602df0560e01b815260040161035b9190610e60565b6001600160a01b03831661079d576000604051634a1406b160e11b815260040161035b9190610e60565b6001600160a01b038085166000908152600160209081526040808320938716835292905220829055801561050457826001600160a01b0316846001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9258460405161081091815260200190565b60405180910390a350505050565b6001600160a01b03831661084957806002600082825461083e9190610e74565b909155506108a89050565b6001600160a01b038316600090815260208190526040902054818110156108895783818360405163391434e360e21b815260040161035b93929190610e3f565b6001600160a01b03841660009081526020819052604090209082900390555b6001600160a01b0382166108c4576002805482900390556108e3565b6001600160a01b03821660009081526020819052604090208054820190555b816001600160a01b0316836001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8360405161092891815260200190565b60405180910390a3505050565b606060ff831461094f5761094883610b62565b9050610281565b81805461095b90610e05565b80601f016020809104026020016040519081016040528092919081815260200182805461098790610e05565b80156109d45780601f106109a9576101008083540402835291602001916109d4565b820191906000526020600020905b8154815290600101906020018083116109b757829003601f168201915b50505050509050610281565b600080806fa2a8918ca85bafe22016d0b997e4df60600160ff1b03841115610a115750600091506003905082610a9b565b604080516000808252602082018084528a905260ff891692820192909252606081018790526080810186905260019060a0016020604051602081039080840390855afa158015610a65573d6000803e3d6000fd5b5050604051601f1901519150506001600160a01b038116610a9157506000925060019150829050610a9b565b9250600091508190505b9450945094915050565b6000826003811115610ab957610ab9610e95565b03610ac2575050565b6001826003811115610ad657610ad6610e95565b03610af45760405163f645eedf60e01b815260040160405180910390fd5b6002826003811115610b0857610b08610e95565b03610b295760405163fce698f760e01b81526004810182905260240161035b565b6003826003811115610b3d57610b3d610e95565b03610b5e576040516335e2f38360e21b81526004810182905260240161035b565b5050565b60606000610b6f83610ba1565b604080516020808252818301909252919250600091906020820181803683375050509182525060208101929092525090565b600060ff8216601f81111561028157604051632cd44ac360e21b815260040160405180910390fd5b6000815180845260005b81811015610bef57602081850181015186830182015201610bd3565b506000602082860101526020601f19601f83011685010191505092915050565b602081526000610c226020830184610bc9565b9392505050565b80356001600160a01b0381168114610c4057600080fd5b919050565b60008060408385031215610c5857600080fd5b610c6183610c29565b946020939093013593505050565b600080600060608486031215610c8457600080fd5b610c8d84610c29565b9250610c9b60208501610c29565b929592945050506040919091013590565b600060208284031215610cbe57600080fd5b610c2282610c29565b60ff60f81b8816815260e060208201526000610ce660e0830189610bc9565b8281036040840152610cf88189610bc9565b606084018890526001600160a01b038716608085015260a0840186905283810360c08501528451808252602080870193509091019060005b81811015610d4e578351835260209384019390920191600101610d30565b50909b9a5050505050505050505050565b600080600080600080600060e0888a031215610d7a57600080fd5b610d8388610c29565b9650610d9160208901610c29565b95506040880135945060608801359350608088013560ff81168114610db557600080fd5b9699959850939692959460a0840135945060c09093013592915050565b60008060408385031215610de557600080fd5b610dee83610c29565b9150610dfc60208401610c29565b90509250929050565b600181811c90821680610e1957607f821691505b602082108103610e3957634e487b7160e01b600052602260045260246000fd5b50919050565b6001600160a01b039390931683526020830191909152604082015260600190565b6001600160a01b0391909116815260200190565b8082018082111561028157634e487b7160e01b600052601160045260246000fd5b634e487b7160e01b600052602160045260246000fdfea164736f6c634300081a000a", + "bytecode": "0x61016060405234801561001157600080fd5b5060405161155838038061155883398101604081905261003091610429565b6040518060400160405280600d81526020016c29b2b9b9b4b7b7102a37b5b2b760991b81525080604051806040016040528060018152602001603160f81b8152506040518060400160405280600d81526020016c29b2b9b9b4b7b7102a37b5b2b760991b815250604051806040016040528060048152602001630a68aa6960e31b81525081600390816100c39190610505565b5060046100d08282610505565b506100e091508390506005610254565b610120526100ef816006610254565b61014052815160208084019190912060e052815190820120610100524660a05261017c60e05161010051604080517f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f60208201529081019290925260608201524660808201523060a082015260009060c00160405160208183030381529060405280519060200120905090565b60805250503060c05250806001600160a01b0381166101f05760405162461bcd60e51b815260206004820152602560248201527f5368617265643a205a65726f2d61646472657373206973206e6f74207065726d6044820152641a5d1d195960da1b60648201526084015b60405180910390fd5b82806000036102415760405162461bcd60e51b815260206004820152601b60248201527f5368617265643a2075696e7420696e70757420697320656d707479000000000060448201526064016101e7565b61024b8385610287565b50505050610656565b600060208351101561027057610269836102c1565b9050610281565b8161027b8482610505565b5060ff90505b92915050565b6001600160a01b0382166102b15760405163ec442f0560e01b8152600060048201526024016101e7565b6102bd600083836102ff565b5050565b600080829050601f815111156102ec578260405163305a27a960e01b81526004016101e791906105c3565b80516102f782610611565b179392505050565b6001600160a01b03831661032a57806002600082825461031f9190610635565b9091555061039c9050565b6001600160a01b0383166000908152602081905260409020548181101561037d5760405163391434e360e21b81526001600160a01b038516600482015260248101829052604481018390526064016101e7565b6001600160a01b03841660009081526020819052604090209082900390555b6001600160a01b0382166103b8576002805482900390556103d7565b6001600160a01b03821660009081526020819052604090208054820190555b816001600160a01b0316836001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8360405161041c91815260200190565b60405180910390a3505050565b6000806040838503121561043c57600080fd5b825160208401519092506001600160a01b038116811461045b57600080fd5b809150509250929050565b634e487b7160e01b600052604160045260246000fd5b600181811c9082168061049057607f821691505b6020821081036104b057634e487b7160e01b600052602260045260246000fd5b50919050565b601f82111561050057806000526020600020601f840160051c810160208510156104dd5750805b601f840160051c820191505b818110156104fd57600081556001016104e9565b50505b505050565b81516001600160401b0381111561051e5761051e610466565b6105328161052c845461047c565b846104b6565b6020601f821160018114610566576000831561054e5750848201515b600019600385901b1c1916600184901b1784556104fd565b600084815260208120601f198516915b828110156105965787850151825560209485019460019092019101610576565b50848210156105b45786840151600019600387901b60f8161c191681555b50505050600190811b01905550565b602081526000825180602084015260005b818110156105f157602081860181015160408684010152016105d4565b506000604082850101526040601f19601f83011684010191505092915050565b805160208083015191908110156104b05760001960209190910360031b1b16919050565b8082018082111561028157634e487b7160e01b600052601160045260246000fd5b60805160a05160c05160e051610100516101205161014051610ea86106b060003960006106b80152600061068b015260006106330152600061060b0152600061056601526000610590015260006105ba0152610ea86000f3fe608060405234801561001057600080fd5b50600436106100af5760003560e01c806306fdde03146100b4578063095ea7b3146100d257806318160ddd146100f557806323b872dd14610107578063313ce5671461011a5780633644e5151461012957806370a08231146101315780637ecebe001461015a57806384b0196e1461016d57806395d89b4114610188578063a9059cbb14610190578063d505accf146101a3578063dd62ed3e146101b8575b600080fd5b6100bc6101cb565b6040516100c99190610bff565b60405180910390f35b6100e56100e0366004610c35565b61025d565b60405190151581526020016100c9565b6002545b6040519081526020016100c9565b6100e5610115366004610c5f565b610277565b604051600981526020016100c9565b6100f961029b565b6100f961013f366004610c9c565b6001600160a01b031660009081526020819052604090205490565b6100f9610168366004610c9c565b6102aa565b6101756102c8565b6040516100c99796959493929190610cb7565b6100bc61030e565b6100e561019e366004610c35565b61031d565b6101b66101b1366004610d4f565b61032b565b005b6100f96101c6366004610dc2565b61046a565b6060600380546101da90610df5565b80601f016020809104026020016040519081016040528092919081815260200182805461020690610df5565b80156102535780601f1061022857610100808354040283529160200191610253565b820191906000526020600020905b81548152906001019060200180831161023657829003601f168201915b5050505050905090565b60003361026b818585610495565b60019150505b92915050565b6000336102858582856104a7565b6102908585856104fa565b506001949350505050565b60006102a5610559565b905090565b6001600160a01b038116600090815260076020526040812054610271565b6000606080600080600060606102dc610684565b6102e46106b1565b60408051600080825260208201909252600f60f81b9b939a50919850469750309650945092509050565b6060600480546101da90610df5565b60003361026b8185856104fa565b834211156103545760405163313c898160e11b8152600481018590526024015b60405180910390fd5b60007f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c98888886103a18c6001600160a01b0316600090815260076020526040902080546001810190915590565b6040805160208101969096526001600160a01b0394851690860152929091166060840152608083015260a082015260c0810186905260e00160405160208183030381529060405280519060200120905060006103fc826106de565b9050600061040c8287878761070b565b9050896001600160a01b0316816001600160a01b031614610453576040516325c0072360e11b81526001600160a01b0380831660048301528b16602482015260440161034b565b61045e8a8a8a610495565b50505050505050505050565b6001600160a01b03918216600090815260016020908152604080832093909416825291909152205490565b6104a28383836001610739565b505050565b60006104b3848461046a565b905060001981146104f457818110156104e557828183604051637dc7a0d960e11b815260040161034b93929190610e2f565b6104f484848484036000610739565b50505050565b6001600160a01b038316610524576000604051634b637e8f60e11b815260040161034b9190610e50565b6001600160a01b03821661054e57600060405163ec442f0560e01b815260040161034b9190610e50565b6104a283838361080e565b6000306001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000161480156105b257507f000000000000000000000000000000000000000000000000000000000000000046145b156105dc57507f000000000000000000000000000000000000000000000000000000000000000090565b6102a5604080517f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f60208201527f0000000000000000000000000000000000000000000000000000000000000000918101919091527f000000000000000000000000000000000000000000000000000000000000000060608201524660808201523060a082015260009060c00160405160208183030381529060405280519060200120905090565b60606102a57f00000000000000000000000000000000000000000000000000000000000000006005610925565b60606102a57f00000000000000000000000000000000000000000000000000000000000000006006610925565b60006102716106eb610559565b8360405161190160f01b8152600281019290925260228201526042902090565b60008060008061071d888888886109d0565b92509250925061072d8282610a95565b50909695505050505050565b6001600160a01b03841661076357600060405163e602df0560e01b815260040161034b9190610e50565b6001600160a01b03831661078d576000604051634a1406b160e11b815260040161034b9190610e50565b6001600160a01b03808516600090815260016020908152604080832093871683529290522082905580156104f457826001600160a01b0316846001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9258460405161080091815260200190565b60405180910390a350505050565b6001600160a01b03831661083957806002600082825461082e9190610e64565b909155506108989050565b6001600160a01b038316600090815260208190526040902054818110156108795783818360405163391434e360e21b815260040161034b93929190610e2f565b6001600160a01b03841660009081526020819052604090209082900390555b6001600160a01b0382166108b4576002805482900390556108d3565b6001600160a01b03821660009081526020819052604090208054820190555b816001600160a01b0316836001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8360405161091891815260200190565b60405180910390a3505050565b606060ff831461093f5761093883610b52565b9050610271565b81805461094b90610df5565b80601f016020809104026020016040519081016040528092919081815260200182805461097790610df5565b80156109c45780601f10610999576101008083540402835291602001916109c4565b820191906000526020600020905b8154815290600101906020018083116109a757829003601f168201915b50505050509050610271565b600080806fa2a8918ca85bafe22016d0b997e4df60600160ff1b03841115610a015750600091506003905082610a8b565b604080516000808252602082018084528a905260ff891692820192909252606081018790526080810186905260019060a0016020604051602081039080840390855afa158015610a55573d6000803e3d6000fd5b5050604051601f1901519150506001600160a01b038116610a8157506000925060019150829050610a8b565b9250600091508190505b9450945094915050565b6000826003811115610aa957610aa9610e85565b03610ab2575050565b6001826003811115610ac657610ac6610e85565b03610ae45760405163f645eedf60e01b815260040160405180910390fd5b6002826003811115610af857610af8610e85565b03610b195760405163fce698f760e01b81526004810182905260240161034b565b6003826003811115610b2d57610b2d610e85565b03610b4e576040516335e2f38360e21b81526004810182905260240161034b565b5050565b60606000610b5f83610b91565b604080516020808252818301909252919250600091906020820181803683375050509182525060208101929092525090565b600060ff8216601f81111561027157604051632cd44ac360e21b815260040160405180910390fd5b6000815180845260005b81811015610bdf57602081850181015186830182015201610bc3565b506000602082860101526020601f19601f83011685010191505092915050565b602081526000610c126020830184610bb9565b9392505050565b80356001600160a01b0381168114610c3057600080fd5b919050565b60008060408385031215610c4857600080fd5b610c5183610c19565b946020939093013593505050565b600080600060608486031215610c7457600080fd5b610c7d84610c19565b9250610c8b60208501610c19565b929592945050506040919091013590565b600060208284031215610cae57600080fd5b610c1282610c19565b60ff60f81b8816815260e060208201526000610cd660e0830189610bb9565b8281036040840152610ce88189610bb9565b606084018890526001600160a01b038716608085015260a0840186905283810360c08501528451808252602080870193509091019060005b81811015610d3e578351835260209384019390920191600101610d20565b50909b9a5050505050505050505050565b600080600080600080600060e0888a031215610d6a57600080fd5b610d7388610c19565b9650610d8160208901610c19565b95506040880135945060608801359350608088013560ff81168114610da557600080fd5b9699959850939692959460a0840135945060c09093013592915050565b60008060408385031215610dd557600080fd5b610dde83610c19565b9150610dec60208401610c19565b90509250929050565b600181811c90821680610e0957607f821691505b602082108103610e2957634e487b7160e01b600052602260045260246000fd5b50919050565b6001600160a01b039390931683526020830191909152604082015260600190565b6001600160a01b0391909116815260200190565b8082018082111561027157634e487b7160e01b600052601160045260246000fd5b634e487b7160e01b600052602160045260246000fdfea164736f6c634300081a000a", + "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100af5760003560e01c806306fdde03146100b4578063095ea7b3146100d257806318160ddd146100f557806323b872dd14610107578063313ce5671461011a5780633644e5151461012957806370a08231146101315780637ecebe001461015a57806384b0196e1461016d57806395d89b4114610188578063a9059cbb14610190578063d505accf146101a3578063dd62ed3e146101b8575b600080fd5b6100bc6101cb565b6040516100c99190610bff565b60405180910390f35b6100e56100e0366004610c35565b61025d565b60405190151581526020016100c9565b6002545b6040519081526020016100c9565b6100e5610115366004610c5f565b610277565b604051600981526020016100c9565b6100f961029b565b6100f961013f366004610c9c565b6001600160a01b031660009081526020819052604090205490565b6100f9610168366004610c9c565b6102aa565b6101756102c8565b6040516100c99796959493929190610cb7565b6100bc61030e565b6100e561019e366004610c35565b61031d565b6101b66101b1366004610d4f565b61032b565b005b6100f96101c6366004610dc2565b61046a565b6060600380546101da90610df5565b80601f016020809104026020016040519081016040528092919081815260200182805461020690610df5565b80156102535780601f1061022857610100808354040283529160200191610253565b820191906000526020600020905b81548152906001019060200180831161023657829003601f168201915b5050505050905090565b60003361026b818585610495565b60019150505b92915050565b6000336102858582856104a7565b6102908585856104fa565b506001949350505050565b60006102a5610559565b905090565b6001600160a01b038116600090815260076020526040812054610271565b6000606080600080600060606102dc610684565b6102e46106b1565b60408051600080825260208201909252600f60f81b9b939a50919850469750309650945092509050565b6060600480546101da90610df5565b60003361026b8185856104fa565b834211156103545760405163313c898160e11b8152600481018590526024015b60405180910390fd5b60007f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c98888886103a18c6001600160a01b0316600090815260076020526040902080546001810190915590565b6040805160208101969096526001600160a01b0394851690860152929091166060840152608083015260a082015260c0810186905260e00160405160208183030381529060405280519060200120905060006103fc826106de565b9050600061040c8287878761070b565b9050896001600160a01b0316816001600160a01b031614610453576040516325c0072360e11b81526001600160a01b0380831660048301528b16602482015260440161034b565b61045e8a8a8a610495565b50505050505050505050565b6001600160a01b03918216600090815260016020908152604080832093909416825291909152205490565b6104a28383836001610739565b505050565b60006104b3848461046a565b905060001981146104f457818110156104e557828183604051637dc7a0d960e11b815260040161034b93929190610e2f565b6104f484848484036000610739565b50505050565b6001600160a01b038316610524576000604051634b637e8f60e11b815260040161034b9190610e50565b6001600160a01b03821661054e57600060405163ec442f0560e01b815260040161034b9190610e50565b6104a283838361080e565b6000306001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000161480156105b257507f000000000000000000000000000000000000000000000000000000000000000046145b156105dc57507f000000000000000000000000000000000000000000000000000000000000000090565b6102a5604080517f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f60208201527f0000000000000000000000000000000000000000000000000000000000000000918101919091527f000000000000000000000000000000000000000000000000000000000000000060608201524660808201523060a082015260009060c00160405160208183030381529060405280519060200120905090565b60606102a57f00000000000000000000000000000000000000000000000000000000000000006005610925565b60606102a57f00000000000000000000000000000000000000000000000000000000000000006006610925565b60006102716106eb610559565b8360405161190160f01b8152600281019290925260228201526042902090565b60008060008061071d888888886109d0565b92509250925061072d8282610a95565b50909695505050505050565b6001600160a01b03841661076357600060405163e602df0560e01b815260040161034b9190610e50565b6001600160a01b03831661078d576000604051634a1406b160e11b815260040161034b9190610e50565b6001600160a01b03808516600090815260016020908152604080832093871683529290522082905580156104f457826001600160a01b0316846001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9258460405161080091815260200190565b60405180910390a350505050565b6001600160a01b03831661083957806002600082825461082e9190610e64565b909155506108989050565b6001600160a01b038316600090815260208190526040902054818110156108795783818360405163391434e360e21b815260040161034b93929190610e2f565b6001600160a01b03841660009081526020819052604090209082900390555b6001600160a01b0382166108b4576002805482900390556108d3565b6001600160a01b03821660009081526020819052604090208054820190555b816001600160a01b0316836001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8360405161091891815260200190565b60405180910390a3505050565b606060ff831461093f5761093883610b52565b9050610271565b81805461094b90610df5565b80601f016020809104026020016040519081016040528092919081815260200182805461097790610df5565b80156109c45780601f10610999576101008083540402835291602001916109c4565b820191906000526020600020905b8154815290600101906020018083116109a757829003601f168201915b50505050509050610271565b600080806fa2a8918ca85bafe22016d0b997e4df60600160ff1b03841115610a015750600091506003905082610a8b565b604080516000808252602082018084528a905260ff891692820192909252606081018790526080810186905260019060a0016020604051602081039080840390855afa158015610a55573d6000803e3d6000fd5b5050604051601f1901519150506001600160a01b038116610a8157506000925060019150829050610a8b565b9250600091508190505b9450945094915050565b6000826003811115610aa957610aa9610e85565b03610ab2575050565b6001826003811115610ac657610ac6610e85565b03610ae45760405163f645eedf60e01b815260040160405180910390fd5b6002826003811115610af857610af8610e85565b03610b195760405163fce698f760e01b81526004810182905260240161034b565b6003826003811115610b2d57610b2d610e85565b03610b4e576040516335e2f38360e21b81526004810182905260240161034b565b5050565b60606000610b5f83610b91565b604080516020808252818301909252919250600091906020820181803683375050509182525060208101929092525090565b600060ff8216601f81111561027157604051632cd44ac360e21b815260040160405180910390fd5b6000815180845260005b81811015610bdf57602081850181015186830182015201610bc3565b506000602082860101526020601f19601f83011685010191505092915050565b602081526000610c126020830184610bb9565b9392505050565b80356001600160a01b0381168114610c3057600080fd5b919050565b60008060408385031215610c4857600080fd5b610c5183610c19565b946020939093013593505050565b600080600060608486031215610c7457600080fd5b610c7d84610c19565b9250610c8b60208501610c19565b929592945050506040919091013590565b600060208284031215610cae57600080fd5b610c1282610c19565b60ff60f81b8816815260e060208201526000610cd660e0830189610bb9565b8281036040840152610ce88189610bb9565b606084018890526001600160a01b038716608085015260a0840186905283810360c08501528451808252602080870193509091019060005b81811015610d3e578351835260209384019390920191600101610d20565b50909b9a5050505050505050505050565b600080600080600080600060e0888a031215610d6a57600080fd5b610d7388610c19565b9650610d8160208901610c19565b95506040880135945060608801359350608088013560ff81168114610da557600080fd5b9699959850939692959460a0840135945060c09093013592915050565b60008060408385031215610dd557600080fd5b610dde83610c19565b9150610dec60208401610c19565b90509250929050565b600181811c90821680610e0957607f821691505b602082108103610e2957634e487b7160e01b600052602260045260246000fd5b50919050565b6001600160a01b039390931683526020830191909152604082015260600190565b6001600160a01b0391909116815260200190565b8082018082111561027157634e487b7160e01b600052601160045260246000fd5b634e487b7160e01b600052602160045260246000fdfea164736f6c634300081a000a", "linkReferences": {}, "deployedLinkReferences": {} } diff --git a/src/web3client/abis/TokenVestingStaking.json b/src/web3client/abis/TokenVestingStaking.json new file mode 100644 index 0000000..272f92d --- /dev/null +++ b/src/web3client/abis/TokenVestingStaking.json @@ -0,0 +1,537 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "TokenVestingStaking", + "sourceName": "contracts/utils/TokenVestingStaking.sol", + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "beneficiary_", + "type": "address" + }, + { + "internalType": "address", + "name": "revoker_", + "type": "address" + }, + { + "internalType": "uint256", + "name": "start_", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "end_", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "transferableBeneficiary_", + "type": "bool" + }, + { + "internalType": "contract IServiceNodeRewards", + "name": "rewardsContract_", + "type": "address" + }, + { + "internalType": "contract IServiceNodeContributionFactory", + "name": "snContribFactory_", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "sesh_", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "target", + "type": "address" + } + ], + "name": "AddressEmptyCode", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "AddressInsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "FailedInnerCall", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "SafeERC20FailedOperation", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldBeneficiary", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newBeneficiary", + "type": "address" + } + ], + "name": "BeneficiaryTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "oldRevoker", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newRevoker", + "type": "address" + } + ], + "name": "RevokerTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IERC20", + "name": "token", + "type": "address" + } + ], + "name": "TokenVestingRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "TokensReleased", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "TokensRevokedReleased", + "type": "event" + }, + { + "inputs": [], + "name": "SESH", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "X", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "Y", + "type": "uint256" + } + ], + "internalType": "struct BN256G1.G1Point", + "name": "blsPubkey", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "sigs0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "sigs1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "sigs2", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "sigs3", + "type": "uint256" + } + ], + "internalType": "struct IServiceNodeRewards.BLSSignatureParams", + "name": "blsSignature", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "serviceNodePubkey", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "serviceNodeSignature1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "serviceNodeSignature2", + "type": "uint256" + }, + { + "internalType": "uint16", + "name": "fee", + "type": "uint16" + } + ], + "internalType": "struct IServiceNodeRewards.ServiceNodeParams", + "name": "serviceNodeParams", + "type": "tuple" + }, + { + "internalType": "address", + "name": "snBeneficiary", + "type": "address" + } + ], + "name": "addBLSPublicKey", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "beneficiary", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "claimRewards", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "claimRewards", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "snContribAddr", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "snContribBeneficiary", + "type": "address" + } + ], + "name": "contributeFunds", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "end", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "serviceNodeID", + "type": "uint64" + } + ], + "name": "initiateExitBLSPublicKey", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + } + ], + "name": "release", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + } + ], + "name": "revoke", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "revoked", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "revoker", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rewardsContract", + "outputs": [ + { + "internalType": "contract IServiceNodeRewards", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "snContribFactory", + "outputs": [ + { + "internalType": "contract IServiceNodeContributionFactory", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "start", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "beneficiary_", + "type": "address" + } + ], + "name": "transferBeneficiary", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "revoker_", + "type": "address" + } + ], + "name": "transferRevoker", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "transferableBeneficiary", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "snContribAddr", + "type": "address" + }, + { + "internalType": "address", + "name": "snContribBeneficiary", + "type": "address" + } + ], + "name": "updateBeneficiary", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "factoryAddr", + "type": "address" + } + ], + "name": "updateContributionFactory", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "snContribAddr", + "type": "address" + } + ], + "name": "withdrawContribution", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x61012060405234801561001157600080fd5b50604051611ddc380380611ddc833981016040819052610030916101d1565b876001600160a01b0381166100605760405162461bcd60e51b815260040161005790610273565b60405180910390fd5b836001600160a01b0381166100875760405162461bcd60e51b815260040161005790610273565b826001600160a01b0381166100ae5760405162461bcd60e51b815260040161005790610273565b878911156100fe5760405162461bcd60e51b815260206004820152601a60248201527f56657374696e673a2073746172745f20616674657220656e645f0000000000006044820152606401610057565b8842106101585760405162461bcd60e51b815260206004820152602260248201527f56657374696e673a207374617274206265666f72652063757272656e742074696044820152616d6560f01b6064820152608401610057565b5050600080546001600160a01b03199081166001600160a01b039b8c1617909155600180548216998b16999099179098555060a09590955260c09390935290151560805284166101005260028054909316908416179091551660e0526102b8565b6001600160a01b03811681146101ce57600080fd5b50565b600080600080600080600080610100898b0312156101ee57600080fd5b88516101f9816101b9565b60208a015190985061020a816101b9565b60408a015160608b015160808c01519299509097509550801515811461022f57600080fd5b60a08a0151909450610240816101b9565b60c08a0151909350610251816101b9565b60e08a0151909250610262816101b9565b809150509295985092959890939650565b60208082526025908201527f5368617265643a205a65726f2d61646472657373206973206e6f74207065726d6040820152641a5d1d195960da1b606082015260800190565b60805160a05160c05160e05161010051611a6261037a6000396000818161015f015281816103f5015281816107ec01528181610db201528181610f1201528181610f9701526112a00152600081816102cf015281816109560152610ee501526000818161030901528181610bbb01526112db01526000818161029a015281816103a1015281816107ac015281816108cc01528181610aa701528181610d490152818161107a01526112440152600081816101dd01526104b00152611a626000f3fe608060405234801561001057600080fd5b50600436106101075760003560e01c80630962ef791461010c57806314bbe21c146101215780631916558714610134578063205a306114610147578063220cce971461015a57806335dec5d114610197578063372500ab146101aa57806338af3eed146101b257806343cac780146101c557806352208a5f146101d857806363d256ce1461020f5780637249d9d91461022357806374a8f103146102365780638a0b4e991461024957806395e3fd371461025c578063a35c47091461026f578063ac8d6c0c14610282578063be9a655514610295578063c3aca558146102ca578063d9054b09146102f1578063efbe1c1c14610304575b600080fd5b61011f61011a366004611657565b61032b565b005b61011f61012f366004611685565b61045d565b61011f610142366004611685565b610593565b61011f610155366004611685565b6106cb565b6101817f000000000000000000000000000000000000000000000000000000000000000081565b60405161018e91906116a2565b60405180910390f35b600154610181906001600160a01b031681565b61011f61073f565b600054610181906001600160a01b031681565b61011f6101d33660046116b6565b61085f565b6101ff7f000000000000000000000000000000000000000000000000000000000000000081565b604051901515815260200161018e565b6001546101ff90600160a01b900460ff1681565b61011f6102313660046116f8565b610a3a565b61011f610244366004611685565b610b7e565b600254610181906001600160a01b031681565b61011f61026a366004611743565b610cdc565b61011f61027d366004611685565b61100d565b61011f610290366004611685565b61111c565b6102bc7f000000000000000000000000000000000000000000000000000000000000000081565b60405190815260200161018e565b6101817f000000000000000000000000000000000000000000000000000000000000000081565b61011f6102ff3660046117a5565b6111d7565b6102bc7f000000000000000000000000000000000000000000000000000000000000000081565b600154600160a01b900460ff1615610375576001546001600160a01b031633146103705760405162461bcd60e51b8152600401610367906117ce565b60405180910390fd5b61039f565b6000546001600160a01b0316331461039f5760405162461bcd60e51b815260040161036790611805565b7f00000000000000000000000000000000000000000000000000000000000000004210156103df5760405162461bcd60e51b815260040161036790611848565b604051630962ef7960e01b8152600481018290527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031690630962ef79906024015b600060405180830381600087803b15801561044257600080fd5b505af1158015610456573d6000803e3d6000fd5b5050505050565b6000546001600160a01b031633146104875760405162461bcd60e51b815260040161036790611805565b806001600160a01b0381166104ae5760405162461bcd60e51b815260040161036790611876565b7f00000000000000000000000000000000000000000000000000000000000000006105295760405162461bcd60e51b815260206004820152602560248201527f56657374696e673a2062656e6566696369617279206e6f74207472616e7366656044820152647261626c6560d81b6064820152608401610367565b6000546040517f57005c5083fa0952870a7906715a2f6f9ef2d01b4a423e4b3ce59c6129b1a76391610568916001600160a01b039091169085906118bb565b60405180910390a150600080546001600160a01b0319166001600160a01b0392909216919091179055565b6000546001600160a01b031633146105bd5760405162461bcd60e51b815260040161036790611805565b600154600160a01b900460ff16156106105760405162461bcd60e51b815260206004820152601660248201527515995cdd1a5b99ce881d1bdad95b881c995d9bdad95960521b6044820152606401610367565b600061061b826112d7565b90506000811161066a5760405162461bcd60e51b815260206004820152601a60248201527956657374696e673a206e6f20746f6b656e73206172652064756560301b6044820152606401610367565b816001600160a01b03167fc7798891864187665ac6dd119286e44ec13f014527aeeb2b8eb3fd413df93179826040516106a591815260200190565b60405180910390a26000546106c7906001600160a01b0384811691168361137b565b5050565b6001546001600160a01b031633146106f55760405162461bcd60e51b8152600401610367906117ce565b806001600160a01b03811661071c5760405162461bcd60e51b815260040161036790611876565b50600280546001600160a01b0319166001600160a01b0392909216919091179055565b600154600160a01b900460ff1615610780576001546001600160a01b0316331461077b5760405162461bcd60e51b8152600401610367906117ce565b6107aa565b6000546001600160a01b031633146107aa5760405162461bcd60e51b815260040161036790611805565b7f00000000000000000000000000000000000000000000000000000000000000004210156107ea5760405162461bcd60e51b815260040161036790611848565b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663372500ab6040518163ffffffff1660e01b8152600401600060405180830381600087803b15801561084557600080fd5b505af1158015610859573d6000803e3d6000fd5b50505050565b600154600160a01b900460ff16156108a0576001546001600160a01b0316331461089b5760405162461bcd60e51b8152600401610367906117ce565b6108ca565b6000546001600160a01b031633146108ca5760405162461bcd60e51b815260040161036790611805565b7f000000000000000000000000000000000000000000000000000000000000000042101561090a5760405162461bcd60e51b815260040161036790611848565b806001600160a01b0381166109315760405162461bcd60e51b815260040161036790611876565b600061093c856113d8565b60405163095ea7b360e01b81529091506001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169063095ea7b39061098d90889088906004016118d5565b6020604051808303816000875af11580156109ac573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109d091906118ee565b50604051632816ee7360e01b8152600481018590526001600160a01b038481166024830152821690632816ee7390604401600060405180830381600087803b158015610a1b57600080fd5b505af1158015610a2f573d6000803e3d6000fd5b505050505050505050565b600154600160a01b900460ff1615610a7b576001546001600160a01b03163314610a765760405162461bcd60e51b8152600401610367906117ce565b610aa5565b6000546001600160a01b03163314610aa55760405162461bcd60e51b815260040161036790611805565b7f0000000000000000000000000000000000000000000000000000000000000000421015610ae55760405162461bcd60e51b815260040161036790611848565b806001600160a01b038116610b0c5760405162461bcd60e51b815260040161036790611876565b6000610b17846113d8565b604051630557fe9560e11b81529091506001600160a01b03821690630aaffd2a90610b469086906004016116a2565b600060405180830381600087803b158015610b6057600080fd5b505af1158015610b74573d6000803e3d6000fd5b5050505050505050565b6001546001600160a01b03163314610ba85760405162461bcd60e51b8152600401610367906117ce565b600154600160a01b900460ff16610c6c577f0000000000000000000000000000000000000000000000000000000000000000421115610c245760405162461bcd60e51b815260206004820152601860248201527715995cdd1a5b99ce881d995cdd1a5b99c8195e1c1a5c995960421b6044820152606401610367565b6001805460ff60a01b1916600160a01b1790556040516001600160a01b038216907f39983c6d4d174a7aee564f449d4a5c3c7ac9649d72b7793c56901183996f8af690600090a25b6000610c77826112d7565b905080156106c757816001600160a01b03167fa415e62e43678c8b550c180a7ef9a2031e826b306d4052104d288f3c83ac964b82604051610cba91815260200190565b60405180910390a26001546106c7906001600160a01b0384811691168361137b565b600154600160a01b900460ff1615610d1d576001546001600160a01b03163314610d185760405162461bcd60e51b8152600401610367906117ce565b610d47565b6000546001600160a01b03163314610d475760405162461bcd60e51b815260040161036790611805565b7f0000000000000000000000000000000000000000000000000000000000000000421015610d875760405162461bcd60e51b815260040161036790611848565b806001600160a01b038116610dae5760405162461bcd60e51b815260040161036790611876565b60007f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166356d399e86040518163ffffffff1660e01b8152600401602060405180830381865afa158015610e0e573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610e329190611910565b60408051600180825281830190925291925060009190816020015b6040805160808101825260009181018281526060820183905281526020810191909152815260200190600190039081610e4d57505060408051608081018252309181019182526001600160a01b0387166060820152908152602081018490528151919250908290600090610ec357610ec3611929565b602090810291909101015260405163095ea7b360e01b81526001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169063095ea7b390610f3c907f00000000000000000000000000000000000000000000000000000000000000009086906004016118d5565b6020604051808303816000875af1158015610f5b573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610f7f91906118ee565b50604051632f1fbfb360e21b81526001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169063bc7efecc90610fd2908a908a908a90879060040161199e565b600060405180830381600087803b158015610fec57600080fd5b505af1158015611000573d6000803e3d6000fd5b5050505050505050505050565b600154600160a01b900460ff161561104e576001546001600160a01b031633146110495760405162461bcd60e51b8152600401610367906117ce565b611078565b6000546001600160a01b031633146110785760405162461bcd60e51b815260040161036790611805565b7f00000000000000000000000000000000000000000000000000000000000000004210156110b85760405162461bcd60e51b815260040161036790611848565b60006110c3826113d8565b9050806001600160a01b0316630d616d206040518163ffffffff1660e01b8152600401600060405180830381600087803b15801561110057600080fd5b505af1158015611114573d6000803e3d6000fd5b505050505050565b6001546001600160a01b031633146111465760405162461bcd60e51b8152600401610367906117ce565b806001600160a01b03811661116d5760405162461bcd60e51b815260040161036790611876565b6001546040517fd0fc5fb9c7c77f1a24739dedc1219c212e20f8711ce4551d2e97909e0f3f59ba916111ac916001600160a01b039091169085906118bb565b60405180910390a150600180546001600160a01b0319166001600160a01b0392909216919091179055565b600154600160a01b900460ff1615611218576001546001600160a01b031633146112135760405162461bcd60e51b8152600401610367906117ce565b611242565b6000546001600160a01b031633146112425760405162461bcd60e51b815260040161036790611805565b7f00000000000000000000000000000000000000000000000000000000000000004210156112825760405162461bcd60e51b815260040161036790611848565b60405163d9054b0960e01b81526001600160401b03821660048201527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03169063d9054b0990602401610428565b60007f00000000000000000000000000000000000000000000000000000000000000004210611372576040516370a0823160e01b81526001600160a01b038316906370a082319061132c9030906004016116a2565b602060405180830381865afa158015611349573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061136d9190611910565b611375565b60005b92915050565b6113d383846001600160a01b031663a9059cbb85856040516024016113a19291906118d5565b604051602081830303815290604052915060e01b6020820180516001600160e01b0383818316178352505050506114cc565b505050565b60025460405163f7bc39bf60e01b815260009182916001600160a01b039091169063f7bc39bf9061140d9086906004016116a2565b602060405180830381865afa15801561142a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061144e91906118ee565b9050829150806114c65760405162461bcd60e51b815260206004820152603d60248201527f436f6e74726163742061646472657373206973206e6f7420612076616c69642060448201527f6d756c74692d636f6e7472696275746f7220534e20636f6e74726163740000006064820152608401610367565b50919050565b60006114e16001600160a01b03841683611526565b9050805160001415801561150657508080602001905181019061150491906118ee565b155b156113d35782604051635274afe760e01b815260040161036791906116a2565b60606115348383600061153b565b9392505050565b606081471015611560573060405163cd78605960e01b815260040161036791906116a2565b600080856001600160a01b0316848660405161157c9190611a26565b60006040518083038185875af1925050503d80600081146115b9576040519150601f19603f3d011682016040523d82523d6000602084013e6115be565b606091505b50915091506115ce8683836115d8565b9695505050505050565b6060826115ed576115e88261162b565b611534565b815115801561160457506001600160a01b0384163b155b156116245783604051639996b31560e01b815260040161036791906116a2565b5080611534565b80511561163b5780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b50565b60006020828403121561166957600080fd5b5035919050565b6001600160a01b038116811461165457600080fd5b60006020828403121561169757600080fd5b813561153481611670565b6001600160a01b0391909116815260200190565b6000806000606084860312156116cb57600080fd5b83356116d681611670565b92506020840135915060408401356116ed81611670565b809150509250925092565b6000806040838503121561170b57600080fd5b823561171681611670565b9150602083013561172681611670565b809150509250929050565b6000608082840312156114c657600080fd5b60008060008084860361016081121561175b57600080fd5b604081121561176957600080fd5b5084935061177a8660408701611731565b92506117898660c08701611731565b915061014085013561179a81611670565b939692955090935050565b6000602082840312156117b757600080fd5b81356001600160401b038116811461153457600080fd5b6020808252601f908201527f56657374696e673a2043616c6c6572206d757374206265207265766f6b657200604082015260600190565b60208082526023908201527f56657374696e673a2043616c6c6572206d7573742062652062656e656669636960408201526261727960e81b606082015260800190565b60208082526014908201527315995cdd1a5b99ce881b9bdd081cdd185c9d195960621b604082015260600190565b60208082526025908201527f5368617265643a205a65726f2d61646472657373206973206e6f74207065726d6040820152641a5d1d195960da1b606082015260800190565b6001600160a01b0392831681529116602082015260400190565b6001600160a01b03929092168252602082015260400190565b60006020828403121561190057600080fd5b8151801515811461153457600080fd5b60006020828403121561192257600080fd5b5051919050565b634e487b7160e01b600052603260045260246000fd5b600081518084526020840193506020830160005b82811015611994578151805180516001600160a01b039081168952602091820151168189015290810151604088015260609096019590910190600101611953565b5093949350505050565b84358152602080860135818301528435604080840191909152858201356060808501919091528187013560808501528087013560a0850152853560c08501529185013560e084015284013561010083015260009084013561ffff8116808214611a0657600080fd5b8061012085015250506101606101408301526115ce61016083018461193f565b6000825160005b81811015611a475760208186018101518583015201611a2d565b50600092019182525091905056fea164736f6c634300081a000a", + "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106101075760003560e01c80630962ef791461010c57806314bbe21c146101215780631916558714610134578063205a306114610147578063220cce971461015a57806335dec5d114610197578063372500ab146101aa57806338af3eed146101b257806343cac780146101c557806352208a5f146101d857806363d256ce1461020f5780637249d9d91461022357806374a8f103146102365780638a0b4e991461024957806395e3fd371461025c578063a35c47091461026f578063ac8d6c0c14610282578063be9a655514610295578063c3aca558146102ca578063d9054b09146102f1578063efbe1c1c14610304575b600080fd5b61011f61011a366004611657565b61032b565b005b61011f61012f366004611685565b61045d565b61011f610142366004611685565b610593565b61011f610155366004611685565b6106cb565b6101817f000000000000000000000000000000000000000000000000000000000000000081565b60405161018e91906116a2565b60405180910390f35b600154610181906001600160a01b031681565b61011f61073f565b600054610181906001600160a01b031681565b61011f6101d33660046116b6565b61085f565b6101ff7f000000000000000000000000000000000000000000000000000000000000000081565b604051901515815260200161018e565b6001546101ff90600160a01b900460ff1681565b61011f6102313660046116f8565b610a3a565b61011f610244366004611685565b610b7e565b600254610181906001600160a01b031681565b61011f61026a366004611743565b610cdc565b61011f61027d366004611685565b61100d565b61011f610290366004611685565b61111c565b6102bc7f000000000000000000000000000000000000000000000000000000000000000081565b60405190815260200161018e565b6101817f000000000000000000000000000000000000000000000000000000000000000081565b61011f6102ff3660046117a5565b6111d7565b6102bc7f000000000000000000000000000000000000000000000000000000000000000081565b600154600160a01b900460ff1615610375576001546001600160a01b031633146103705760405162461bcd60e51b8152600401610367906117ce565b60405180910390fd5b61039f565b6000546001600160a01b0316331461039f5760405162461bcd60e51b815260040161036790611805565b7f00000000000000000000000000000000000000000000000000000000000000004210156103df5760405162461bcd60e51b815260040161036790611848565b604051630962ef7960e01b8152600481018290527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031690630962ef79906024015b600060405180830381600087803b15801561044257600080fd5b505af1158015610456573d6000803e3d6000fd5b5050505050565b6000546001600160a01b031633146104875760405162461bcd60e51b815260040161036790611805565b806001600160a01b0381166104ae5760405162461bcd60e51b815260040161036790611876565b7f00000000000000000000000000000000000000000000000000000000000000006105295760405162461bcd60e51b815260206004820152602560248201527f56657374696e673a2062656e6566696369617279206e6f74207472616e7366656044820152647261626c6560d81b6064820152608401610367565b6000546040517f57005c5083fa0952870a7906715a2f6f9ef2d01b4a423e4b3ce59c6129b1a76391610568916001600160a01b039091169085906118bb565b60405180910390a150600080546001600160a01b0319166001600160a01b0392909216919091179055565b6000546001600160a01b031633146105bd5760405162461bcd60e51b815260040161036790611805565b600154600160a01b900460ff16156106105760405162461bcd60e51b815260206004820152601660248201527515995cdd1a5b99ce881d1bdad95b881c995d9bdad95960521b6044820152606401610367565b600061061b826112d7565b90506000811161066a5760405162461bcd60e51b815260206004820152601a60248201527956657374696e673a206e6f20746f6b656e73206172652064756560301b6044820152606401610367565b816001600160a01b03167fc7798891864187665ac6dd119286e44ec13f014527aeeb2b8eb3fd413df93179826040516106a591815260200190565b60405180910390a26000546106c7906001600160a01b0384811691168361137b565b5050565b6001546001600160a01b031633146106f55760405162461bcd60e51b8152600401610367906117ce565b806001600160a01b03811661071c5760405162461bcd60e51b815260040161036790611876565b50600280546001600160a01b0319166001600160a01b0392909216919091179055565b600154600160a01b900460ff1615610780576001546001600160a01b0316331461077b5760405162461bcd60e51b8152600401610367906117ce565b6107aa565b6000546001600160a01b031633146107aa5760405162461bcd60e51b815260040161036790611805565b7f00000000000000000000000000000000000000000000000000000000000000004210156107ea5760405162461bcd60e51b815260040161036790611848565b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663372500ab6040518163ffffffff1660e01b8152600401600060405180830381600087803b15801561084557600080fd5b505af1158015610859573d6000803e3d6000fd5b50505050565b600154600160a01b900460ff16156108a0576001546001600160a01b0316331461089b5760405162461bcd60e51b8152600401610367906117ce565b6108ca565b6000546001600160a01b031633146108ca5760405162461bcd60e51b815260040161036790611805565b7f000000000000000000000000000000000000000000000000000000000000000042101561090a5760405162461bcd60e51b815260040161036790611848565b806001600160a01b0381166109315760405162461bcd60e51b815260040161036790611876565b600061093c856113d8565b60405163095ea7b360e01b81529091506001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169063095ea7b39061098d90889088906004016118d5565b6020604051808303816000875af11580156109ac573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906109d091906118ee565b50604051632816ee7360e01b8152600481018590526001600160a01b038481166024830152821690632816ee7390604401600060405180830381600087803b158015610a1b57600080fd5b505af1158015610a2f573d6000803e3d6000fd5b505050505050505050565b600154600160a01b900460ff1615610a7b576001546001600160a01b03163314610a765760405162461bcd60e51b8152600401610367906117ce565b610aa5565b6000546001600160a01b03163314610aa55760405162461bcd60e51b815260040161036790611805565b7f0000000000000000000000000000000000000000000000000000000000000000421015610ae55760405162461bcd60e51b815260040161036790611848565b806001600160a01b038116610b0c5760405162461bcd60e51b815260040161036790611876565b6000610b17846113d8565b604051630557fe9560e11b81529091506001600160a01b03821690630aaffd2a90610b469086906004016116a2565b600060405180830381600087803b158015610b6057600080fd5b505af1158015610b74573d6000803e3d6000fd5b5050505050505050565b6001546001600160a01b03163314610ba85760405162461bcd60e51b8152600401610367906117ce565b600154600160a01b900460ff16610c6c577f0000000000000000000000000000000000000000000000000000000000000000421115610c245760405162461bcd60e51b815260206004820152601860248201527715995cdd1a5b99ce881d995cdd1a5b99c8195e1c1a5c995960421b6044820152606401610367565b6001805460ff60a01b1916600160a01b1790556040516001600160a01b038216907f39983c6d4d174a7aee564f449d4a5c3c7ac9649d72b7793c56901183996f8af690600090a25b6000610c77826112d7565b905080156106c757816001600160a01b03167fa415e62e43678c8b550c180a7ef9a2031e826b306d4052104d288f3c83ac964b82604051610cba91815260200190565b60405180910390a26001546106c7906001600160a01b0384811691168361137b565b600154600160a01b900460ff1615610d1d576001546001600160a01b03163314610d185760405162461bcd60e51b8152600401610367906117ce565b610d47565b6000546001600160a01b03163314610d475760405162461bcd60e51b815260040161036790611805565b7f0000000000000000000000000000000000000000000000000000000000000000421015610d875760405162461bcd60e51b815260040161036790611848565b806001600160a01b038116610dae5760405162461bcd60e51b815260040161036790611876565b60007f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166356d399e86040518163ffffffff1660e01b8152600401602060405180830381865afa158015610e0e573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610e329190611910565b60408051600180825281830190925291925060009190816020015b6040805160808101825260009181018281526060820183905281526020810191909152815260200190600190039081610e4d57505060408051608081018252309181019182526001600160a01b0387166060820152908152602081018490528151919250908290600090610ec357610ec3611929565b602090810291909101015260405163095ea7b360e01b81526001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169063095ea7b390610f3c907f00000000000000000000000000000000000000000000000000000000000000009086906004016118d5565b6020604051808303816000875af1158015610f5b573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610f7f91906118ee565b50604051632f1fbfb360e21b81526001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169063bc7efecc90610fd2908a908a908a90879060040161199e565b600060405180830381600087803b158015610fec57600080fd5b505af1158015611000573d6000803e3d6000fd5b5050505050505050505050565b600154600160a01b900460ff161561104e576001546001600160a01b031633146110495760405162461bcd60e51b8152600401610367906117ce565b611078565b6000546001600160a01b031633146110785760405162461bcd60e51b815260040161036790611805565b7f00000000000000000000000000000000000000000000000000000000000000004210156110b85760405162461bcd60e51b815260040161036790611848565b60006110c3826113d8565b9050806001600160a01b0316630d616d206040518163ffffffff1660e01b8152600401600060405180830381600087803b15801561110057600080fd5b505af1158015611114573d6000803e3d6000fd5b505050505050565b6001546001600160a01b031633146111465760405162461bcd60e51b8152600401610367906117ce565b806001600160a01b03811661116d5760405162461bcd60e51b815260040161036790611876565b6001546040517fd0fc5fb9c7c77f1a24739dedc1219c212e20f8711ce4551d2e97909e0f3f59ba916111ac916001600160a01b039091169085906118bb565b60405180910390a150600180546001600160a01b0319166001600160a01b0392909216919091179055565b600154600160a01b900460ff1615611218576001546001600160a01b031633146112135760405162461bcd60e51b8152600401610367906117ce565b611242565b6000546001600160a01b031633146112425760405162461bcd60e51b815260040161036790611805565b7f00000000000000000000000000000000000000000000000000000000000000004210156112825760405162461bcd60e51b815260040161036790611848565b60405163d9054b0960e01b81526001600160401b03821660048201527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03169063d9054b0990602401610428565b60007f00000000000000000000000000000000000000000000000000000000000000004210611372576040516370a0823160e01b81526001600160a01b038316906370a082319061132c9030906004016116a2565b602060405180830381865afa158015611349573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061136d9190611910565b611375565b60005b92915050565b6113d383846001600160a01b031663a9059cbb85856040516024016113a19291906118d5565b604051602081830303815290604052915060e01b6020820180516001600160e01b0383818316178352505050506114cc565b505050565b60025460405163f7bc39bf60e01b815260009182916001600160a01b039091169063f7bc39bf9061140d9086906004016116a2565b602060405180830381865afa15801561142a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061144e91906118ee565b9050829150806114c65760405162461bcd60e51b815260206004820152603d60248201527f436f6e74726163742061646472657373206973206e6f7420612076616c69642060448201527f6d756c74692d636f6e7472696275746f7220534e20636f6e74726163740000006064820152608401610367565b50919050565b60006114e16001600160a01b03841683611526565b9050805160001415801561150657508080602001905181019061150491906118ee565b155b156113d35782604051635274afe760e01b815260040161036791906116a2565b60606115348383600061153b565b9392505050565b606081471015611560573060405163cd78605960e01b815260040161036791906116a2565b600080856001600160a01b0316848660405161157c9190611a26565b60006040518083038185875af1925050503d80600081146115b9576040519150601f19603f3d011682016040523d82523d6000602084013e6115be565b606091505b50915091506115ce8683836115d8565b9695505050505050565b6060826115ed576115e88261162b565b611534565b815115801561160457506001600160a01b0384163b155b156116245783604051639996b31560e01b815260040161036791906116a2565b5080611534565b80511561163b5780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b50565b60006020828403121561166957600080fd5b5035919050565b6001600160a01b038116811461165457600080fd5b60006020828403121561169757600080fd5b813561153481611670565b6001600160a01b0391909116815260200190565b6000806000606084860312156116cb57600080fd5b83356116d681611670565b92506020840135915060408401356116ed81611670565b809150509250925092565b6000806040838503121561170b57600080fd5b823561171681611670565b9150602083013561172681611670565b809150509250929050565b6000608082840312156114c657600080fd5b60008060008084860361016081121561175b57600080fd5b604081121561176957600080fd5b5084935061177a8660408701611731565b92506117898660c08701611731565b915061014085013561179a81611670565b939692955090935050565b6000602082840312156117b757600080fd5b81356001600160401b038116811461153457600080fd5b6020808252601f908201527f56657374696e673a2043616c6c6572206d757374206265207265766f6b657200604082015260600190565b60208082526023908201527f56657374696e673a2043616c6c6572206d7573742062652062656e656669636960408201526261727960e81b606082015260800190565b60208082526014908201527315995cdd1a5b99ce881b9bdd081cdd185c9d195960621b604082015260600190565b60208082526025908201527f5368617265643a205a65726f2d61646472657373206973206e6f74207065726d6040820152641a5d1d195960da1b606082015260800190565b6001600160a01b0392831681529116602082015260400190565b6001600160a01b03929092168252602082015260400190565b60006020828403121561190057600080fd5b8151801515811461153457600080fd5b60006020828403121561192257600080fd5b5051919050565b634e487b7160e01b600052603260045260246000fd5b600081518084526020840193506020830160005b82811015611994578151805180516001600160a01b039081168952602091820151168189015290810151604088015260609096019590910190600101611953565b5093949350505050565b84358152602080860135818301528435604080840191909152858201356060808501919091528187013560808501528087013560a0850152853560c08501529185013560e084015284013561010083015260009084013561ffff8116808214611a0657600080fd5b8061012085015250506101606101408301526115ce61016083018461193f565b6000825160005b81811015611a475760208186018101518583015201611a2d565b50600092019182525091905056fea164736f6c634300081a000a", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/src/web3client/contracts/token.py b/src/web3client/contracts/token.py index 75afc2c..1313cf3 100644 --- a/src/web3client/contracts/token.py +++ b/src/web3client/contracts/token.py @@ -28,19 +28,4 @@ def balance_of(self, address: str): """ return self.contract.functions.balanceOf(address).call() - @staticmethod - def to_atomic(amount: float | int) -> int: - """ - Converts a float or int to an atomic amount - """ - print(TokenInterface.decimals) - print(amount) - print((amount * 10 ** TokenInterface.decimals)) - return int(amount * 10 ** TokenInterface.decimals) - @staticmethod - def from_atomic(amount: int) -> float: - """ - Converts an atomic amount to a float - """ - return amount / 10 ** TokenInterface.decimals From a6755e7ab9dfbbbc1405f9de4530263516095f64 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 7 Mar 2025 16:08:49 +1100 Subject: [PATCH 110/138] feat: compile smart contracts from source --- .gitmodules | 3 + session-token-contracts | 1 + src/util/__init__.py | 5 -- src/web3client/contract_factory.py | 103 ++++++++++++++++++++++++ src/web3client/contracts_ws/__init__.py | 0 5 files changed, 107 insertions(+), 5 deletions(-) create mode 100644 .gitmodules create mode 160000 session-token-contracts create mode 100644 src/web3client/contract_factory.py create mode 100644 src/web3client/contracts_ws/__init__.py diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..0516dd5 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "session-token-contracts"] + path = session-token-contracts + url = https://github.com/oxen-io/eth-sn-contracts.git diff --git a/session-token-contracts b/session-token-contracts new file mode 160000 index 0000000..b908510 --- /dev/null +++ b/session-token-contracts @@ -0,0 +1 @@ +Subproject commit b9085104fd695bb8cb8e250a546069a606b1cac1 diff --git a/src/util/__init__.py b/src/util/__init__.py index 5889db6..f4d7ed4 100644 --- a/src/util/__init__.py +++ b/src/util/__init__.py @@ -4,11 +4,6 @@ def is_not_empty_string(value) -> bool: return value is not None and len(value) > 0 - -def valid_address_assertion(address: str, name: str | None = None): - assert is_address(address), "{} in config.py is not a valid address: {}".format(name, address) - - def format_seconds(seconds: int | float, precision: int = 3) -> str: assert precision >= 0 if precision == 0: diff --git a/src/web3client/contract_factory.py b/src/web3client/contract_factory.py new file mode 100644 index 0000000..809ba4e --- /dev/null +++ b/src/web3client/contract_factory.py @@ -0,0 +1,103 @@ +import subprocess +from solcx import compile_source, install_solc +import pathlib + +SOLC_VERSION = "0.8.26" +install_solc(SOLC_VERSION) + +base_path = pathlib.Path(__file__).parent.parent.parent.parent.joinpath("session-token-contracts") + +subprocess.run(["pnpm", "install"], cwd=base_path) + +compiled_sol = compile_source( + """ +import "ServiceNodeRewards.sol"; +import "RewardRatePool.sol"; +import "SESH.sol"; +import "ServiceNodeContribution.sol"; +import "ServiceNodeContributionFactory.sol"; +import "utils/TokenVestingStaking.sol"; +import "utils/TokenVestingNoStaking.sol"; +import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import "@openzeppelin/contracts/interfaces/IERC1967.sol"; +""", + base_path=base_path, + include_path=base_path.joinpath("contracts"), + solc_version=SOLC_VERSION, + import_remappings={ + "@openzeppelin/contracts": "node_modules/@openzeppelin/contracts", + "@openzeppelin/contracts-upgradeable": "node_modules/@openzeppelin/contracts-upgradeable", + }, +) + +abis = {} + + +class ContractFactory: + def __init__(self, w3): + """ + Creates factories for the contracts in the session-token-contracts repo. + + Factories can be used to instantiate contracts with the same ABI and bytecode by calling them with the address + of the contract. + + Example: + factory = ContractFactory(w3) + sesh_contract = factory.SESH(address) + """ + self.w3 = w3 + + def get(self, name: str): + if name not in abis: + key = f"{name}.sol:{name}" + + if name == "Ownable2StepUpgradeable": + key = "node_modules/@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol:Ownable2StepUpgradeable" + elif name == "PausableUpgradeable": + key = "node_modules/@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol:PausableUpgradeable" + elif name == "IERC1967": + key = "node_modules/@openzeppelin/contracts/interfaces/IERC1967.sol:IERC1967" + elif name == "TokenVestingStaking": + key = "utils/TokenVestingStaking.sol:TokenVestingStaking" + elif name == "TokenVestingNoStaking": + key = "utils/TokenVestingNoStaking.sol:TokenVestingNoStaking" + + abis[name] = self.w3.eth.contract(abi=compiled_sol[key]["abi"]) + return abis[name] + + @property + def SESH(self): + return self.get("SESH") + + @property + def RewardRatePool(self): + return self.get("RewardRatePool") + + @property + def ServiceNodeRewards(self): + return self.get("ServiceNodeRewards") + + @property + def ServiceNodeContributionFactory(self): + return self.get("ServiceNodeContributionFactory") + + @property + def ServiceNodeContribution(self): + return self.get("ServiceNodeContribution") + + @property + def TokenVestingStaking(self): + return self.get("TokenVestingStaking") + + @property + def TokenVestingNoStaking(self): + return self.get("TokenVestingNoStaking") + + @property + def Ownable2StepUpgradeable(self): + return self.get("Ownable2StepUpgradeable") + + @property + def PausableUpgradeable(self): + return self.get("PausableUpgradeable") diff --git a/src/web3client/contracts_ws/__init__.py b/src/web3client/contracts_ws/__init__.py new file mode 100644 index 0000000..e69de29 From a722604a6b3c41cbe9f7eb24360aace781efb6da Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 7 Mar 2025 16:34:05 +1100 Subject: [PATCH 111/138] feat: create websockets arbitrum event scanner --- requirements.txt | 2 +- src/app_events.py | 39 ++ src/staking/read.py | 152 +++++-- src/staking/schema.sql | 45 +-- src/staking/write.py | 375 +++++++++++++++--- src/util/parse.py | 24 +- src/web3client/contracts_ws/contract_ws.py | 33 ++ src/web3client/contracts_ws/ierc_1967.py | 36 ++ .../ownable_2_step_upgradeable.py | 36 ++ .../contracts_ws/pausable_upgradeable.py | 35 ++ .../contracts_ws/reward_rate_pool.py | 45 +++ .../contracts_ws/service_node_contribution.py | 219 ++++++++++ .../service_node_contribution_factory.py | 59 +++ .../contracts_ws/service_node_rewards.py | 67 ++++ src/web3client/contracts_ws/subscription.py | 65 +++ src/web3client/contracts_ws/token.py | 65 +++ .../contracts_ws/token_vesting_staking.py | 68 ++++ src/web3client/event_queue_manager.py | 97 +++++ src/web3client/event_scanner.py | 2 +- src/web3client/event_ws.py | 309 +++++++++++++++ src/web3client/util.py | 2 + 21 files changed, 1648 insertions(+), 127 deletions(-) create mode 100644 src/app_events.py create mode 100644 src/web3client/contracts_ws/contract_ws.py create mode 100644 src/web3client/contracts_ws/ierc_1967.py create mode 100644 src/web3client/contracts_ws/ownable_2_step_upgradeable.py create mode 100644 src/web3client/contracts_ws/pausable_upgradeable.py create mode 100644 src/web3client/contracts_ws/reward_rate_pool.py create mode 100644 src/web3client/contracts_ws/service_node_contribution.py create mode 100644 src/web3client/contracts_ws/service_node_contribution_factory.py create mode 100644 src/web3client/contracts_ws/service_node_rewards.py create mode 100644 src/web3client/contracts_ws/subscription.py create mode 100644 src/web3client/contracts_ws/token.py create mode 100644 src/web3client/contracts_ws/token_vesting_staking.py create mode 100644 src/web3client/event_queue_manager.py create mode 100644 src/web3client/event_ws.py create mode 100644 src/web3client/util.py diff --git a/requirements.txt b/requirements.txt index eb40939..5afa05a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ oxenc==1.0.4 PyNaCl==1.5.0 eth-utils==5.0.0 Werkzeug==3.0.4 -web3==7.2.0 +web3==7.8.0 eth-typing==5.0.0 eth_abi==5.1.0 requests==2.32.3 diff --git a/src/app_events.py b/src/app_events.py new file mode 100644 index 0000000..2f79873 --- /dev/null +++ b/src/app_events.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +import csv + +from src import config +from src.web3client.event_ws import init_ws_event_scanner, EventScannerConfig, VestingContractDetails + +vesting_contract_details = [] +if config.backend.vesting_contract_details_csv is not None: + data = list(csv.reader(open(config.backend.vesting_contract_details_csv))) + header = data[0] + assert header == ["beneficiary", "vestingAddress", "amount", "start", "end", "transferableBeneficiary", "revoker"] + vesting_contract_details = [] + for row in data[1:]: + row.extend([config.backend.addr_token, config.backend.addr_sn_rewards, config.backend.addr_sn_contrib_factory]) + vesting_contract_details.append(VestingContractDetails(*row)) + +config = EventScannerConfig( + log_level=config.backend.log_level, + enable_perf=config.backend.performance_logging, + log_level_generic=config.backend.log_level_generic, + genesis_block=config.backend.genesis_block, + ws_max_run_depth=config.backend.ws_max_run_depth, + ws_providers=config.backend.ws_providers, + ws_max_size=config.backend.ws_max_size, + addr_token=config.backend.addr_token, + addr_sn_contrib_factory=config.backend.addr_sn_contrib_factory, + addr_sn_rewards=config.backend.addr_sn_rewards, + addr_reward_rate_pool=config.backend.addr_reward_rate_pool, + sqlite_db=config.backend.sqlite_db, + sqlite_schema=config.backend.sqlite_schema, + db_reset_events_on_startup=config.backend.db_reset_events_on_startup, + db_reset_contrib_on_startup=config.backend.db_reset_contrib_on_startup, + reset_vesting_contracts_on_startup=config.backend.reset_vesting_contracts_on_startup, + vesting_contract_details=vesting_contract_details, + ws_watch_token_events=config.backend.ws_watch_token_events, +) + +if __name__ == "__main__": + init_ws_event_scanner(config) diff --git a/src/staking/read.py b/src/staking/read.py index afd349c..2b45113 100644 --- a/src/staking/read.py +++ b/src/staking/read.py @@ -3,7 +3,7 @@ from ..db.read import DBReader from .dataclasses import DBNode, DBContributionMain, DBNetworkInfo, DBContributionContract, \ - DBContributionContractContribution, SmartContractABI, ArbitrumInfo + DBContributionContractContribution, SmartContractABI, ArbitrumInfo, VestingContract from ..util.parse import eth_format from ..web3client.event_scanner import ProcessedEvent @@ -63,6 +63,16 @@ def get_last_fetched_arbitrum_event_block_height(self) -> int: self.log.perf.end("get_last_fetched_arbitrum_event_block_height") return fetched_block_height if fetched_block_height is not None else 0 + def get_contribution_contract_contributors(self, address:str): + self.log.perf.start("get_contribution_contract_contributors") + with closing(self.connect()) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute("""SELECT * FROM contribution_contracts_contributions WHERE contract_address = ?""", (address,)) + contributors = [DBContributionContractContribution(*contribution) for contribution in cursor.fetchall()] + self.log.debug("Contributors: {}".format(len(contributors))) + self.log.perf.end("get_contribution_contract_contributors") + return contributors + def get_contribution_contracts(self): self.log.perf.start("get_contribution_contracts") with closing(self.connect()) as connection: @@ -72,7 +82,7 @@ def get_contribution_contracts(self): parsed_contracts = {} for contract in contracts: - contract_dict = DBContributionContract(*contract, contributors=[]) + contract_dict = DBContributionContract(*contract, contributors=[], events=[]) parsed_contracts[contract_dict.address] = contract_dict cursor.execute( @@ -89,7 +99,7 @@ def get_contribution_contracts(self): self.log.debug("Parsed contribution contracts: {}".format(len(parsed_contracts))) self.log.perf.end("get_contribution_contracts") - return list(parsed_contracts.values()) + return parsed_contracts def get_contribution_contract_addresses(self): self.log.perf.start("get_contribution_contracts") @@ -177,6 +187,23 @@ def get_nodes(self): self.log.perf.end("get_nodes") return list(parsed_nodes.values()) + def get_contribution_addresses(self): + self.log.perf.start("get_contribution_addresses") + with closing(self.connect()) as connection: + with closing(connection.cursor()) as cursor: + addresses = set() + cursor.execute("""SELECT address, beneficiary from service_nodes_contributions_main""") + cursor.execute("""SELECT address, beneficiary from service_nodes_contributions_staging ORDER BY fetched_block_height ASC""") + + for address, beneficiary in cursor.fetchall(): + addresses.add(address) + if beneficiary is not None: + addresses.add(beneficiary) + + self.log.debug("Contribution addresses: {}".format(len(addresses))) + self.log.perf.end("get_contribution_addresses") + return addresses + def get_rewards_info(self): self.log.perf.start("get_rewards_info") with closing(self.connect()) as connection: @@ -280,6 +307,64 @@ def get_smart_contract_address(self, name: str): self.log.perf.end("get_smart_contract_address") return address[0] + def get_arbitrum_events(self, from_block = 0, names: list = None): + self.log.perf.start("get_arbitrum_events") + assert from_block >= 0, "from_block must be >= 0" + with closing(self.connect()) as connection: + with closing(connection.cursor()) as cursor: + self.log.debug(f"Getting events from block {from_block} with names {names}") + if names is None: + cursor.execute( + """ + SELECT * FROM arbitrum_events WHERE block >= ? + """, + (from_block,), + ) + else: + placeholder= '?' # For SQLite. See DBAPI paramstyle. + placeholders= ', '.join(placeholder for unused in names) + query= 'SELECT * FROM arbitrum_events WHERE block >= ? AND name IN ({})'.format(placeholders) + cursor.execute(query, (from_block, *names)) + + events = [ProcessedEvent(*event) for event in cursor.fetchall()] + self.log.debug("Arbitrum events: {}".format(len(events))) + self.log.perf.end("get_arbitrum_events") + return events + + def get_arbitrum_events_by_name(self, name: str, from_block = 0): + self.log.perf.start("get_arbitrum_events_by_name") + assert from_block >= 0, "from_block must be >= 0" + with closing(self.connect()) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute( + """ + SELECT * FROM arbitrum_events WHERE name = ? + """, + (name,), + ) + events = [ProcessedEvent(*event) for event in cursor.fetchall()] + self.log.debug("Arbitrum events: {}".format(len(events))) + self.log.perf.end("get_arbitrum_events_by_name") + return events + + def get_arbitrum_event_main_args_by_name(self, name: str, from_block = 0): + self.log.perf.start("get_arbitrum_event_main_args_by_name") + assert from_block >= 0, "from_block must be >= 0" + with closing(self.connect()) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute( + """ + SELECT main_arg FROM arbitrum_events WHERE name = ? AND block >= ? + """, + (name, from_block), + ) + events = cursor.fetchall() + addresses = [event[0] for event in events] + self.log.debug("Arbitrum Event Args: {}".format(len(addresses))) + self.log.perf.end("get_arbitrum_event_main_args_by_name") + return addresses + + def get_arbitrum_events_page(self, args=None): if args is None: args = [1000, 0] @@ -304,36 +389,6 @@ def get_arbitrum_events_page(self, args=None): return events, limit, skip, total - def get_arbitrum_events_since_timestamp(self, params: [int, list[str] | None]) -> list[ProcessedEvent]: - timestamp = params[0] if len(params) > 0 else None - events_types = params[1] if len(params) > 1 and len(params[1]) > 0 else None - - if timestamp is None or (not isinstance(timestamp, int) and not isinstance(timestamp, float)): - raise ValueError("Invalid timestamp, timestamp must be an integer or float") - - if events_types is not None: - if isinstance(events_types, str): - events_types = [events_types] - elif not isinstance(events_types, list): - raise ValueError("Invalid events_types, events_types must be a list of strings or a string") - - - self.log.perf.start("get_arbitrum_events_since_timestamp") - with closing(self.connect()) as connection: - with closing(connection.cursor()) as cursor: - if events_types is None: - cursor.execute("SELECT * FROM arbitrum_events WHERE timestamp > ? ORDER BY timestamp DESC", (timestamp,)) - else: - placeholder= '?' # For SQLite. See DBAPI paramstyle. - placeholders= ', '.join(placeholder for unused in events_types) - query= 'SELECT * FROM arbitrum_events WHERE timestamp > ? AND name IN (%s) ORDER BY timestamp DESC' % placeholders - cursor.execute(query, (timestamp, *events_types)) - # cursor.execute("SELECT * FROM arbitrum_events WHERE timestamp > ? AND name IN ({}) ORDER BY timestamp DESC".format(",".join(["?"]*len(events_types))), tuple(events_types)+(timestamp,)) - events = [ProcessedEvent(*event) for event in cursor.fetchall()] - self.log.debug("Arbitrum events: {}".format(len(events))) - self.log.perf.end("get_arbitrum_events_since_timestamp") - return events - def get_arbitrum_info(self): self.log.perf.start("get_arbitrum_info") with closing(self.connect()) as connection: @@ -360,19 +415,30 @@ def get_arbitrum_events_for_stake_contrat_id(self, contract_id: int): self.log.perf.end("get_events_for_stake_contrat_id") return events - def get_service_node_rewards_contract_id_bls_key_map(self): - self.log.perf.start("get_service_node_rewards_contract_id_bls_key_map") + def get_vesting_contracts(self) -> list[VestingContract]: + self.log.perf.start("get_vesting_contracts") with closing(self.connect()) as connection: with closing(connection.cursor()) as cursor: cursor.execute( """ - SELECT contract_id, pubkey_bls FROM service_node_rewards_contract_id_bls_key_map + SELECT * FROM vesting_contracts """ ) - contract_id_map = { - pubkey_bls: contract_id - for contract_id, pubkey_bls in cursor.fetchall() - } - self.log.debug("Service node rewards contract id bls key map: {}".format(len(contract_id_map))) - self.log.perf.end("get_service_node_rewards_contract_id_bls_key_map") - return contract_id_map \ No newline at end of file + contracts = [VestingContract(*contract) for contract in cursor.fetchall()] + self.log.debug("Vesting contracts: {}".format(len(contracts))) + self.log.perf.end("get_vesting_contracts") + return contracts + + def has_vesting_contracts(self) -> bool: + self.log.perf.start("has_vesting_contracts") + with closing(self.connect()) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute( + """ + SELECT COUNT(*) FROM vesting_contracts + """ + ) + count = cursor.fetchone()[0] + self.log.debug("Vesting contracts: {}".format(count)) + self.log.perf.end("has_vesting_contracts") + return count > 0 diff --git a/src/staking/schema.sql b/src/staking/schema.sql index f51bea2..cb757f3 100644 --- a/src/staking/schema.sql +++ b/src/staking/schema.sql @@ -162,41 +162,34 @@ CREATE INDEX arbitrum_info_block_idx ON arbitrum_info(block DESC); CREATE TABLE arbitrum_events ( args TEXT NOT NULL, block INTEGER NOT NULL, + log_index INTEGER NOT NULL, main_arg TEXT, name TEXT NOT NULL, - timestamp INTEGER NOT NULL, tx TEXT NOT NULL, - PRIMARY KEY (block, tx, name) + PRIMARY KEY (log_index, tx) ); CREATE INDEX arbitrum_events_block_idx ON arbitrum_events(block DESC); -CREATE INDEX arbitrum_events_block_timestamp ON arbitrum_events(timestamp DESC); CREATE INDEX arbitrum_events_main_arg_idx ON arbitrum_events(main_arg, block DESC); CREATE TABLE contribution_contracts ( address TEXT NOT NULL, - created_timestamp INTEGER, - fee INTEGER NOT NULL, - last_added_timestamp INTEGER, - manual_finalize BOOLEAN NOT NULL, - node_add_timestamp INTEGER, - operator_address TEXT NOT NULL, - pubkey_bls BLOB NOT NULL, - service_node_pubkey BLOB NOT NULL, - service_node_signature BLOB NOT NULL, - status INTEGER NOT NULL, + fee INTEGER, + manual_finalize BOOLEAN, + operator_address TEXT, + pubkey_bls BLOB, + service_node_pubkey BLOB, + status INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (address) ); CREATE INDEX contribution_contracts_address_idx ON contribution_contracts(address); -CREATE INDEX contribution_contracts_node_add_timestamp_idx ON contribution_contracts(node_add_timestamp DESC); -CREATE TABLE contribution_contracts_contributions -( +CREATE TABLE contribution_contracts_contributions ( address BLOB NOT NULL, - amount INTEGER NOT NULL, - beneficiary_address BLOB NOT NULL, + amount INTEGER NOT NULL DEFAULT 0, + beneficiary_address BLOB, contract_address BLOB NOT NULL, reserved INTEGER, @@ -223,11 +216,17 @@ CREATE TABLE smart_contracts ( foreign key (name) references smart_contract_abis(name) ); -CREATE TABLE service_node_rewards_contract_id_bls_key_map ( - contract_id INTEGER NOT NULL, - pubkey_bls BLOB NOT NULL, +CREATE TABLE vesting_contracts ( + address TEXT NOT NULL, + beneficiary TEXT NOT NULL, + initial_amount INTEGER NOT NULL, + initial_beneficiary TEXT NOT NULL, + revoker TEXT NOT NULL, + time_end INTEGER NOT NULL, + time_start INTEGER NOT NULL, + transferable_beneficiary BOOLEAN NOT NULL, - PRIMARY KEY (contract_id) + PRIMARY KEY (address) ); -CREATE INDEX service_node_rewards_contract_id_bls_key_map_pubkey_bls_idx ON service_node_rewards_contract_id_bls_key_map(pubkey_bls); +CREATE INDEX vesting_contracts_beneficiary_idx ON vesting_contracts(beneficiary); diff --git a/src/staking/write.py b/src/staking/write.py index 013a396..bb8a4d7 100644 --- a/src/staking/write.py +++ b/src/staking/write.py @@ -1,13 +1,12 @@ import json -import sqlite3 import time from contextlib import closing + from web3 import Web3 from ..db.write import DBWriter from ..staking.arbitrum import ContributionContractDetails -from ..staking.dataclasses import RewardsInfo, DBNodeExit -from ..log import Log +from ..staking.dataclasses import RewardsInfo, DBNodeExit, VestingContract from ..oxen.rpc import ServiceNode, NetworkInfo from ..web3client.abi_manager import ABIData from ..web3client.event_scanner import ProcessedEvent @@ -16,20 +15,21 @@ class DBWriterStaking(DBWriter): def __init__(self, db_path: str, log_level: int, perf: bool = False): super().__init__(db_path, log_level, perf) + self.defer_writing_arbitrum_events = False + self.deferred_arbitrum_events = [] def write_nodes_to_staging_db( - self, - height: int, - parsed_nodes: list[ServiceNode], - # TODO: type the contributor_stake_map properly - contributions: list[dict[str, int]], + self, + height: int, + parsed_nodes: list[ServiceNode], + # TODO: type the contributor_stake_map properly + contributions: list[dict[str, int]], ): self.log.perf.start("write_to_db") with closing(self.connect()) as connection: connection.execute("BEGIN") with closing(connection.cursor()) as cursor: - self.log.debug("Inserting {} service nodes".format(len(parsed_nodes))) self.log.perf.start("write_nodes_to_staging_db -> insert nodes") @@ -353,10 +353,10 @@ def write_exit_list_to_db(self, exit_list: list[DBNodeExit]): self.log.perf.end("write_exit_list_to_db") def write_network_info_to_db( - self, - network: NetworkInfo, - node_count: int, - active_node_count: int, + self, + network: NetworkInfo, + node_count: int, + active_node_count: int, ): self.log.perf.start("write_network_info_to_db") with closing(self.connect()) as connection: @@ -435,36 +435,91 @@ def write_rewards_info_to_db(self, rewards_info: list[RewardsInfo]): connection.commit() self.log.perf.end("write_rewards_info_to_db") + def write_arbitrum_event_to_db(self, event: ProcessedEvent): + if self.defer_writing_arbitrum_events: + self.log.debug(f"Deferring arbitrum event write: {event}") + self.deferred_arbitrum_events.append(event) + return + self.log.perf.start("write_arbitrum_event_to_db") + with closing(self.connect()) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + self.log.debug("Inserting event into arbitrum_events") + self.log.debug(event) + self.log.perf.start("write_arbitrum_event_to_db -> insert event") + cursor.execute( + """ + INSERT INTO arbitrum_events ( + args, + block, + log_index, + main_arg, + name, + tx + ) + VALUES (?, ?, ?, ?, ?, ?) + """, + ( + Web3.to_json(dict(event.args)), + event.block, + event.log_index, + event.main_arg, + event.name, + event.tx, + ), + ) + inserted_event_rows = cursor.rowcount + self.log.perf.end("write_arbitrum_event_to_db -> insert event") + self.log.debug( + "Inserted {} rows into arbitrum_events".format(inserted_event_rows) + ) + connection.commit() + self.log.perf.end("write_arbitrum_event_to_db") + + def write_deferred_arbitrum_events_to_db(self): + if len(self.deferred_arbitrum_events) == 0: + self.log.warning("No deferred arbitrum events to write") + return + + events, self.deferred_arbitrum_events = self.deferred_arbitrum_events, [] + + try: + self.log.info(f"Writing {len(events)} deferred arbitrum events to db") + self.write_arbitrum_events_to_db(events) + except Exception as e: + self.log.error("Error writing deferred arbitrum events") + self.log.error(e) + self.deferred_arbitrum_events = events + self.deferred_arbitrum_events + def write_arbitrum_events_to_db(self, events: list[ProcessedEvent]): self.log.perf.start("write_arbitrum_events_to_db") with closing(self.connect()) as connection: connection.execute("BEGIN") with closing(connection.cursor()) as cursor: - self.log.debug("Inserting {} events into arbitrum_events".format(len(events))) self.log.perf.start("write_arbitrum_events_to_db -> insert events") cursor.executemany( """ INSERT OR REPLACE INTO arbitrum_events ( + args, block, - timestamp, - tx, - name, + log_index, main_arg, - args + name, + tx ) VALUES (?, ?, ?, ?, ?, ?) """, ( ( + Web3.to_json(dict(event.args)), event.block, - event.timestamp, - "0x" + event.tx, - event.name, + event.log_index, event.main_arg, - Web3.to_json(dict(event.args)), + event.name, + event.tx, ) for event in events ), @@ -482,15 +537,139 @@ def write_arbitrum_events_to_db(self, events: list[ProcessedEvent]): connection.commit() self.log.perf.end("write_arbitrum_events_to_db") + def write_new_contribution_contract(self, address: str, operator_address: str): + self.log.perf.start("write_new_contribution_contract") + with closing(self.connect()) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + self.log.debug(f"Inserting new contribution contract") + cursor.execute(""" + INSERT OR REPLACE INTO contribution_contracts ( + address, + operator_address + ) + VALUES (?, ?) + """, (address, operator_address)) + + connection.commit() + self.log.perf.end("write_new_contribution_contract") + + def write_update_contribution_contract_status(self, address: str, status: int): + self.log.perf.start("write_update_contribution_contract_status") + with closing(self.connect()) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + self.log.debug(f"Updating contribution contract status to {status}") + cursor.execute( + """ + UPDATE contribution_contracts SET status = ? WHERE address = ? + """, + (status, address), + ) + + connection.commit() + self.log.perf.end("write_update_contribution_contract_status") + + def write_update_contribution_contract_manual_finalize(self, address: str, manual_finalize: bool): + self.log.perf.start("write_update_contribution_contract_manual_finalize") + with closing(self.connect()) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + self.log.debug(f"Updating contribution contract manual_finalize to {manual_finalize}") + cursor.execute( + """ + UPDATE contribution_contracts SET manual_finalize = ? WHERE address = ? + """, + (manual_finalize, address), + ) + + connection.commit() + self.log.perf.end("write_update_contribution_contract_manual_finalize") + + def write_update_contribution_contract_fee(self, address: str, fee: int): + self.log.perf.start("write_update_contribution_contract_fee") + with closing(self.connect()) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + self.log.debug(f"Updating contribution contract fee to {fee}") + cursor.execute( + """ + UPDATE contribution_contracts SET fee = ? WHERE address = ? + """, + (fee, address), + ) + + connection.commit() + self.log.perf.end("write_update_contribution_contract_fee") + + def write_update_contribution_contract_pubkeys(self, address: str, pubkey_bls: str, service_node_pubkey: str): + self.log.perf.start("write_update_contribution_contract_pubkeys") + with closing(self.connect()) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + self.log.debug(f"Updating contribution contract pubkeys") + cursor.execute( + """ + INSERT OR UPDATE contribution_contracts SET pubkey_bls = ?, service_node_pubkey = ? WHERE address = ? + """, + (pubkey_bls, service_node_pubkey, address), + ) + + connection.commit() + self.log.perf.end("write_update_contribution_contract_pubkeys") + + def write_update_contribution_contract_contributor(self, contract_address: str, contributor): + self.log.perf.start("write_update_contribution_contract_contributor") + with closing(self.connect()) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + self.log.debug(f"Updating contribution contract contributor") + cursor.execute( + """ + INSERT OR REPLACE INTO contribution_contracts_contributions ( + address, + amount, + beneficiary_address, + contract_address, + reserved + ) + VALUES (?, ?, ?, ?, ?) + """, + ( + contributor.address, + contributor.amount, + contributor.beneficiary_address, + contract_address, + contributor.reserved + ), + ) + + connection.commit() + + def write_delete_contribution_contract_contributor(self, contract_address: str, contributor): + self.log.perf.start("write_delete_contribution_contract_contributor") + with closing(self.connect()) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + self.log.debug(f"Deleting contribution contract contributor") + cursor.execute( + """ + DELETE FROM contribution_contracts_contributions WHERE address = ? AND contract_address = ? + """, + (contributor.address, contract_address), + ) + + connection.commit() + self.log.perf.end("write_delete_contribution_contract_contributor") + def write_contribution_contracts_to_db( - self, contracts: list[ContributionContractDetails], contributions_list: list, add_event_timestamps: dict[str, int], node_last_added_timestamps: dict[str,int], create_contract_timestamps: dict[str, int] + self, contracts: list[ContributionContractDetails], contributions_list: list ): self.log.perf.start("write_contribution_contracts_to_db") with closing(self.connect()) as connection: connection.execute("BEGIN") with closing(connection.cursor()) as cursor: - self.log.debug("Inserting {} contribution contracts".format(len(contracts))) self.log.perf.start("write_contribution_contracts_to_db -> insert contracts") @@ -498,31 +677,23 @@ def write_contribution_contracts_to_db( """ INSERT OR REPLACE INTO contribution_contracts ( address, - created_timestamp, fee, - last_added_timestamp, manual_finalize, - node_add_timestamp, operator_address, pubkey_bls, service_node_pubkey, - service_node_signature, status ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?) """, ( ( contract.address, - create_contract_timestamps.get(contract.address), contract.fee, - node_last_added_timestamps.get(contract.pubkey_bls), contract.manual_finalize, - add_event_timestamps.get(contract.pubkey_bls), contract.operator_address, contract.pubkey_bls, contract.service_node_pubkey, - contract.service_node_signature, contract.status, ) for contract in contracts @@ -608,7 +779,6 @@ def write_smart_contract_abis_to_db(self, abis: list[ABIData]): with closing(self.connect()) as connection: connection.execute("BEGIN") with closing(connection.cursor()) as cursor: - self.log.debug("Inserting {} smart contract abis".format(len(abis))) self.log.perf.start("write_smart_contract_abis_to_db -> insert abis") @@ -644,14 +814,13 @@ def write_smart_contract_abis_to_db(self, abis: list[ABIData]): self.log.perf.end("write_smart_contract_abis_to_db") def write_smart_contract_details_to_db( - self, - contracts, + self, + contracts, ): self.log.perf.start("write_smart_contract_details_to_db") with closing(self.connect()) as connection: connection.execute("BEGIN") with closing(connection.cursor()) as cursor: - self.log.debug("Inserting {} smart contract details".format(len(contracts))) self.log.perf.start("write_smart_contract_details_to_db -> insert contracts") @@ -691,8 +860,10 @@ def write_arbitrum_info_to_db(self, current_block, service_node_rewards_balance, "Inserting arbitrum info: current block {}, service node rewards balance {}, reward rate pool balance {}".format( current_block, service_node_rewards_balance, reward_rate_pool_balance)) self.log.perf.start("write_arbitrum_info_to_db -> insert info") - - cursor.execute("INSERT OR REPLACE INTO arbitrum_info (block, balance_service_node_rewards, balance_reward_rate_pool) VALUES (?, ?, ?)", (current_block, service_node_rewards_balance, reward_rate_pool_balance)) + + cursor.execute( + "INSERT OR REPLACE INTO arbitrum_info (block, balance_service_node_rewards, balance_reward_rate_pool) VALUES (?, ?, ?)", + (current_block, service_node_rewards_balance, reward_rate_pool_balance)) inserted_info_rows = cursor.rowcount @@ -704,41 +875,131 @@ def write_arbitrum_info_to_db(self, current_block, service_node_rewards_balance, connection.commit() self.log.perf.end("write_arbitrum_info_to_db") - def write_service_node_rewards_contract_id_bls_key_map(self, contract_id_map: dict[str, str]): - self.log.perf.start("write_service_node_rewards_contract_id_bls_key_map") + def write_vesting_contracts(self, vesting_contracts: list[VestingContract]): + self.log.perf.start("write_vesting_contracts") with closing(self.connect()) as connection: connection.execute("BEGIN") with closing(connection.cursor()) as cursor: - self.log.debug("Inserting {} service node rewards contract ids".format(len(contract_id_map))) - self.log.perf.start("write_service_node_rewards_contract_id_bls_key_map -> insert contract ids") + self.log.debug("Inserting {} vesting contracts".format(len(vesting_contracts))) + self.log.perf.start("write_vesting_contracts -> insert contracts") - cursor.execute("DELETE FROM service_node_rewards_contract_id_bls_key_map") + # assert the table is empty + cursor.execute("SELECT COUNT(*) FROM vesting_contracts") + assert cursor.fetchone()[0] == 0, "Vesting contract table is not empty" cursor.executemany( """ - INSERT INTO service_node_rewards_contract_id_bls_key_map ( - contract_id, - pubkey_bls + INSERT OR REPLACE INTO vesting_contracts ( + address, + beneficiary, + initial_amount, + initial_beneficiary, + revoker, + time_end, + time_start, + transferable_beneficiary ) - VALUES (?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) """, ( ( - int(contract_id), - pubkey_bls, + contract.address, + contract.beneficiary, + contract.initial_amount, + contract.initial_beneficiary, + contract.revoker, + contract.time_end, + contract.time_start, + contract.transferable_beneficiary, ) - for pubkey_bls, contract_id in contract_id_map.items() + for contract in vesting_contracts ), ) - inserted_contract_id_rows = cursor.rowcount + inserted_contract_rows = cursor.rowcount - self.log.perf.end("write_service_node_rewards_contract_id_bls_key_map -> insert contract ids") + self.log.perf.end("write_vesting_contracts -> insert contracts") self.log.debug( - "Inserted {} rows into service_node_rewards_contract_id_bls_key_map".format( - inserted_contract_id_rows - ) + "Inserted {} rows into vesting_contracts".format(inserted_contract_rows) + ) + + connection.commit() + self.log.perf.end("write_vesting_contracts") + + def delete_all_vesting_contracts(self): + self.log.perf.start("delete_all_vesting_contracts") + with closing(self.connect()) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + cursor.execute("DELETE FROM vesting_contracts") + deleted_rows = cursor.rowcount + self.log.debug( + "Cleared {} rows from vesting_contracts".format(deleted_rows) + ) + connection.commit() + self.log.perf.end("delete_all_vesting_contracts") + + def write_update_vesting_contract_beneficiary(self, address: str, beneficiary: str): + self.log.perf.start("write_update_vesting_contract_beneficiary") + with closing(self.connect()) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + self.log.debug("Updating vesting contract {} beneficiary to {}".format(address, beneficiary)) + self.log.perf.start("write_update_vesting_contract_beneficiary -> update beneficiary") + + cursor.execute( + """ + UPDATE vesting_contracts SET beneficiary = ? WHERE address = ? + """, + (beneficiary, address), + ) + + updated_rows = cursor.rowcount + + self.log.perf.end("write_update_vesting_contract_beneficiary -> update beneficiary") + self.log.debug( + "Updated {} rows in vesting_contracts".format(updated_rows) + ) + + connection.commit() + self.log.perf.end("write_update_vesting_contract_beneficiary") + + def delete_all_events(self): + self.log.perf.start("delete_all_events") + with closing(self.connect()) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + self.log.debug("Deleting all events from the db") + + cursor.execute("""Delete from arbitrum_events""") + + deleted_rows = cursor.rowcount + + self.log.debug( + "Cleared {} rows from vesting_contracts".format(deleted_rows) + ) + + connection.commit() + self.log.perf.end("delete_all_events") + + def delete_all_contrib_contracts_and_contributors(self): + self.log.perf.start("delete_all_contrib_contracts_and_contributors") + with closing(self.connect()) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + cursor.execute("""Delete from contribution_contracts_contributions""") + deleted_contributions_rows = cursor.rowcount + + self.log.debug( + "Cleared {} rows from contribution_contracts_contributions".format(deleted_contributions_rows) + ) + + cursor.execute("""Delete from contribution_contracts""") + deleted_contract_rows = cursor.rowcount + + self.log.debug( + "Cleared {} rows from contribution_contracts".format(deleted_contract_rows) ) connection.commit() - self.log.perf.end("write_service_node_rewards_contract_id_bls_key_map") \ No newline at end of file + self.log.perf.end("delete_all_contrib_contracts_and_contributors") diff --git a/src/util/parse.py b/src/util/parse.py index f68a432..c42eab3 100644 --- a/src/util/parse.py +++ b/src/util/parse.py @@ -13,10 +13,12 @@ eth_regex = "0x[0-9a-fA-F]{40}" -def parse_bls_pubkey(bls_pubkey: (str, str)): - x, y = bls_pubkey +def parse_bls_pubkey(bls_pubkey: dict): + x, y = bls_pubkey["X"], bls_pubkey["Y"] return f"{x:064x}{y:064x}" +def parse_ed25519_pubkey(ed25519_pubkey: int): + return f"{ed25519_pubkey:032x}" def raw_eth_addr(k, v): if re.fullmatch(eth_regex, v): @@ -25,6 +27,24 @@ def raw_eth_addr(k, v): return bytes.fromhex(v[2:]) raise ParseError(k, "not an ETH address") +def get_relative_time_from_ms(ms: int, short: bool = False, include_suffix = False): + if include_suffix: + prefix, suffix = ("in ", "") if ms > 0 else ("", " ago") + else: + prefix, suffix = "", "" + + if ms < 1000: + time = f"{ms} {"ms" if short else "milliseconds"}" + elif ms < 1000 * 60: + time = f"{ms // 1000} {"s" if short else "seconds"}" + elif ms < 1000 * 60 * 60: + time = f"{ms // (1000 * 60)} {"m" if short else "minutes"}" + elif ms < 1000 * 60 * 60 * 24: + time = f"{ms // (1000 * 60 * 60)} {"h" if short else "hours"}" + else: + time = f"{ms // (1000 * 60 * 60 * 24)} {"d" if short else "days"}" + + return f"{prefix}{time}{suffix}" def hexify(container): """ diff --git a/src/web3client/contracts_ws/contract_ws.py b/src/web3client/contracts_ws/contract_ws.py new file mode 100644 index 0000000..a20d51f --- /dev/null +++ b/src/web3client/contracts_ws/contract_ws.py @@ -0,0 +1,33 @@ +import logging + +from web3 import AsyncWeb3 +from web3.types import EventData +from web3.utils.subscriptions import EthSubscriptionContext + +from src.staking.write import DBWriterStaking +from src.web3client.contract_factory import ContractFactory +from src.web3client.contracts_ws.subscription import parse_event, write_event_to_db +from src.web3client.event_queue_manager import EventQueueManager + + +class ContractWS: + def __init__(self, name: str, w3: AsyncWeb3, db_writer: DBWriterStaking, log: logging, + event_queue: EventQueueManager | None = None): + self.w3 = w3 + self.log = log + self.factory = ContractFactory(w3).get(name) + self.db_writer = db_writer + self.event_abis = {} + self.event_queue = event_queue + + def _parse_event(self, event: EthSubscriptionContext): + return parse_event(self.event_abis, event) + + async def _handle_event(self, event: EventData, main_arg: str | None = None): + self.log.debug(f"New {event.event}: {event.args}") + if main_arg is None: + main_arg = event.address + write_event_to_db(self.db_writer, event, main_arg=main_arg) + + async def _handle_event_sub(self, event: EthSubscriptionContext): + await self._handle_event(self._parse_event(event)) diff --git a/src/web3client/contracts_ws/ierc_1967.py b/src/web3client/contracts_ws/ierc_1967.py new file mode 100644 index 0000000..03cce53 --- /dev/null +++ b/src/web3client/contracts_ws/ierc_1967.py @@ -0,0 +1,36 @@ +import logging + +from eth_typing import ChecksumAddress +from web3 import AsyncWeb3 + +from src.staking.write import DBWriterStaking +from src.web3client.contracts_ws.contract_ws import ContractWS +from src.web3client.contracts_ws.subscription import create_subscriptions +from src.web3client.event_queue_manager import EventQueueManager + + +class IERC1967(ContractWS): + name = "IERC1967" + + def __init__(self, w3: AsyncWeb3, db_writer: DBWriterStaking, log: logging, + event_queue: EventQueueManager | None = None): + super().__init__(self.name, w3, db_writer, log, event_queue) + + def create_subscriptions(self, address: ChecksumAddress | list[ChecksumAddress]): + events = self.factory(address[0] if isinstance(address, list) else address).events + event_list = [ + events.Upgraded, + events.AdminChanged, + events.BeaconUpgraded, + ] + + for event in event_list: + event.address = address + + return create_subscriptions( + events=event_list, + event_queue=self.event_queue, + event_abis=self.event_abis, + handler_sub=self._handle_event_sub, + handler_past=self._handle_event, + ) diff --git a/src/web3client/contracts_ws/ownable_2_step_upgradeable.py b/src/web3client/contracts_ws/ownable_2_step_upgradeable.py new file mode 100644 index 0000000..4d9a01e --- /dev/null +++ b/src/web3client/contracts_ws/ownable_2_step_upgradeable.py @@ -0,0 +1,36 @@ +import logging + +from eth_typing import ChecksumAddress +from web3 import AsyncWeb3 + +from src.staking.write import DBWriterStaking +from src.web3client.contracts_ws.contract_ws import ContractWS +from src.web3client.contracts_ws.subscription import create_subscriptions +from src.web3client.event_queue_manager import EventQueueManager + + +class Ownable2StepUpgradeable(ContractWS): + name = "Ownable2StepUpgradeable" + + def __init__(self, w3: AsyncWeb3, db_writer: DBWriterStaking, log: logging, + event_queue: EventQueueManager | None = None): + super().__init__(self.name, w3, db_writer, log, event_queue) + + def create_subscriptions(self, address: ChecksumAddress | list[ChecksumAddress]): + events = self.factory(address[0] if isinstance(address, list) else address).events + event_list = [ + events.OwnershipTransferStarted, + events.OwnershipTransferred, + events.Initialized, + ] + + for event in event_list: + event.address = address + + return create_subscriptions( + events=event_list, + event_queue=self.event_queue, + event_abis=self.event_abis, + handler_sub=self._handle_event_sub, + handler_past=self._handle_event, + ) diff --git a/src/web3client/contracts_ws/pausable_upgradeable.py b/src/web3client/contracts_ws/pausable_upgradeable.py new file mode 100644 index 0000000..c203871 --- /dev/null +++ b/src/web3client/contracts_ws/pausable_upgradeable.py @@ -0,0 +1,35 @@ +import logging + +from eth_typing import ChecksumAddress +from web3 import AsyncWeb3 + +from src.staking.write import DBWriterStaking +from src.web3client.contracts_ws.contract_ws import ContractWS +from src.web3client.contracts_ws.subscription import create_subscriptions +from src.web3client.event_queue_manager import EventQueueManager + + +class PausableUpgradeable(ContractWS): + name = "PausableUpgradeable" + + def __init__(self, w3: AsyncWeb3, db_writer: DBWriterStaking, log: logging, + event_queue: EventQueueManager | None = None): + super().__init__(self.name, w3, db_writer, log, event_queue) + + def create_subscriptions(self, address: ChecksumAddress | list[ChecksumAddress]): + events = self.factory(address[0] if isinstance(address, list) else address).events + event_list = [ + events.Paused, + events.Unpaused, + ] + + for event in event_list: + event.address = address + + return create_subscriptions( + events=event_list, + event_queue=self.event_queue, + event_abis=self.event_abis, + handler_sub=self._handle_event_sub, + handler_past=self._handle_event, + ) diff --git a/src/web3client/contracts_ws/reward_rate_pool.py b/src/web3client/contracts_ws/reward_rate_pool.py new file mode 100644 index 0000000..47afdac --- /dev/null +++ b/src/web3client/contracts_ws/reward_rate_pool.py @@ -0,0 +1,45 @@ +import logging + +from eth_typing import ChecksumAddress +from web3 import AsyncWeb3 +from web3.types import EventData +from web3.utils.subscriptions import EthSubscriptionContext + +from src.staking.write import DBWriterStaking +from src.web3client.contracts_ws.contract_ws import ContractWS +from src.web3client.contracts_ws.subscription import create_subscriptions +from src.web3client.event_queue_manager import EventQueueManager + + +class RewardRatePool(ContractWS): + name = "RewardRatePool" + + def __init__(self, w3: AsyncWeb3, db_writer: DBWriterStaking, log: logging, + event_queue: EventQueueManager | None = None): + super().__init__(self.name, w3, db_writer, log, event_queue) + + @staticmethod + def get_main_arg(event: EventData): + match event.event: + case "FundsReleased": + return event.args.amount + case _: + return None + + async def handle_event(self, event: EventData): + main_arg = RewardRatePool.get_main_arg(event) + assert main_arg is not None + return await self._handle_event(event, main_arg=main_arg) + + async def handle_event_sub(self, event: EthSubscriptionContext): + return await self.handle_event(self._parse_event(event)) + + def create_subscriptions(self, address: ChecksumAddress): + events = self.factory(address).events + return create_subscriptions( + events=[events.FundsReleased], + event_queue=self.event_queue, + event_abis=self.event_abis, + handler_sub=self.handle_event_sub, + handler_past=self.handle_event, + ) diff --git a/src/web3client/contracts_ws/service_node_contribution.py b/src/web3client/contracts_ws/service_node_contribution.py new file mode 100644 index 0000000..9e80592 --- /dev/null +++ b/src/web3client/contracts_ws/service_node_contribution.py @@ -0,0 +1,219 @@ +import logging +from dataclasses import dataclass + +from eth_typing import ChecksumAddress +from web3 import AsyncWeb3 +from web3.types import EventData +from web3.utils.subscriptions import EthSubscriptionContext + +from src.staking.read import DBReaderStaking +from src.staking.write import DBWriterStaking +from src.util.parse import parse_ed25519_pubkey, parse_bls_pubkey +from src.web3client.contracts_ws.contract_ws import ContractWS +from src.web3client.contracts_ws.subscription import create_subscriptions, create_processed_event +from src.web3client.event_queue_manager import EventQueueManager + + +@dataclass +class ContributionContractContributor: + address: str = None + amount: int = 0 + beneficiary_address: str = None + reserved: int = 0 + + +class ContributionContract: + def __init__(self, address, log, db_writer: DBWriterStaking, db_reader: DBReaderStaking = None): + self.log = log + self.db_writer = db_writer + self.address = address + + contributors = db_reader.get_contribution_contract_contributors(address) if db_reader else [] + + self._contributors = {} + if len(contributors) > 0: + for contributor in contributors: + self._contributors[contributor.address] = contributor + self.log.debug(f"Initialized {self.address} with Contributors: {self._contributors}") + + def update_status(self, status: int): + self.db_writer.write_update_contribution_contract_status(self.address, status) + + def update_manual_finalize(self, new_value: bool): + self.db_writer.write_update_contribution_contract_manual_finalize(self.address, new_value) + + def update_fee(self, new_fee: int): + self.db_writer.write_update_contribution_contract_fee(self.address, new_fee) + + def update_pubkeys(self, new_bls_pubkey: dict, new_ed25519_pubkey: int): + if new_bls_pubkey is None: + self.log.warning(f"No new BLS pubkey found for {self.address}") + return + if new_ed25519_pubkey is None: + self.log.warning(f"No new Ed25519 pubkey found for {self.address}") + return + + pubkey_bls = parse_bls_pubkey(new_bls_pubkey) + service_node_pubkey = parse_ed25519_pubkey(new_ed25519_pubkey) + self.db_writer.write_update_contribution_contract_pubkeys(self.address, pubkey_bls, service_node_pubkey) + + def _upsert_contributor(self, address: str): + self._contributors.setdefault(address, ContributionContractContributor(address)) + + def update_contributor_new_contribution(self, address: str, amount: int): + self._upsert_contributor(address) + self._contributors[address].address = address + self._contributors[address].amount += amount + self.db_writer.write_update_contribution_contract_contributor(self.address, self._contributors[address]) + + def update_contributor_withdraw_contribution(self, address: str, amount: int): + if address in self._contributors: + self._contributors[address].amount -= amount + if self._contributors[address].amount == 0: + del self._contributors[address] + self.db_writer.write_delete_contribution_contract_contributor(self.address) + else: + self.db_writer.write_update_contribution_contract_contributor(self.address, self._contributors[address]) + else: + self.log.warning(f"No contributor found for address {address} to withdraw {amount}") + + def update_contributor_beneficiary(self, address: str, beneficiary_address: str): + self._upsert_contributor(address) + self._contributors[address].beneficiary_address = beneficiary_address + self.db_writer.write_update_contribution_contract_contributor(self.address, self._contributors[address]) + + def update_reserved_contributors(self, reserved_contributors: list[dict[str, str]]): + for reserved_contributor in reserved_contributors: + address = reserved_contributor.get("addr") + reserved_amount = reserved_contributor.get("amount") + self._upsert_contributor(address) + self._contributors[address].reserved = reserved_amount + + def update_reset(self): + self._contributors = {} + self.update_status(0) + self.update_reserved_contributors([]) + + def process_event(self, raw_event: EventData): + event = create_processed_event(raw_event, main_arg=self.address) + match event.name: + case "OpenForPublicContribution": + return self.update_status(1) + + case "Filled": + return self.update_status(2) + + case "Finalized": + return self.update_status(3) + + case "NewContribution": + return self.update_contributor_new_contribution(event.args.get("contributor"), event.args.get("amount")) + + case "WithdrawContribution": + return self.update_contributor_withdraw_contribution(event.args.get("contributor"), + event.args.get("amount")) + + case "UpdateStakerBeneficiary": + return self.update_contributor_beneficiary(event.args.get("staker"), event.args.get("newBeneficiary")) + + case "UpdateManualFinalize": + return self.update_manual_finalize(event.args.get("newValue")) + + case "UpdateFee": + return self.update_fee(event.args.get("newFee")) + + case "UpdatePubkeys": + return self.update_pubkeys(event.args.get("newBLSPubkey"), event.args.get("newEd25519Pubkey")) + + case "UpdateReservedContributors": + return self.update_reserved_contributors(event.args.get("newReservedContributors")) + + case "Reset": + return self.update_reset() + + case _: + return self.log.warning(f"Unknown event to process: {event}") + + +tracked_contracts: dict[str, ContributionContract] = {} + + +class ServiceNodeContribution(ContractWS): + name = "ServiceNodeContribution" + event_names = [ + "Finalized", + "NewContribution", + "OpenForPublicContribution", + "Filled", + "WithdrawContribution", + "UpdateStakerBeneficiary", + # TODO: uncomment when these events exist in the contract + # "UpdateManualFinalize", + # "UpdateFee", + # "UpdatePubkeys", + # "UpdateReservedContributors", + # "Reset", + ] + + def __init__(self, w3: AsyncWeb3, db_writer: DBWriterStaking, db_reader: DBReaderStaking, log: logging, + event_queue: EventQueueManager | None = None): + super().__init__(self.name, w3, db_writer, log, event_queue) + self.db_reader = db_reader + + async def handle_event(self, event: EventData): + await self._handle_event(event, main_arg=event.address) + tracked_contracts.setdefault( + event.address, + ContributionContract(address=event.address, log=self.log, db_writer=self.db_writer, + db_reader=self.db_reader) + ) + tracked_contracts[event.address].process_event(event) + + async def handle_event_sub(self, event: EthSubscriptionContext): + return await self.handle_event(self._parse_event(event)) + + def create_subscriptions(self, address: ChecksumAddress | list[ChecksumAddress]): + events = self.factory(address[0] if isinstance(address, list) else address).events + event_list = [events[name] for name in self.event_names] + + for event in event_list: + event.address = address + + return create_subscriptions( + events=event_list, + event_queue=self.event_queue, + event_abis=self.event_abis, + handler_sub=self.handle_event_sub, + handler_past=self.handle_event, + ) + + batch_items = 7 + # TODO: use this to get details for old contracts + async def batch_get_details(self, addresses: list[ChecksumAddress]): + chunk_size = 100 + chunks = [addresses[i: i + chunk_size] for i in range(0, len(addresses), chunk_size)] + responses = [] + for chunk in chunks: + assert len(chunk) <= chunk_size, "Expected chunk size <= {} got {}".format(chunk_size, len(chunk)) + async with self.w3.batch_requests() as batch: + for address in chunk: + contract = self.factory(address) + batch.add(contract.functions.serviceNodeParams()) + batch.add(contract.functions.operator()) + batch.add(contract.functions.blsPubkey()) + batch.add(contract.functions.getContributions()) + batch.add(contract.functions.status()) + batch.add(contract.functions.manualFinalize()) + batch.add(contract.functions.getReserved()) + + res = await batch.async_execute() + if len(res) == 1: + self.log.warning(res[0]) + assert len(res) == len( + chunk) * ServiceNodeContribution.batch_items, f"Expected {len(chunk) * ServiceNodeContribution.batch_items} responses, got {len(res)}" + responses.extend(res) + + assert len(responses) == len( + addresses) * ServiceNodeContribution.batch_items, f"Expected {len(addresses) * ServiceNodeContribution.batch_items} responses, got {len(responses)}" + + return responses diff --git a/src/web3client/contracts_ws/service_node_contribution_factory.py b/src/web3client/contracts_ws/service_node_contribution_factory.py new file mode 100644 index 0000000..239b246 --- /dev/null +++ b/src/web3client/contracts_ws/service_node_contribution_factory.py @@ -0,0 +1,59 @@ +import logging + +from eth_typing import ChecksumAddress +from web3 import AsyncWeb3 +from web3.types import EventData +from web3.utils.subscriptions import EthSubscriptionContext + +from src.staking.read import DBReaderStaking +from src.staking.write import DBWriterStaking +from src.web3client.contracts_ws.contract_ws import ContractWS +from src.web3client.contracts_ws.service_node_contribution import ServiceNodeContribution +from src.web3client.contracts_ws.subscription import create_subscriptions +from src.web3client.event_queue_manager import EventQueueManager + + +class ServiceNodeContributionFactory(ContractWS): + name = "ServiceNodeContributionFactory" + event_names = ["NewServiceNodeContributionContract"] + + def __init__(self, w3: AsyncWeb3, db_writer: DBWriterStaking, db_reader: DBReaderStaking, log: logging, + event_queue: EventQueueManager | None = None): + super().__init__(self.name, w3, db_writer, log, event_queue) + self.service_node_contribution_contract = ServiceNodeContribution(w3=w3, db_writer=db_writer, + db_reader=db_reader, log=log, + event_queue=self.event_queue) + self.bootstrap_contribution_contract_addresses = [] + + def add_existing_contribution_contracts(self, addresses: list[str]): + self.bootstrap_contribution_contract_addresses.extend(addresses) + + def bootstrap_contribution_contracts(self): + if len(self.bootstrap_contribution_contract_addresses) == 0: + self.log.warning("No bootstrap contribution contract addresses found") + return + self.log.info(f"Bootstrapping {len(self.bootstrap_contribution_contract_addresses)} contribution contracts") + self.service_node_contribution_contract.create_subscriptions( + address=self.bootstrap_contribution_contract_addresses) + + async def handle_event(self, event: EventData, is_bootstrap: bool = True): + address = event.args.get("contributorContract") + await self._handle_event(event, main_arg=address) + self.db_writer.write_new_contribution_contract(address, event.args.get("operator")) + if is_bootstrap: + self.bootstrap_contribution_contract_addresses.append(address) + else: + self.service_node_contribution_contract.create_subscriptions(address=address) + + async def handle_event_sub(self, event: EthSubscriptionContext): + return await self.handle_event(self._parse_event(event), is_bootstrap=False) + + def create_subscriptions(self, address: ChecksumAddress): + events = self.factory(address).events + return create_subscriptions( + events=[events[name] for name in self.event_names], + event_queue=self.event_queue, + event_abis=self.event_abis, + handler_sub=self.handle_event_sub, + handler_past=self.handle_event, + ) diff --git a/src/web3client/contracts_ws/service_node_rewards.py b/src/web3client/contracts_ws/service_node_rewards.py new file mode 100644 index 0000000..9c02266 --- /dev/null +++ b/src/web3client/contracts_ws/service_node_rewards.py @@ -0,0 +1,67 @@ +import logging + +from eth_typing import ChecksumAddress +from web3 import AsyncWeb3 +from web3.types import EventData +from web3.utils.subscriptions import EthSubscriptionContext + +from src.staking.write import DBWriterStaking +from src.web3client.contracts_ws.contract_ws import ContractWS +from src.web3client.contracts_ws.subscription import create_subscriptions +from src.web3client.event_queue_manager import EventQueueManager + + +class ServiceNodeRewards(ContractWS): + name = "ServiceNodeRewards" + + def __init__(self, w3: AsyncWeb3, db_writer: DBWriterStaking, log: logging, + event_queue: EventQueueManager | None = None): + super().__init__(self.name, w3, db_writer, log, event_queue) + + @staticmethod + def get_main_arg(event: EventData): + match event.event: + case "RewardsClaimed": + return event.args.recipientAddress + case "StakingRequirementUpdated": + return event.args.newRequirement + case "ClaimThresholdUpdated": + return event.args.newThreshold + case "ClaimCycleUpdated": + return event.args.newValue + case "LiquidationRatiosUpdated": + return event.args.liquidatorRatio + case "BLSNonSignerThresholdMaxUpdated": + return event.args.newMax + case _: + return event.args.serviceNodeID + + async def handle_event(self, event: EventData): + main_arg = ServiceNodeRewards.get_main_arg(event) + assert main_arg is not None + return await self._handle_event(event, main_arg=main_arg) + + async def handle_event_sub(self, event: EthSubscriptionContext): + return await self.handle_event(self._parse_event(event)) + + def create_subscriptions(self, address: ChecksumAddress): + events = self.factory(address).events + return create_subscriptions( + events=[ + events.NewSeededServiceNode, + events.NewServiceNodeV2, + events.ServiceNodeExitRequest, + events.ServiceNodeExit, + events.ServiceNodeLiquidated, + events.RewardsClaimed, + events.StakingRequirementUpdated, + events.ClaimThresholdUpdated, + events.ClaimCycleUpdated, + events.LiquidationRatiosUpdated, + events.BLSNonSignerThresholdMaxUpdated, + ], + event_queue=self.event_queue, + event_abis=self.event_abis, + handler_sub=self.handle_event_sub, + handler_past=self.handle_event, + ) diff --git a/src/web3client/contracts_ws/subscription.py b/src/web3client/contracts_ws/subscription.py new file mode 100644 index 0000000..8a27b9d --- /dev/null +++ b/src/web3client/contracts_ws/subscription.py @@ -0,0 +1,65 @@ +from collections.abc import Callable + +from web3._utils.events import get_event_data +from web3.contract.async_contract import AsyncContractEvent +from web3.types import EventData +from web3.utils.subscriptions import LogsSubscription, EthSubscriptionContext + +from src.staking.write import DBWriterStaking +from src.web3client.event_queue_manager import EventQueueManager +from src.web3client.event_scanner import ProcessedEvent + + +def create_subscriptions( + events: list[AsyncContractEvent], + event_queue: EventQueueManager, + handler_sub: Callable, + handler_past: Callable = None, + event_abis: dict[str, any] = None +): + assert isinstance(event_queue, EventQueueManager), "event_queue must be an instance of EventQueueManager" + for event in events: + label_address = event.address[0] if isinstance(event.address, list) else event.address + label = f"{event.name}_{label_address}" + + event_queue.add( + event=event, + handler=handler_past, + sub=LogsSubscription( + label=label, + address=event.address, + topics=[event().topic], + handler=handler_sub, + ), + ) + + if event.name not in event_abis: + event_abis[event.name] = event._get_event_abi() + + +def parse_event(event_abis, event: EthSubscriptionContext): + result = event.result + name = event.subscription.label + + if "_" in name: + name = name.split("_")[0] + + abi = event_abis[name] + data = get_event_data(event.async_w3.codec, abi, result) + return data + + +def create_processed_event(event: EventData, main_arg: str | None = None): + return ProcessedEvent( + name=event.event, + args=event.args, + log_index=event.logIndex, + block=event.blockNumber, + main_arg=main_arg, + tx=event.transactionHash.hex() + ) + + +def write_event_to_db(db_writer: DBWriterStaking, event: EventData, main_arg: str | None = None): + event = create_processed_event(event, main_arg=main_arg) + db_writer.write_arbitrum_event_to_db(event) diff --git a/src/web3client/contracts_ws/token.py b/src/web3client/contracts_ws/token.py new file mode 100644 index 0000000..030133f --- /dev/null +++ b/src/web3client/contracts_ws/token.py @@ -0,0 +1,65 @@ +import logging + +from eth_typing import ChecksumAddress +from web3 import AsyncWeb3 +from web3.types import EventData +from web3.utils.subscriptions import EthSubscriptionContext + +from src.staking.write import DBWriterStaking +from src.web3client.contracts_ws.contract_ws import ContractWS +from src.web3client.contracts_ws.subscription import create_subscriptions +from src.web3client.event_queue_manager import EventQueueManager + + +class Token(ContractWS): + name = "SESH" + decimals = 9 + + def __init__(self, w3: AsyncWeb3, db_writer: DBWriterStaking, log: logging, + event_queue: EventQueueManager | None = None): + super().__init__(self.name, w3, db_writer, log, event_queue) + + @staticmethod + def to_atomic(amount: float | int) -> int: + """ + Converts a float or int to an atomic amount + """ + return int(amount * (10 ** Token.decimals)) + + @staticmethod + def from_atomic(amount: int) -> float: + """ + Converts an atomic amount to a float + """ + return amount / (10 ** Token.decimals) + + @staticmethod + def get_main_arg(event: EventData): + match event.event: + case "Approval": + return event.args.owner + case "Transfer": + return event.args.to + case _: + return None + + async def handle_event(self, event: EventData): + main_arg = Token.get_main_arg(event) + assert main_arg is not None + return await self._handle_event(event, main_arg=main_arg) + + async def handle_event_sub(self, event: EthSubscriptionContext): + return await self.handle_event(self._parse_event(event)) + + def create_subscriptions(self, address: ChecksumAddress): + events = self.factory(address).events + return create_subscriptions( + events=[ + events.Transfer, + events.Approval, + ], + event_queue=self.event_queue, + event_abis=self.event_abis, + handler_sub=self.handle_event_sub, + handler_past=self.handle_event, + ) diff --git a/src/web3client/contracts_ws/token_vesting_staking.py b/src/web3client/contracts_ws/token_vesting_staking.py new file mode 100644 index 0000000..fba0c06 --- /dev/null +++ b/src/web3client/contracts_ws/token_vesting_staking.py @@ -0,0 +1,68 @@ +import logging + +from eth_typing import ChecksumAddress +from web3 import AsyncWeb3 +from web3.contract import Contract +from web3.types import EventData +from web3.utils.subscriptions import EthSubscriptionContext + +from src.staking.write import DBWriterStaking +from src.web3client.contracts_ws.contract_ws import ContractWS +from src.web3client.contracts_ws.subscription import create_subscriptions +from src.web3client.event_queue_manager import EventQueueManager + + +class TokenVestingStaking(ContractWS): + name = "TokenVestingStaking" + + def __init__(self, w3: AsyncWeb3, db_writer: DBWriterStaking, log: logging, + event_queue: EventQueueManager | None = None): + super().__init__(self.name, w3, db_writer, log, event_queue) + + async def handle_event(self, event: EventData): + await self._handle_event(event, main_arg=event.address) + + async def handle_event_sub(self, event: EthSubscriptionContext): + await self.handle_event(self._parse_event(event)) + + def create_subscriptions(self, address: ChecksumAddress | list[ChecksumAddress]): + events = self.factory(address[0] if isinstance(address, list) else address).events + event_list = [ + events.TokensReleased, + events.TokenVestingRevoked, + events.TokensRevokedReleased, + events.BeneficiaryTransferred, + events.RevokerTransferred, + ] + + for event in event_list: + event.address = address + + return create_subscriptions( + events=events, + event_queue=self.event_queue, + event_abis=self.event_abis, + handler_sub=self.handle_event_sub, + handler_past=self.handle_event, + ) + + batch_items = 9 + + async def batch_get_details(self, addresses: list[ChecksumAddress], token_contract: Contract): + async with self.w3.batch_requests() as batch: + for address in addresses: + contract = self.factory(address) + batch.add(contract.functions.beneficiary()) + batch.add(contract.functions.revoker()) + batch.add(token_contract.functions.balanceOf(address)) + batch.add(contract.functions.transferableBeneficiary()) + batch.add(contract.functions.start()) + batch.add(contract.functions.end()) + batch.add(contract.functions.SESH()) + batch.add(contract.functions.rewardsContract()) + batch.add(contract.functions.snContribFactory()) + + responses = await batch.async_execute() + assert len(responses) == len(addresses) * TokenVestingStaking.batch_items + + return responses diff --git a/src/web3client/event_queue_manager.py b/src/web3client/event_queue_manager.py new file mode 100644 index 0000000..7a2c74f --- /dev/null +++ b/src/web3client/event_queue_manager.py @@ -0,0 +1,97 @@ +import asyncio +import logging +from dataclasses import dataclass +from typing import Callable + +from web3 import AsyncWeb3 +from web3.utils.subscriptions import LogsSubscription + +from src.util.parse import get_relative_time_from_ms +from src.web3client.util import get_time_of_arbitrum_blocks_ms + + +@dataclass +class PastEventsFetchQueueItem: + event: any + handler: callable + start_block: int | None + + +class EventQueueManager: + def __init__(self, w3: AsyncWeb3, log: logging, start_block: int = 0, max_run_depth: int = 10): + self.processed_events = 0 + self.processed_subs = 0 + self.sub_queue = [] + self.event_queue = [] + self.w3 = w3 + self.log = log + self.start_block = start_block + self.max_run_depth = max_run_depth + log.info(f"Event queue manager starting with start block {start_block}") + log.debug(f"Event queue manager max run depth {max_run_depth}") + + def add_event(self, event: any, handler: Callable, start_block: int | None = None): + if start_block is None: + start_block = self.start_block + self.event_queue.append(PastEventsFetchQueueItem(event=event, handler=handler, start_block=start_block)) + + def add_subscription(self, sub: LogsSubscription): + self.sub_queue.append(sub) + + def add(self, event: any, handler: Callable, sub: LogsSubscription, start_block: int | None = None): + self.add_subscription(sub) + self.add_event(event, handler, start_block) + + async def process_event_queue(self, start_block: int | None = None): + if start_block is None: + start_block = self.start_block + + # Once a websocket subscription is made, any events will be queued to be processed by the websocket consumer. This + # past event scan should be done right after the websocket subscription is made so the block number at this time + # can be used for all past event scans. + block_current = await self.w3.eth.block_number + + queue, self.event_queue = self.event_queue, [] + + if len(queue) > 0: + self.log.info(f"Fetching past events for {len(queue)} events") + + responses = [] + for past_event in queue: + from_block = past_event.start_block if past_event.start_block else start_block + self.log.debug( + f"Fetching past events for {past_event.event.event_name} from block {from_block} to block {block_current} ({block_current - from_block} blocks ~{get_relative_time_from_ms(get_time_of_arbitrum_blocks_ms(block_current - from_block))})") + for recent in await past_event.event.get_logs(from_block=from_block, to_block=block_current): + responses.append(past_event.handler(recent)) + self.processed_events += 1 + + await asyncio.gather(*responses) + + else: + self.log.debug("No past events to fetch") + + async def process_sub_queue(self): + sub_queue, self.sub_queue = self.sub_queue, [] + + if len(sub_queue) > 0: + await self.w3.subscription_manager.subscribe(sub_queue) + self.processed_subs += len(sub_queue) + self.log.info(f"Subscribed to {len(sub_queue)} subscriptions") + else: + self.log.debug("No subscriptions to subscribe to") + + async def run(self): + # The max depth ensures the loop won't get stuck in an infinite loop. This is just a safety measure as it should + # not be possible due to the queue population dependencies. + run_depth = 0 + while run_depth <= self.max_run_depth and (len(self.sub_queue) > 0 or len(self.event_queue) > 0): + await self.process_sub_queue() + await self.process_event_queue() + run_depth += 1 + + logging.debug( + f"Processed lifetime total of {self.processed_events} events and {self.processed_subs} subscriptions") + + if run_depth > self.max_run_depth: + self.log.warning( + f"Reached max run depth of {self.max_run_depth}. This may indicate a problem with the event scanner. Events may have been missed.") diff --git a/src/web3client/event_scanner.py b/src/web3client/event_scanner.py index d380191..54cfa96 100644 --- a/src/web3client/event_scanner.py +++ b/src/web3client/event_scanner.py @@ -28,9 +28,9 @@ class ProcessedEvent: args: dict block: int + log_index: int main_arg: str | None name: str - timestamp: None | int tx: str def __post_init__(self): diff --git a/src/web3client/event_ws.py b/src/web3client/event_ws.py new file mode 100644 index 0000000..5a3dbf7 --- /dev/null +++ b/src/web3client/event_ws.py @@ -0,0 +1,309 @@ +import asyncio +import logging +import time +from datetime import datetime +from attr import dataclass + +from eth_typing import ChecksumAddress +from eth_utils import is_checksum_address, to_checksum_address +from web3 import AsyncWeb3, WebSocketProvider + +from src.config_validate import validate_log_config, validate_contract_addresses +from src.db.util import is_db_initialized, init_db +from src.log import Log +from src.staking.dataclasses import VestingContract +from src.staking.read import DBReaderStaking +from src.staking.write import DBWriterStaking +from src.web3client.contracts_ws.ierc_1967 import IERC1967 +from src.web3client.contracts_ws.ownable_2_step_upgradeable import Ownable2StepUpgradeable +from src.web3client.contracts_ws.pausable_upgradeable import PausableUpgradeable +from src.web3client.contracts_ws.reward_rate_pool import RewardRatePool +from src.web3client.contracts_ws.service_node_contribution_factory import ServiceNodeContributionFactory +from src.web3client.contracts_ws.service_node_rewards import ServiceNodeRewards +from src.web3client.contracts_ws.token import Token +from src.web3client.contracts_ws.token_vesting_staking import TokenVestingStaking +from src.web3client.contrib_contract_details import load_contributor_contract_details +from src.web3client.event_queue_manager import EventQueueManager + +log = Log("event_ws", enable_perf=True).logger +global_db_writer: DBWriterStaking | None = None +global_db_reader: DBReaderStaking | None = None +event_queue: EventQueueManager | None = None +sn_contrib_factory: ServiceNodeContributionFactory | None = None + + +@dataclass(init=False) +class VestingContractDetails: + beneficiary: ChecksumAddress + vesting_address: ChecksumAddress + amount: int + start: int + end: int + transferable_beneficiary: bool + revoker: ChecksumAddress + SESH: ChecksumAddress + rewards_contract: ChecksumAddress + sn_contrib_factory: ChecksumAddress + + def __init__(self, beneficiary, vesting_address, amount, start, end, transferable_beneficiary, revoker, SESH, + rewards_contract, sn_contrib_factory): + self.beneficiary = beneficiary + self.vesting_address = vesting_address + self.amount = Token.to_atomic(float(amount)) + self.start = int(datetime.fromisoformat(start).timestamp()) + self.end = int(datetime.fromisoformat(end).timestamp()) + self.transferable_beneficiary = str.lower(transferable_beneficiary) == "true" + self.revoker = revoker + self.SESH = SESH + self.rewards_contract = rewards_contract + self.sn_contrib_factory = sn_contrib_factory + + assert is_checksum_address(self.beneficiary) + assert is_checksum_address(self.vesting_address) + assert self.amount > 0 + assert self.start < self.end + assert self.transferable_beneficiary is not None + assert is_checksum_address(self.revoker) + assert is_checksum_address(self.SESH) + assert is_checksum_address(self.rewards_contract) + assert is_checksum_address(self.sn_contrib_factory) + + +@dataclass +class EventScannerConfig: + log_level: int + log_level_generic: str | None + enable_perf: bool | None + ws_max_run_depth: int + ws_providers: list[str] + ws_max_size: int + genesis_block: int + addr_token: ChecksumAddress + addr_sn_contrib_factory: ChecksumAddress + addr_sn_rewards: ChecksumAddress + addr_reward_rate_pool: ChecksumAddress + sqlite_db: str + sqlite_schema: str + db_reset_events_on_startup: bool + db_reset_contrib_on_startup: bool + reset_vesting_contracts_on_startup: bool + vesting_contract_details: list[VestingContractDetails] + ws_watch_token_events: bool + run_once_as_script: bool = False + + def __post_init__(self): + self.addr_token = to_checksum_address(self.addr_token) + self.addr_sn_contrib_factory = to_checksum_address(self.addr_sn_contrib_factory) + self.addr_sn_rewards = to_checksum_address(self.addr_sn_rewards) + self.addr_reward_rate_pool = to_checksum_address(self.addr_reward_rate_pool) + + +async def load_vesting_staking_contracts(w3: AsyncWeb3, details: list[VestingContractDetails]): + contract_interface = TokenVestingStaking(w3=w3, db_writer=global_db_writer, log=log, event_queue=event_queue) + token_contract = Token(w3=w3, db_writer=global_db_writer, log=log).factory(details[0].SESH) + + if global_db_reader.has_vesting_contracts(): + if len(details) > 0: + log.warning("Vesting contracts already loaded in database, skipping provided contracts.") + address_list = [contract.address for contract in global_db_reader.get_vesting_contracts()] + else: + if len(details) == 0: + log.warning( + "No vesting contracts found in database and none were provided to load. Skipping vesting contract loading.") + return + + address_list = [contract.vesting_address for contract in details] + + now = time.time() + res = await contract_interface.batch_get_details(address_list, token_contract) + + contracts = [] + for i in range(0, len(res), contract_interface.batch_items): + known_details = details[i // contract_interface.batch_items] + beneficiary = res[i] + revoker = res[i + 1] + amount = res[i + 2] + transferable_beneficiary = res[i + 3] + start = res[i + 4] + end = res[i + 5] + SESH = res[i + 6] + rewards_contract = res[i + 7] + sn_contrib_factory = res[i + 8] + + assert revoker == known_details.revoker, f"Expected {known_details.revoker}, got {revoker}" + + if start < now: + assert amount == known_details.amount, f"Expected {known_details.amount}, got {amount}" + # TODO: consider checking staked amounts and asserting those + + assert transferable_beneficiary == known_details.transferable_beneficiary, f"Expected {known_details.transferable_beneficiary}, got {transferable_beneficiary}" + if not transferable_beneficiary: + assert beneficiary == known_details.beneficiary, f"Expected {known_details.beneficiary}, got {beneficiary}" + # TODO: consider checking if beneficiary has changed and is correct + + assert start == known_details.start, f"Expected {known_details.start}, got {start}" + assert end == known_details.end, f"Expected {known_details.end}, got {end}" + assert SESH == known_details.SESH, f"Expected {known_details.SESH}, got {SESH}" + assert rewards_contract == known_details.rewards_contract, f"Expected {known_details.rewards_contract}, got {rewards_contract}" + assert sn_contrib_factory == known_details.sn_contrib_factory, f"Expected {known_details.sn_contrib_factory}, got {sn_contrib_factory}" + + contracts.append(VestingContract( + address=known_details.vesting_address, + beneficiary=beneficiary, + initial_amount=known_details.amount, + initial_beneficiary=known_details.beneficiary, + revoker=revoker, + time_end=end, + time_start=start, + transferable_beneficiary=transferable_beneficiary, + )) + + global_db_writer.write_vesting_contracts(contracts) + + # Verify that the contracts are the same coming out of the db + db_vesting_contracts = global_db_reader.get_vesting_contracts() + for i in range(len(contracts)): + contract = contracts[i] + db_contract = db_vesting_contracts[i] + if not contract.transferable_beneficiary: + assert contract.beneficiary == db_contract.beneficiary, f"Expected {contract.beneficiary}, got {db_contract.beneficiary}" + + assert contract.revoker == db_contract.revoker, f"Expected {contract.revoker}, got {db_contract.revoker}" + if contract.time_start < now: + assert contract.initial_amount == db_contract.initial_amount, f"Expected {contract.initial_amount}, got {db_contract.initial_amount}" + + assert contract.time_start == db_contract.time_start, f"Expected {contract.time_start}, got {db_contract.time_start}" + assert contract.time_end == db_contract.time_end, f"Expected {contract.time_end}, got {db_contract.time_end}" + assert contract.transferable_beneficiary == db_contract.transferable_beneficiary, f"Expected {contract.transferable_beneficiary}, got {db_contract.transferable_beneficiary}" + + log.info( + f"Added vesting contract {contract.address} with initial balance {contract.initial_amount} for {contract.beneficiary}") + + log.info(f"Subscribing to {len(address_list)} vesting contracts") + contract_interface.create_subscriptions(address=address_list) + + +async def init_global_contracts(w3: AsyncWeb3, config: EventScannerConfig): + global event_queue, sn_contrib_factory + + last_event_block = global_db_reader.get_last_fetched_arbitrum_event_block_height() + start_block = last_event_block + 1 if last_event_block else config.genesis_block + log.info(f"Last block for an event from the database: {last_event_block}, starting from block {start_block}") + + event_queue = EventQueueManager(w3=w3, log=log, start_block=start_block, max_run_depth=config.ws_max_run_depth) + + await load_vesting_staking_contracts(w3, details=config.vesting_contract_details) + + sn_contrib_factory = ServiceNodeContributionFactory(w3=w3, db_writer=global_db_writer, db_reader=global_db_reader, + log=log, event_queue=event_queue) + sn_contrib_factory.create_subscriptions(address=config.addr_sn_contrib_factory) + + ServiceNodeRewards(w3=w3, db_writer=global_db_writer, log=log, event_queue=event_queue).create_subscriptions( + config.addr_sn_rewards, + ) + + RewardRatePool(w3=w3, db_writer=global_db_writer, log=log, event_queue=event_queue).create_subscriptions( + config.addr_reward_rate_pool + ) + + Ownable2StepUpgradeable(w3=w3, db_writer=global_db_writer, log=log, event_queue=event_queue).create_subscriptions( + [config.addr_sn_contrib_factory, config.addr_sn_rewards, config.addr_reward_rate_pool] + ) + + PausableUpgradeable(w3=w3, db_writer=global_db_writer, log=log, event_queue=event_queue).create_subscriptions( + [config.addr_sn_contrib_factory, config.addr_sn_rewards] + ) + + IERC1967(w3=w3, db_writer=global_db_writer, log=log, event_queue=event_queue).create_subscriptions( + [config.addr_sn_contrib_factory, config.addr_sn_rewards, config.addr_reward_rate_pool] + ) + + if config.ws_watch_token_events: + log.warning("Watching all token events, this may greatly increase the rescan time (ws_watch_token_events=True)") + Token(w3=w3, db_writer=global_db_writer, log=log, event_queue=event_queue).create_subscriptions( + config.addr_token + ) + + +async def monitor_events(w3: AsyncWeb3, run_once_as_script=False): + existing_sn_contract_addresses = global_db_reader.get_arbitrum_event_main_args_by_name( + "NewServiceNodeContributionContract") + for address in existing_sn_contract_addresses: + assert is_checksum_address( + address), f"Invalid existing NewServiceNodeContributionContract contract address: {address}" + + global_db_writer.defer_writing_arbitrum_events = True + sn_contrib_factory.add_existing_contribution_contracts(existing_sn_contract_addresses) + await event_queue.run() + sn_contrib_factory.bootstrap_contribution_contracts() + await event_queue.run() + + # WIP: To support old contracts we will need this but it isnt working yet TODO: DELETE THIS AFTER EVENTS ARE AVAILABLE + # await load_contributor_contract_details(w3, existing_sn_contract_addresses) + + global_db_writer.defer_writing_arbitrum_events = False + global_db_writer.write_deferred_arbitrum_events_to_db() + + assert len(event_queue.sub_queue) == 0 and len( + event_queue.event_queue) == 0 and len( + global_db_writer.deferred_arbitrum_events) == 0, \ + f"Expected all queues to be empty." \ + f"Deferred DB write events: {len(global_db_writer.deferred_arbitrum_events)}" + + log.info(f"Created {len(w3.subscription_manager.subscriptions)} subscriptions") + log.perf.end("startup_till_processing_websocket_subscriptions") + if run_once_as_script: + log.info("run_once_as_script is True, exiting...") + return + else: + await w3.subscription_manager.handle_subscriptions() + + +async def start(config: EventScannerConfig): + log.perf.start("startup_till_processing_websocket_subscriptions") + global global_db_writer, global_db_reader, event_queue + # This loop repeats if the connection is lost, to reestablish the connection: when that happens + # we need to re-subscribe and re-fetch any lost events that might have happened during the + # disconnect (or restart). + log.setLevel(config.log_level) + + validate_log_config(config) + validate_contract_addresses(config) + + if config.log_level_generic is not None: + logging.getLogger(None).setLevel(config.log_level_generic) + + if not is_db_initialized(config.sqlite_db): + log.info(f"Initializing database {config.sqlite_db} with schema {config.sqlite_schema}") + init_db(config.sqlite_db, config.sqlite_schema) + + global_db_writer = DBWriterStaking(db_path=config.sqlite_db, log_level=config.log_level, perf=config.enable_perf) + global_db_reader = DBReaderStaking(db_path=config.sqlite_db, log_level=config.log_level, perf=config.enable_perf) + + if config.db_reset_events_on_startup: + log.warning("Deleting events database on startup (db_reset_events_on_startup=True)") + global_db_writer.delete_all_events() + + if config.db_reset_contrib_on_startup: + log.warning("Deleting contrib contracts database on startup (db_reset_contrib_on_startup=True)") + global_db_writer.delete_all_contrib_contracts_and_contributors() + + if config.reset_vesting_contracts_on_startup: + log.warning("Deleting vesting contracts database on startup (reset_vesting_contracts_on_startup=True)") + global_db_writer.delete_all_vesting_contracts() + + log.info("Starting event scanner") + provider = config.ws_providers[0] + async for w3 in AsyncWeb3( + WebSocketProvider(provider, websocket_kwargs={"max_size": config.ws_max_size}, request_timeout=300) + ): + # TODO: investigate if this properly handles disconnects + await init_global_contracts(w3, config) + await asyncio.create_task(monitor_events(w3=w3, run_once_as_script=config.run_once_as_script)) + if config.run_once_as_script: + log.info("Exiting websocket disconnection loop as run_once_as_script is True") + break + + +def init_ws_event_scanner(config: EventScannerConfig): + asyncio.run(start(config)) diff --git a/src/web3client/util.py b/src/web3client/util.py new file mode 100644 index 0000000..57f6600 --- /dev/null +++ b/src/web3client/util.py @@ -0,0 +1,2 @@ +def get_time_of_arbitrum_blocks_ms(block_number: int) -> int: + return block_number * 250 \ No newline at end of file From d538fdcdcd9e6bb197c72b30e59cfa5bcb8ec2f4 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 7 Mar 2025 16:35:29 +1100 Subject: [PATCH 112/138] feat: add vesting support and updated event support to staking backend --- src/staking/app.py | 96 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 4 deletions(-) diff --git a/src/staking/app.py b/src/staking/app.py index 8a858b6..ef2e2cf 100644 --- a/src/staking/app.py +++ b/src/staking/app.py @@ -2,6 +2,7 @@ import dataclasses from dataclasses import dataclass import statistics + import flask import eth_utils from eth_typing import ChecksumAddress @@ -13,7 +14,10 @@ from ..oxen.rpc import OxenRPC from ..registration.read import DBReaderRegistrations from ..util.flask_utils import FlaskApp, json_response, FlaskAppConfig -from ..util.parse import Hex64Converter, EthConverter, eth_format +from ..util.parse import Hex64Converter, EthConverter, eth_format, parse_bls_pubkey +from ..web3client.client import Web3Client +from ..web3client.contracts_ws.service_node_contribution import ServiceNodeContribution + @dataclass class StakingAppConfig(FlaskAppConfig): @@ -65,6 +69,45 @@ def __init__(self, config: StakingAppConfig): self.allowed_contract_names = set() + self.arbitrum_sn_events = {} + self.contribution_contract_events = {} + self.arb_event_next_block = 0 + + self.contribution_contracts = {} + + self.contribution_contract_map = {} + + def get_arbitrum_events(self): + self.log.perf.start("get_arbitrum_sn_events") + missed_events = self.db_reader.get_arbitrum_events(from_block=self.arb_event_next_block, names=["NewServiceNodeV2", "ServiceNodeExitRequest", "ServiceNodeExit", "ServiceNodeLiquidated"]) + latest_block = self.arb_event_next_block - 1 + + for event in missed_events: + if event.block > latest_block: + latest_block = event.block + + if event.name == "NewServiceNodeContributionContract": + address = event.args.get("contributorContract") + if address: + self.contribution_contract_events.setdefault(address, []).append(event) + else: + contract_id = event.args.get("serviceNodeID") + if contract_id: + self.arbitrum_sn_events.setdefault(contract_id, []).append(event) + + missed_contribution_contract_events = self.db_reader.get_arbitrum_events(from_block=self.arb_event_next_block, names=["NewServiceNodeContributionContract", *ServiceNodeContribution.event_names]) + for event in missed_contribution_contract_events: + if event.block > latest_block: + latest_block = event.block + address = event.main_arg + if address: + self.contribution_contract_events.setdefault(address, []).append(event) + + self.arb_event_next_block = latest_block + 1 + + self.log.perf.end("get_arbitrum_sn_events") + return self.arbitrum_sn_events, self.contribution_contract_events + def create_app(config: StakingAppConfig) -> App: app = App(config) @@ -103,6 +146,38 @@ def get_next_block_timestamp_est(): def get_network_info_cached(): return app.cache.get("network_info", getter=get_network_info_uncached, ttl=1) + def get_arbitrum_events_cached(): + return app.cache.get("arbitrum_events_all", getter=app.get_arbitrum_events, ttl=1) + + def get_contribution_contract_contributor_map_uncached(): + _, contribution_contract_events = get_arbitrum_events_cached() + contracts = app.db_reader.get_contribution_contracts() + for address, events in contribution_contract_events.items(): + contracts[address].events.extend(events) + for contributor in contracts[address].contributors: + app.contribution_contract_map.setdefault(contributor.address, []) + app.contribution_contract_map[contributor.address].append(contracts[address]) + return app.contribution_contract_map + + def get_contribution_contract_map_cached(): + return app.cache.get("contribution_contracts", getter=get_contribution_contract_contributor_map_uncached, ttl=1) + + def get_contribution_contracts_for_address_uncached(address: str): + return get_contribution_contract_map_cached().get(address, []) + + def get_contribution_contracts_for_address_cached(address: str): + return app.cache.get(f"contribution_contracts-{address}", getter=get_contribution_contracts_for_address_uncached, getter_args=address, ttl=1) + + def get_vesting_contracts_cached(): + return app.cache.get("vesting_contracts", getter=app.db_reader.get_vesting_contracts, ttl=120) + + def get_vesting_contracts_for_beneficiary_cached(beneficiary: str): + contracts = [] + for contract in get_vesting_contracts_cached(): + if contract.beneficiary == beneficiary: + contracts.append(contract) + return contracts + def json_res(vals, include_network_info=True): if include_network_info: network_info, arbitrum_info = get_network_info_cached() @@ -123,9 +198,21 @@ def get_nodes_cached(): def route_get_nodes(): return json_res({"nodes": get_nodes_cached()}) + def get_added_bls_keys(): + events_exit = app.db_reader.get_arbitrum_events_by_name("ServiceNodeExit") + sn_ids_exited = set([event.args["serviceNodeID"] for event in events_exit]) + + contract_id_map = {} + for event in app.db_reader.get_arbitrum_events_by_name("NewServiceNodeV2"): + sn_id = event.args["serviceNodeID"] + if sn_id not in sn_ids_exited: + contract_id_map[parse_bls_pubkey(event.args["pubkey"])] = sn_id + return contract_id_map + + def get_nodes_bls_keys_cached(): - return app.cache.get("contract_node_bls_keys_added", - getter=app.db_reader.get_service_node_rewards_contract_id_bls_key_map) + return app.cache.get("contract_node_bls_keys_added", getter=get_added_bls_keys) + @app.route("/nodes/bls") def route_get_nodes_bls_keys(): @@ -164,7 +251,8 @@ def route_get_stakes_for_eth_address(eth_wal: str): try: address = eth_format(eth_wal) return json_res({"stakes": get_related_stakes_for_eth_address_cached(address), - "contracts": get_related_contribution_contracts_for_eth_address_cached(address), + "contracts": get_contribution_contracts_for_address_cached(address), + "vesting": get_vesting_contracts_for_beneficiary_cached(address), "added_bls_keys": get_nodes_bls_keys_cached()}) except ValueError as e: From c67f4f1c080836b11f6083766a55963e68f6989c Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 7 Mar 2025 16:37:28 +1100 Subject: [PATCH 113/138] feat: add vesting support and updated event support to staking backend --- src/app_fetcher.py | 4 +- src/staking/dataclasses.py | 13 +++ src/staking/fetcher.py | 181 ++++++++++++++----------------------- 3 files changed, 84 insertions(+), 114 deletions(-) diff --git a/src/app_fetcher.py b/src/app_fetcher.py index d9d4782..29c2c5a 100644 --- a/src/app_fetcher.py +++ b/src/app_fetcher.py @@ -2,5 +2,5 @@ from src import config from src.staking.fetcher import App -app = App(config.backend.fetcher_name if config.backend.fetcher_name else __name__) -app.run() \ No newline at end of file +app = App("fetcher") +app.run() diff --git a/src/staking/dataclasses.py b/src/staking/dataclasses.py index 5a642f5..1de25ad 100644 --- a/src/staking/dataclasses.py +++ b/src/staking/dataclasses.py @@ -86,6 +86,18 @@ def __post_init__(self): } +@dataclass +class VestingContract: + address: str + beneficiary: str + initial_amount: int + initial_beneficiary: str + revoker: str + time_end: int + time_start: int + transferable_beneficiary: bool + + @dataclass class DBNetworkInfo: id: Optional[int] @@ -128,6 +140,7 @@ class DBContributionContract: status: int # Not in db but added after select contributors: list | None + events: list | None @dataclass diff --git a/src/staking/fetcher.py b/src/staking/fetcher.py index 7d8ec49..7660e28 100644 --- a/src/staking/fetcher.py +++ b/src/staking/fetcher.py @@ -3,12 +3,11 @@ import subprocess import time +from ..config_validate import validate_log_config, validate_contract_addresses, validate_web3_client, validate_oxen_rpc from ..staking.arbitrum import ( - get_service_node_rewards_contract_id_map, get_new_contribution_contracts, - update_contribution_contract_details, batch_populate_events_with_block_timestamps, populate_events_with_main_arg, + update_contribution_contract_details, populate_events_with_main_arg, ) -from ..config_validate import validate_config from .. import config from ..staking.dataclasses import RewardsInfo, DBNodeExit from ..db.util import ( @@ -37,10 +36,25 @@ class App: - def __init__(self, name): + def __init__(self, name, run_once_as_script=False): super().__init__() log = Log(name, enable_perf=config.backend.performance_logging) log.set_level(config.backend.log_level) + self.log = log.logger + self.run_once_as_script = run_once_as_script + + validate_log_config(config.backend) + validate_contract_addresses(config.backend) + + self.arbitrum_updates_disabled = config.backend.ws_enabled + if self.arbitrum_updates_disabled: + self.log.warning( + "Arbitrum updates are disabled because ws_enabled is set to True, the separate websocket fetcher should be used instead") + else: + self.log.info("Arbitrum updates are enabled") + validate_web3_client(config) + + validate_oxen_rpc(config.backend) git_rev = subprocess.run( ["git", "rev-parse", "--short=9", "HEAD"], stdout=subprocess.PIPE, text=True @@ -55,8 +69,6 @@ def __init__(self, name): else config.backend.log_level ) - self.log = log.logger - validate_config(config) if not is_db_initialized(config.backend.sqlite_db): self.log.info( "Initializing database {} with schema {}".format( @@ -76,9 +88,7 @@ def __init__(self, name): perf=config.backend.performance_logging, ) - rpc_url = ( - config.backend.rpc_fetcher if config.backend.rpc_fetcher else config.backend.rpc_shared - ) + rpc_url = config.backend.rpc_shared rpc_cache = ( config.backend.rpc_fetcher_cache if config.backend.rpc_fetcher_cache @@ -92,16 +102,10 @@ def __init__(self, name): usage_tracking=config.backend.rpc_fetcher_usage_logging, ) - self.loop_sleep_refresh_rate_seconds = rpc_cache if rpc_cache > 0 else 5 + self.loop_sleep_refresh_rate_seconds = rpc_cache if rpc_cache > 0 else 1 self.arbitrum_details_next_update_time = 0 - # looks like { pubkey_bls : { add: [], exit: []}} - self.arbitrum_node_events_bls_key_to_events_timestamps_map: dict[str, dict[str, list[int]]] = {} - - # dict of contract address to timestamp of when it was created - self.contribution_contract_creation_timestamps: dict[str, int] = {} - self.web3_client = Web3Client( provider_urls=config.backend.web3_provider_urls, caller_address=config.backend.web3_caller_address, @@ -110,7 +114,8 @@ def __init__(self, name): abi_manager=ABIManager(db_writer=self.db_writer, abi_dir=config.backend.abi_dir), ) - self.log.info(f"Using contract addresses:\n Token: {config.backend.addr_token}\n SN Rewards: {config.backend.addr_sn_rewards}\n Reward Rate Pool: {config.backend.addr_reward_rate_pool}\n SN Contribution Factory: {config.backend.addr_sn_contrib_factory}") + self.log.info( + f"Using contract addresses:\n Token: {config.backend.addr_token}\n SN Rewards: {config.backend.addr_sn_rewards}\n Reward Rate Pool: {config.backend.addr_reward_rate_pool}\n SN Contribution Factory: {config.backend.addr_sn_contrib_factory}") self.token_contract = TokenInterface( web3_client=self.web3_client, contract_address=config.backend.addr_token @@ -141,6 +146,9 @@ def __init__(self, name): max_events=config.backend.max_time_keeper_events, ) + self.last_new_sn_event = 0 + self.contract_id_map = {} + self.bootstrap() def bootstrap(self): @@ -200,9 +208,7 @@ def run(self): ) ) - if ( - time.time() >= self.arbitrum_details_next_update_time - ): + if not self.arbitrum_updates_disabled and time.time() >= self.arbitrum_details_next_update_time: self.time_keeper.add("arb_update") self.update_arbitrum_details() self.time_keeper.end("arb_update") @@ -221,26 +227,25 @@ def run(self): self.log.perf.end("loop") self.time_keeper.log_time_keeper() self.rpc.usage_tracker.log_usage() - self.rpc.usage_tracker.write_failure_reasons_to_file(f"rpc-usage-failure-reasons-fetcher.txt") + if config.backend.write_rpc_fail_reasons_to_file: + self.rpc.usage_tracker.write_failure_reasons_to_file(f"rpc-usage-failure-reasons-fetcher.txt") now = time.time() sleep_seconds = max( self.loop_sleep_refresh_rate_seconds, - min( - self.arbitrum_details_next_update_time, - network.pulse_target_timestamp, - ) - - now, + (network.pulse_target_timestamp - 5 if self.arbitrum_updates_disabled else + min(self.arbitrum_details_next_update_time, network.pulse_target_timestamp) + ) - now, ) - self.log.debug( + self.log.info( "Sleeping for {}s ({}) (Target Event: {})".format( format_seconds(sleep_seconds), format_seconds(now + sleep_seconds, 0), ( "network_update" - if sleep_seconds == network.pulse_target_timestamp - now + if sleep_seconds == network.pulse_target_timestamp - 5 - now else ( "arb_update" if sleep_seconds == self.arbitrum_details_next_update_time - now @@ -250,6 +255,10 @@ def run(self): ) ) + if self.run_once_as_script: + self.log.info("run_once_as_script is True, exiting...") + return + time.sleep(sleep_seconds) except Exception as e: @@ -260,10 +269,10 @@ def run(self): if t2_event_loop_exception_count > 3: self.log.warning( - "Too many t2 event loop exceptions, sleeping for 5 minutes before continuing" + "Too many t2 event loop exceptions, sleeping for 2 minutes before continuing" ) t2_event_loop_exception_count = 0 - time.sleep(300) + time.sleep(120) elif t1_event_loop_exception_count > 10: self.log.warning( "Too many t1 event loop exceptions, sleeping for 30 seconds before continuing" @@ -279,10 +288,9 @@ def run(self): self.log.info("Application exiting...") def update_network_details_and_nodes( - self, - network: NetworkInfo, + self, + network: NetworkInfo, ): - self.log.perf.start("update_service_node_list") self.log.info("Update service node list task start") parsed_nodes, contributor_stake_map, current_height, node_count, active_node_count = self.fetch_service_node_list() @@ -290,28 +298,13 @@ def update_network_details_and_nodes( current_height, parsed_nodes, contributor_stake_map ) - self.db_writer.write_network_info_to_db(network=network, node_count=node_count, active_node_count=active_node_count) + self.db_writer.write_network_info_to_db(network=network, node_count=node_count, + active_node_count=active_node_count) rewards_info = self.get_rewards_info() self.db_writer.write_rewards_info_to_db(rewards_info) self.log.info("Scheduled task finish") - self.log.perf.end("scheduled_task") - - def fetch_service_node_rewards_contract_id_bls_key_map(self): - self.log.perf.start("fetch_service_node_rewards_contract_id_bls_key_map") - contract_id_map = {} - try: - contract_id_map = get_service_node_rewards_contract_id_map(self.service_node_rewards) - self.log.debug( - "Found {} service node rewards contract ids".format(len(contract_id_map)) - ) - except Exception as e: - self.log.error("Error fetching and parsing service node rewards contract id bls key map") - self.log.exception(e) - finally: - self.log.perf.end("fetch_service_node_rewards_contract_id_bls_key_map") - return contract_id_map def fetch_service_node_list(self): self.log.perf.start("fetch_service_node_list") @@ -328,28 +321,32 @@ def fetch_service_node_list(self): nodes: list[ServiceNode] = res.get("service_node_states") self.log.debug("Fetched {} service nodes".format(len(nodes))) - # TODO: remove once contract_id is available via rpc.get_service_nodes - contract_id_map = self.db_reader.get_service_node_rewards_contract_id_bls_key_map() + new_sn_events = self.db_reader.get_arbitrum_events_by_name("NewServiceNodeV2", + from_block=self.last_new_sn_event + 1) + if len(new_sn_events) == 0: + self.log.warning("No new service node events found, waiting for new events") + return + + for event in new_sn_events: + self.contract_id_map[parse_bls_pubkey(event.args["pubkey"])] = event.args["serviceNodeID"] + if event.block > self.last_new_sn_event: + self.last_new_sn_event = event.block for node in nodes: pubkey_bls = None try: - # TODO: remove once contract_id is available via rpc.get_service_nodes vv - pubkey_bls = node.get("pubkey_bls") - contract_id = contract_id_map.get(pubkey_bls) - if node.get("active"): active_node_count += 1 - if contract_id is None: + pubkey_bls = node.get("pubkey_bls") + contract_id = self.contract_id_map.get(pubkey_bls) + node["contract_id"] = contract_id + + if node["contract_id"] is None: self.log.warning( "Contract ID not found for node with BLS pubkey: {}".format(pubkey_bls) ) - node["contract_id"] = contract_id - # TODO: remove once contract_id is available via rpc.get_service_nodes ^^ - - # contract_id = node.get("contract _id") - # assert contract_id is not None + assert node["contract_id"] is not None # Remove some fields that might appear if field:all is passed to the rpc if "portions_for_operator" in node: @@ -382,8 +379,6 @@ def fetch_service_node_list(self): else None ) - assert node["contract_id"] is not None - assert_all_dict_values_are_within_sqlite_integer_range(node) for contributor in node.get("contributors", []): @@ -405,12 +400,7 @@ def fetch_service_node_list(self): ) except Exception as e: - self.log.error( - "Error processing contributor {} for node {}".format( - contributor_address, - pubkey_bls, - ) - ) + self.log.error(f"Error processing contributor {contributor_address} for node {pubkey_bls}") self.log.exception(e) continue @@ -460,7 +450,6 @@ def update_exit_list(self): self.log.info("Update exit list task finish") self.log.perf.end("update_exit_list") - def get_rewards_info(self): self.log.perf.start("update_rewards_details") self.log.debug("Update rewards details task start") @@ -470,9 +459,10 @@ def get_rewards_info(self): accrued_rewards_json = self.rpc.get_accrued_rewards().get() assert accrued_rewards_json is not None, "Accrued rewards request failed" - assert accrued_rewards_json["status"] == "OK", "Accrued rewards request failed {}".format(accrued_rewards_json) - assert "balances" in accrued_rewards_json, "Accrued rewards request failed, 'balances' key was missing: {}".format(accrued_rewards_json) - + assert accrued_rewards_json["status"] == "OK", "Accrued rewards request failed {}".format( + accrued_rewards_json) + assert "balances" in accrued_rewards_json, "Accrued rewards request failed, 'balances' key was missing: {}".format( + accrued_rewards_json) # Populate (Binary ETH wallet address -> accrued_rewards) table for address_hex, rewards in accrued_rewards_json.get("balances").items(): @@ -491,7 +481,6 @@ def get_rewards_info(self): self.log.perf.end("update_rewards_details") return rewards_info - def update_arbitrum_details(self): try: self.log.perf.start("update_arbitrum_details") @@ -501,13 +490,12 @@ def update_arbitrum_details(self): current_block = self.web3_client.web3.eth.block_number end_block = current_block - 1 - contract_id_map = self.fetch_service_node_rewards_contract_id_bls_key_map() - self.db_writer.write_service_node_rewards_contract_id_bls_key_map(contract_id_map) - service_node_rewards_balance = self.token_contract.balance_of(self.service_node_rewards.contract_address) reward_rate_pool_balance = self.token_contract.balance_of(self.reward_rate_pool.contract_address) - self.log.debug("Arbitrum info: service node rewards balance {}, reward rate pool balance {}".format(service_node_rewards_balance, reward_rate_pool_balance)) - self.db_writer.write_arbitrum_info_to_db(current_block, service_node_rewards_balance, reward_rate_pool_balance) + self.log.debug("Arbitrum info: service node rewards balance {}, reward rate pool balance {}".format( + service_node_rewards_balance, reward_rate_pool_balance)) + self.db_writer.write_arbitrum_info_to_db(current_block, service_node_rewards_balance, + reward_rate_pool_balance) new_contribution_contracts, new_contribution_events = get_new_contribution_contracts( self.web3_client, @@ -536,7 +524,6 @@ def update_arbitrum_details(self): ) events.extend(new_contribution_events) - batch_populate_events_with_block_timestamps(self.web3_client, self.log, events) populate_events_with_main_arg(events) self.db_writer.write_arbitrum_events_to_db(events) @@ -549,10 +536,7 @@ def update_arbitrum_details(self): self.web3_client, self.log, contrib_contract_list ) - node_add_timestamps, node_last_added_timestamps = self.update_arbitrum_node_event_timestamps() - self.db_writer.write_contribution_contracts_to_db( - contract_details_list, contributions_list, node_add_timestamps, node_last_added_timestamps, self.contribution_contract_creation_timestamps - ) + self.db_writer.write_contribution_contracts_to_db(contract_details_list, contributions_list) else: self.log.info("No contribution contracts to write to db") @@ -562,30 +546,3 @@ def update_arbitrum_details(self): except Exception as e: self.log.error("Error fetching and parsing arbitrum details") self.log.exception(e) - - - def update_arbitrum_node_event_timestamps(self): - recent_node_events = self.db_reader.get_arbitrum_events_since_timestamp([self.arbitrum_details_next_update_time, ['NewServiceNodeV2', 'ServiceNodeExit', 'ServiceNodeLiquidated', 'NewServiceNodeContributionContract']]) - for event in recent_node_events: - if event.name == "NewServiceNodeContributionContract": - self.contribution_contract_creation_timestamps[event.main_arg] = event.timestamp - else: - pubkey_bls_encoded = event.args.get("pubkey") - pubkey_bls = "0x{}".format(parse_bls_pubkey((pubkey_bls_encoded["X"], pubkey_bls_encoded["Y"]))) - if event.name == "NewServiceNodeV2": - self.arbitrum_node_events_bls_key_to_events_timestamps_map.setdefault(pubkey_bls, {}).setdefault("add", []).append(event.timestamp) - elif event.name == "ServiceNodeExit" or event.name == "ServiceNodeLiquidated": - self.arbitrum_node_events_bls_key_to_events_timestamps_map.setdefault(pubkey_bls, {}).setdefault("exit", []).append(event.timestamp) - - - node_add_timestamps = {} - node_last_added_timestamps = {} - for pubkey_bls, event_timestamps in self.arbitrum_node_events_bls_key_to_events_timestamps_map.items(): - add_events = event_timestamps.get("add", []) - exit_events = event_timestamps.get("exit", []) - last_added_timestamp = max(add_events) if len(add_events) > 0 else None - node_last_added_timestamps[pubkey_bls] = last_added_timestamp - node_add_timestamps[pubkey_bls] = last_added_timestamp if len(add_events) > len(exit_events) else None - - return node_add_timestamps, node_last_added_timestamps - From c1451e4d52a17a57ba8dfa9c440bf88006ed15d3 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Wed, 2 Apr 2025 14:50:37 +1100 Subject: [PATCH 114/138] fix: rework price api to simplify token fetching behaviour --- src/price/app.py | 71 ++++++++++------------------- src/price/coingecko.py | 46 +++++-------------- src/price/dataclasses.py | 1 - src/price/read.py | 67 ++++++--------------------- src/price/schema.sql | 3 -- src/price/write.py | 97 +++++++++------------------------------- 6 files changed, 70 insertions(+), 215 deletions(-) diff --git a/src/price/app.py b/src/price/app.py index 57097bd..bd902f9 100644 --- a/src/price/app.py +++ b/src/price/app.py @@ -1,11 +1,13 @@ #!/usr/bin/env python3 from dataclasses import dataclass +import flask + from ..util.flask_utils import FlaskApp, json_response, FlaskAppConfig from .coingecko import CoinGeckoTokenPriceRequest -from .read import DBReaderPrices +from .read import get_latest_price from .dataclasses import PriceDB -from .write import DBWriterPrices +from .write import write_prices_to_db from ..db.util import is_db_initialized, init_db @@ -21,12 +23,10 @@ class PriceAppConfig(FlaskAppConfig): coingecko_api_key: str = None coingecko_api_url: str = None coingecko_api_token_ids: list[str] = None - coingecko_api_currencies: list[str] = None # Route Config coingecko_api_rate_poll_rate_seconds: int = None default_token: str = None - default_currency: str = None class App(FlaskApp): @@ -36,24 +36,19 @@ def __init__(self, config: PriceAppConfig): if config.enable_price_fetcher: self.log.info(f"Price fetcher enabled, fetching from {config.coingecko_api_url}") - if is_db_initialized(config.sqlite_db): + if not is_db_initialized(config.sqlite_db): self.log.info(f"Initializing database {config.sqlite_db} with schema {config.sqlite_schema}") init_db(config.sqlite_db, config.sqlite_schema) - self.db_writer_prices = DBWriterPrices( - db_path=config.sqlite_db, - log_level=config.log_level, - perf=config.enable_perf, - ) else: - self.log.info("Price fetcher disabled. No API url provided.") + self.log.info("Price fetcher disabled. Set enable_price_fetcher to True to enable price fetcher.") + if not config.default_token: + config.default_token = config.coingecko_api_token_ids[0] + self.log.warning(f"No default token set, using {config.default_token}") + else: + self.log.info(f"Default token set to {config.default_token}") - self.db_reader_prices = DBReaderPrices( - db_path=config.sqlite_db, - log_level=config.log_level, - perf=config.enable_perf, - disable_db_file_rewrite=config.disable_db_file_rewrite, - ) + self.db_path = config.sqlite_db if config.enable_price_fetcher: self.token_price_request = CoinGeckoTokenPriceRequest( @@ -61,7 +56,6 @@ def __init__(self, config: PriceAppConfig): key=config.coingecko_api_key, url=config.coingecko_api_url, token_ids=config.coingecko_api_token_ids, - currencies=config.coingecko_api_currencies, include_market_cap=True, include_last_updated_at=True, ) @@ -73,43 +67,26 @@ def __init__(self, config: PriceAppConfig): def get_token_price_cache_key(token: str): return f"price-{token}-all" - def get_token_info_cached(self, token: str): - key = App.get_token_price_cache_key(token) - - data: list[PriceDB] | None = self.cache.get_cached_only(key) - - if data: - return data + def get_price_for_token_uncached(self, token: str): + return get_latest_price(self.db_path, token) - data = self.db_reader_prices.get_latest_prices(token) - - updated_at = data[0].updated_at - - stale_time = updated_at + self.price_poll_rate_seconds - self.cache.set_cache_value(key, data, invalidate_timestamp=stale_time) - return data - - def get_price_for_token_uncached(self, params: [str, str]): - for price in self.get_token_info_cached(params[0]): - if price.currency == params[1]: - return price - return None - - def get_price_for_token_cached(self, token: str, currency: str) -> PriceDB | None: - return self.cache.get(f"price-{token}-{currency}", getter=self.get_price_for_token_uncached, - getter_args=[token, currency], ttl=1) + def get_price_for_token_cached(self, token: str) -> PriceDB | None: + return self.cache.get(self.get_token_price_cache_key(token), getter=self.get_price_for_token_uncached, + getter_args=token, ttl=1) def get_token_price_info(self, token: str = None): if token is None: token = self.app_config.default_token - data = self.get_price_for_token_cached(token, self.app_config.default_currency) + if token not in self.app_config.coingecko_api_token_ids: + return flask.abort(404, f"Token {token} not found!") + + data = self.get_price_for_token_cached(token) if data is None: - return json_response({"error": "Failed to fetch price"}) + return flask.abort(500, f"Failed to fetch price for token {token}") - key = App.get_token_price_cache_key(token) - stale_time = self.cache.get_stale_timestamp(key) + stale_time = self.cache.get_stale_timestamp(self.get_token_price_cache_key(token)) return { "usd": data.price, @@ -161,7 +138,7 @@ def fetch_token_price_info(signum): app.logger.info("Fetch token price info start") data = app.token_price_request.get() formatted_data = app.token_price_request.format_for_db(data) - app.db_writer_prices.write_prices_to_db(formatted_data) + write_prices_to_db(app.db_path, formatted_data) app.logger.info("Fetch token price info finish") fetch_token_price_info(None) diff --git a/src/price/coingecko.py b/src/price/coingecko.py index 7bb4bb9..3628bee 100644 --- a/src/price/coingecko.py +++ b/src/price/coingecko.py @@ -5,11 +5,9 @@ class CoinGeckoTokenPriceRequest: - def __init__(self, logger: logging, key: str, url: str, token_ids: list[str], currencies: list[str], - include_market_cap: bool = True, include_last_updated_at: bool = True): + def __init__(self, logger: logging, key: str, url: str, token_ids: list[str], include_market_cap: bool = True, include_last_updated_at: bool = True): self.log = logger self.token_ids = token_ids - self.currencies = currencies self.headers = { "accept": "application/json", "x-cg-demo-api-key": key @@ -17,7 +15,7 @@ def __init__(self, logger: logging, key: str, url: str, token_ids: list[str], cu query_params = { "ids": "%2C".join(token_ids), - "vs_currencies": "%2C".join(currencies), + "vs_currencies": "%2C".join(['usd']), } if include_market_cap: @@ -40,17 +38,11 @@ def get(self): "usd_market_cap": 3061960433.734907, "aud": 1.12, "aud_market_cap": 4859220977.76168, - "eur": 0.675465, - "eur_market_cap": 2934457339.3137517, "last_updated_at": 1737685901 }, "ethereum": { "usd": 3305.97, "usd_market_cap": 398379929097.7145, - "aud": 5245.21, - "aud_market_cap": 632065213327.2137, - "eur": 3167.98, - "eur_market_cap": 381751949237.1051, "last_updated_at": 1737685896 } } @@ -63,7 +55,7 @@ def get(self): self.log.warning("Fetch token price info error: {}".format(response)) return None - def format_for_db(self, response: dict, as_list: bool = True): + def format_for_db(self, response: dict): """ Converts the response from the CoinGecko API to a format that can be stored in the database. @@ -72,24 +64,16 @@ def format_for_db(self, response: dict, as_list: bool = True): "arbitrum": { "usd": 0.704886, "usd_market_cap": 3061960433.734907, - "aud": 1.12, - "aud_market_cap": 4859220977.76168, - "eur": 0.675465, - "eur_market_cap": 2934457339.3137517, "last_updated_at": 1737685901 }, "ethereum": { "usd": 3305.97, "usd_market_cap": 398379929097.7145, - "aud": 5245.21, - "aud_market_cap": 632065213327.2137, - "eur": 3167.98, - "eur_market_cap": 381751949237.1051, "last_updated_at": 1737685896 } } """ - result = [] if as_list else {} + result = [] for token in self.token_ids: if token not in response: @@ -97,20 +81,12 @@ def format_for_db(self, response: dict, as_list: bool = True): continue updated_at = response[token].get("last_updated_at", None) - for currency in self.currencies: - market_cap = response[token].get(f"{currency}_market_cap", None) - price = response[token].get(currency, None) - if price is None: - self.log.warning(f"Currency {currency} not found in CoinGecko API response for token {token}") - continue - - data = PriceDB(token=token, currency=currency, price=price, market_cap=market_cap, - updated_at=updated_at) - - if as_list: - result.append(data) - else: - result[token] = {} if result[token] is None else result[token] - result[token][currency] = data + market_cap = response[token].get(f"usd_market_cap", None) + price = response[token].get("usd", None) + if price is None: + self.log.warning(f"USD price not found in CoinGecko API response for token {token}") + + result.append(PriceDB(token=token, price=price, market_cap=market_cap, updated_at=updated_at)) return result + diff --git a/src/price/dataclasses.py b/src/price/dataclasses.py index 9760d96..878595c 100644 --- a/src/price/dataclasses.py +++ b/src/price/dataclasses.py @@ -4,7 +4,6 @@ @dataclass class PriceDB: token: str - currency: str price: float market_cap: float updated_at: int diff --git a/src/price/read.py b/src/price/read.py index 53ff460..2391e83 100644 --- a/src/price/read.py +++ b/src/price/read.py @@ -1,56 +1,17 @@ from contextlib import closing from .dataclasses import PriceDB -from ..db.read import DBReader - - -class DBReaderPrices(DBReader): - def __init__(self, db_path: str, log_level: int, perf: bool = False, disable_db_file_rewrite: bool = False): - super().__init__(db_path, log_level, perf, disable_db_file_rewrite) - - def get_latest_price(self, token: str, currency: str): - self.log.perf.start("get_latest_price") - with closing(self.connect()) as connection: - with closing(connection.cursor()) as cursor: - cursor.execute( - """ - SELECT * FROM prices WHERE token = ? AND currency = ? ORDER BY updated_at DESC LIMIT 1 - """, - (token, currency), - ) - price = PriceDB(*cursor.fetchone()) - self.log.debug("Price: {}".format(price)) - self.log.perf.end("get_latest_price") - return price - - def get_latest_prices(self, token: str): - self.log.perf.start("get_latest_prices") - with closing(self.connect()) as connection: - with closing(connection.cursor()) as cursor: - cursor.execute( - """ - SELECT * FROM prices WHERE token = ? ORDER BY updated_at DESC LIMIT 100 - """, - (token,), - ) - - prices = [PriceDB(*price) for price in cursor.fetchall()] - - self.log.debug("Prices: {}".format(len(prices))) - self.log.perf.end("get_latest_prices") - return prices - - def get_unique_currencies(self, token: str): - self.log.perf.start("get_unique_currencies") - with closing(self.connect()) as connection: - with closing(connection.cursor()) as cursor: - cursor.execute( - """ - SELECT DISTINCT currency FROM prices WHERE token = ? - """, - (token,), - ) - currencies = [currency[0] for currency in cursor.fetchall()] - self.log.debug("Currencies: {}".format(len(currencies))) - self.log.perf.end("get_unique_currencies") - return currencies +from ..db.util import sql_connect_in_read_mode + + +def get_latest_price(db_path: str, token: str): + with closing(sql_connect_in_read_mode(db_path)) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute( + """ + SELECT * FROM prices WHERE token = ? ORDER BY updated_at DESC LIMIT 1 + """, + (token,), + ) + price = PriceDB(*cursor.fetchone()) + return price diff --git a/src/price/schema.sql b/src/price/schema.sql index 15a20a7..8473522 100644 --- a/src/price/schema.sql +++ b/src/price/schema.sql @@ -2,7 +2,6 @@ PRAGMA journal_mode=WAL; CREATE TABLE prices ( token TEXT NOT NULL, - currency TEXT NOT NULL, price FLOAT NOT NULL, market_cap FLOAT NOT NULL, updated_at INTEGER NOT NULL @@ -10,5 +9,3 @@ CREATE TABLE prices ( CREATE INDEX prices_updated_at_idx ON prices(updated_at DESC); CREATE INDEX prices_token_at_idx ON prices(token, updated_at DESC); -CREATE INDEX prices_token_currency_idx ON prices(token, currency, updated_at DESC); -CREATE INDEX prices_currency_idx ON prices(currency, updated_at DESC); diff --git a/src/price/write.py b/src/price/write.py index 82ba12c..21bcfa1 100644 --- a/src/price/write.py +++ b/src/price/write.py @@ -1,81 +1,26 @@ from contextlib import closing from .dataclasses import PriceDB -from ..db.write import DBWriter - - -class DBWriterPrices(DBWriter): - def __init__(self, db_path: str, log_level: int, perf: bool = False): - super().__init__(db_path, log_level, perf) - - def write_prices_to_db(self, prices: list[PriceDB]): - self.log.perf.start("write_prices_to_db") - - with closing(self.connect()) as connection: - connection.execute("BEGIN") - with closing(connection.cursor()) as cursor: - self.log.debug("Inserting {} prices".format(len(prices))) - self.log.perf.start("write_prices_to_db -> insert prices") - - cursor.executemany( - """ - INSERT INTO prices (token, currency, price, market_cap, updated_at) - VALUES (?, ?, ?, ?, ?) - """, +from ..db.util import sql_connect_in_write_mode + + +def write_prices_to_db(db_path: str, prices: list[PriceDB]): + with closing(sql_connect_in_write_mode(db_path)) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + cursor.executemany( + """ + INSERT INTO prices (token, price, market_cap, updated_at) + VALUES (?, ?, ?, ?) + """, + ( ( - ( - price.token, - price.currency, - price.price, - price.market_cap, - price.updated_at, - ) - for price in prices - ), - ) - - inserted_rows = cursor.rowcount - - self.log.perf.end("write_prices_to_db -> insert prices") - self.log.debug("Inserted {} rows into prices".format(inserted_rows)) - - connection.commit() - self.log.perf.end("write_prices_to_db") - - def write_registration_to_db(self, registration): - self.log.perf.start("write_registration_to_db") - with closing(self.connect()) as connection: - connection.execute("BEGIN") - with closing(connection.cursor()) as cursor: - self.log.debug("Inserting {} registration".format(len(registration))) - self.log.perf.start("write_registration_to_db -> insert registration") - - cursor.execute( - """ - INSERT OR REPLACE INTO registrations ( - contract, - operator, - pubkey_bls, - pubkey_ed25519, - sig_bls, - sig_ed25519 + price.token, + price.price, + price.market_cap, + price.updated_at, ) - VALUES (?, ?, ?, ?, ?, ?) - """, - ( - registration.get("contract"), - registration.get("operator"), - registration.get("pubkey_bls"), - registration.get("pubkey_ed25519"), - registration.get("sig_bls"), - registration.get("sig_ed25519"), - ), - ) - - inserted_rows = cursor.rowcount - - self.log.perf.end("write_registration_to_db -> insert registration") - self.log.debug("Inserted {} rows into registration".format(inserted_rows)) - - connection.commit() - self.log.perf.end("write_registration_to_db") + for price in prices + ), + ) + connection.commit() From 0c75a79a0ca6c9922d92c77f2b2444a34965ba2c Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Wed, 2 Apr 2025 14:59:59 +1100 Subject: [PATCH 115/138] fix: add db read mode utils --- src/app_price.py | 3 +-- src/db/util.py | 13 +++++++++++++ src/db/write.py | 12 ------------ 3 files changed, 14 insertions(+), 14 deletions(-) delete mode 100644 src/db/write.py diff --git a/src/app_price.py b/src/app_price.py index 0b885de..c91bc95 100644 --- a/src/app_price.py +++ b/src/app_price.py @@ -12,10 +12,9 @@ coingecko_api_key=config.backend.coingecko_api_key, coingecko_api_url=config.backend.coingecko_api_url, coingecko_api_token_ids=config.backend.coingecko_api_token_ids, - coingecko_api_currencies=config.backend.coingecko_api_currencies, coingecko_api_rate_poll_rate_seconds=config.backend.prices_api_refetch_interval_seconds, default_token=config.backend.prices_api_default_token, - default_currency=config.backend.prices_api_default_currency, + enable_price_fetcher=config.backend.enable_price_fetcher, ) app = create_app(price_config) diff --git a/src/db/util.py b/src/db/util.py index 77ea0a1..9332bc6 100644 --- a/src/db/util.py +++ b/src/db/util.py @@ -30,3 +30,16 @@ def assert_all_dict_values_are_within_sqlite_integer_range(node: dict): assert ( SQLITE_MIN_INT <= value <= SQLITE_MAX_INT ), f"Integer value {value} for key '{key}' in dict is out of SQLite integer range." + + +def sql_connect_in_read_mode(db_path: str): + if not db_path.startswith("file:"): + db_path = "file:" + db_path + + if not db_path.endswith("?mode=ro"): + db_path = db_path + "?mode=ro" + + return sqlite3.connect(db_path, uri=True) + +def sql_connect_in_write_mode(db_path: str): # Maybe dubious, but perhaps good for API symmetry, I'll defer to you + return sqlite3.connect(db_path) \ No newline at end of file diff --git a/src/db/write.py b/src/db/write.py deleted file mode 100644 index 9606439..0000000 --- a/src/db/write.py +++ /dev/null @@ -1,12 +0,0 @@ -import sqlite3 - -from ..log import Log - -class DBWriter: - def __init__(self, db_path: str, log_level: int, perf: bool = False): - self.db_path = db_path - self.log = Log("db_writer", log_level, enable_perf=perf).logger - self.log.info(f"Connecting to db at {db_path}") - - def connect(self): - return sqlite3.connect(self.db_path) From aaa155533b81e6e67ed6e5b872cbe59a250c89f1 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Wed, 2 Apr 2025 15:04:05 +1100 Subject: [PATCH 116/138] feat: add ws contract classes --- src/web3client/contract_factory.py | 2 +- src/web3client/contracts_ws/ierc_1967.py | 8 ++- .../ownable_2_step_upgradeable.py | 8 ++- .../contracts_ws/pausable_upgradeable.py | 8 ++- .../contracts_ws/reward_rate_pool.py | 8 ++- .../contracts_ws/service_node_contribution.py | 34 ++++++++--- .../service_node_contribution_factory.py | 56 ++++++++++++++----- .../contracts_ws/service_node_rewards.py | 32 ++++++----- src/web3client/contracts_ws/subscription.py | 5 +- src/web3client/contracts_ws/token.py | 14 +++-- .../contracts_ws/token_vesting_staking.py | 5 +- 11 files changed, 127 insertions(+), 53 deletions(-) diff --git a/src/web3client/contract_factory.py b/src/web3client/contract_factory.py index 809ba4e..4394548 100644 --- a/src/web3client/contract_factory.py +++ b/src/web3client/contract_factory.py @@ -5,7 +5,7 @@ SOLC_VERSION = "0.8.26" install_solc(SOLC_VERSION) -base_path = pathlib.Path(__file__).parent.parent.parent.parent.joinpath("session-token-contracts") +base_path = pathlib.Path(__file__).parent.parent.parent.joinpath("session-token-contracts") subprocess.run(["pnpm", "install"], cwd=base_path) diff --git a/src/web3client/contracts_ws/ierc_1967.py b/src/web3client/contracts_ws/ierc_1967.py index 03cce53..df6f031 100644 --- a/src/web3client/contracts_ws/ierc_1967.py +++ b/src/web3client/contracts_ws/ierc_1967.py @@ -2,6 +2,7 @@ from eth_typing import ChecksumAddress from web3 import AsyncWeb3 +from web3.contract.async_contract import AsyncContractEvent from src.staking.write import DBWriterStaking from src.web3client.contracts_ws.contract_ws import ContractWS @@ -16,14 +17,17 @@ def __init__(self, w3: AsyncWeb3, db_writer: DBWriterStaking, log: logging, event_queue: EventQueueManager | None = None): super().__init__(self.name, w3, db_writer, log, event_queue) - def create_subscriptions(self, address: ChecksumAddress | list[ChecksumAddress]): + def get_events(self, address: ChecksumAddress | list[ChecksumAddress]) -> list[AsyncContractEvent]: events = self.factory(address[0] if isinstance(address, list) else address).events - event_list = [ + return [ events.Upgraded, events.AdminChanged, events.BeaconUpgraded, ] + def create_subscriptions(self, address: ChecksumAddress | list[ChecksumAddress]): + event_list = self.get_events(address) + for event in event_list: event.address = address diff --git a/src/web3client/contracts_ws/ownable_2_step_upgradeable.py b/src/web3client/contracts_ws/ownable_2_step_upgradeable.py index 4d9a01e..c5b09bf 100644 --- a/src/web3client/contracts_ws/ownable_2_step_upgradeable.py +++ b/src/web3client/contracts_ws/ownable_2_step_upgradeable.py @@ -2,6 +2,7 @@ from eth_typing import ChecksumAddress from web3 import AsyncWeb3 +from web3.contract.async_contract import AsyncContractEvent from src.staking.write import DBWriterStaking from src.web3client.contracts_ws.contract_ws import ContractWS @@ -16,14 +17,17 @@ def __init__(self, w3: AsyncWeb3, db_writer: DBWriterStaking, log: logging, event_queue: EventQueueManager | None = None): super().__init__(self.name, w3, db_writer, log, event_queue) - def create_subscriptions(self, address: ChecksumAddress | list[ChecksumAddress]): + def get_events(self, address: ChecksumAddress | list[ChecksumAddress]) -> list[AsyncContractEvent]: events = self.factory(address[0] if isinstance(address, list) else address).events - event_list = [ + return [ events.OwnershipTransferStarted, events.OwnershipTransferred, events.Initialized, ] + def create_subscriptions(self, address: ChecksumAddress | list[ChecksumAddress]): + event_list = self.get_events(address) + for event in event_list: event.address = address diff --git a/src/web3client/contracts_ws/pausable_upgradeable.py b/src/web3client/contracts_ws/pausable_upgradeable.py index c203871..551c840 100644 --- a/src/web3client/contracts_ws/pausable_upgradeable.py +++ b/src/web3client/contracts_ws/pausable_upgradeable.py @@ -2,6 +2,7 @@ from eth_typing import ChecksumAddress from web3 import AsyncWeb3 +from web3.contract.async_contract import AsyncContractEvent from src.staking.write import DBWriterStaking from src.web3client.contracts_ws.contract_ws import ContractWS @@ -16,13 +17,16 @@ def __init__(self, w3: AsyncWeb3, db_writer: DBWriterStaking, log: logging, event_queue: EventQueueManager | None = None): super().__init__(self.name, w3, db_writer, log, event_queue) - def create_subscriptions(self, address: ChecksumAddress | list[ChecksumAddress]): + def get_events(self, address: ChecksumAddress | list[ChecksumAddress]) -> list[AsyncContractEvent]: events = self.factory(address[0] if isinstance(address, list) else address).events - event_list = [ + return [ events.Paused, events.Unpaused, ] + def create_subscriptions(self, address: ChecksumAddress | list[ChecksumAddress]): + event_list = self.get_events(address) + for event in event_list: event.address = address diff --git a/src/web3client/contracts_ws/reward_rate_pool.py b/src/web3client/contracts_ws/reward_rate_pool.py index 47afdac..17460e2 100644 --- a/src/web3client/contracts_ws/reward_rate_pool.py +++ b/src/web3client/contracts_ws/reward_rate_pool.py @@ -2,6 +2,7 @@ from eth_typing import ChecksumAddress from web3 import AsyncWeb3 +from web3.contract.async_contract import AsyncContractEvent from web3.types import EventData from web3.utils.subscriptions import EthSubscriptionContext @@ -34,10 +35,13 @@ async def handle_event(self, event: EventData): async def handle_event_sub(self, event: EthSubscriptionContext): return await self.handle_event(self._parse_event(event)) - def create_subscriptions(self, address: ChecksumAddress): + def get_events(self, address: ChecksumAddress) -> list[AsyncContractEvent]: events = self.factory(address).events + return [events.FundsReleased] + + def create_subscriptions(self, address: ChecksumAddress): return create_subscriptions( - events=[events.FundsReleased], + events=self.get_events(address), event_queue=self.event_queue, event_abis=self.event_abis, handler_sub=self.handle_event_sub, diff --git a/src/web3client/contracts_ws/service_node_contribution.py b/src/web3client/contracts_ws/service_node_contribution.py index 9e80592..e54d566 100644 --- a/src/web3client/contracts_ws/service_node_contribution.py +++ b/src/web3client/contracts_ws/service_node_contribution.py @@ -3,6 +3,7 @@ from eth_typing import ChecksumAddress from web3 import AsyncWeb3 +from web3.contract.async_contract import AsyncContractEvent from web3.types import EventData from web3.utils.subscriptions import EthSubscriptionContext @@ -37,15 +38,19 @@ def __init__(self, address, log, db_writer: DBWriterStaking, db_reader: DBReader self.log.debug(f"Initialized {self.address} with Contributors: {self._contributors}") def update_status(self, status: int): + self.log.debug(f"Updating status of {self.address} to {status}") self.db_writer.write_update_contribution_contract_status(self.address, status) def update_manual_finalize(self, new_value: bool): + self.log.debug(f"Updating manual finalize of {self.address} to {new_value}") self.db_writer.write_update_contribution_contract_manual_finalize(self.address, new_value) def update_fee(self, new_fee: int): + self.log.debug(f"Updating fee of {self.address} to {new_fee}") self.db_writer.write_update_contribution_contract_fee(self.address, new_fee) def update_pubkeys(self, new_bls_pubkey: dict, new_ed25519_pubkey: int): + self.log.debug(f"Updating pubkeys of {self.address} to {new_bls_pubkey}, {new_ed25519_pubkey}") if new_bls_pubkey is None: self.log.warning(f"No new BLS pubkey found for {self.address}") return @@ -61,12 +66,14 @@ def _upsert_contributor(self, address: str): self._contributors.setdefault(address, ContributionContractContributor(address)) def update_contributor_new_contribution(self, address: str, amount: int): + self.log.debug(f"Updating contributor add {address} with amount {amount}") self._upsert_contributor(address) self._contributors[address].address = address self._contributors[address].amount += amount self.db_writer.write_update_contribution_contract_contributor(self.address, self._contributors[address]) def update_contributor_withdraw_contribution(self, address: str, amount: int): + self.log.debug(f"Updating contributor remove {address} with amount {amount}") if address in self._contributors: self._contributors[address].amount -= amount if self._contributors[address].amount == 0: @@ -78,24 +85,29 @@ def update_contributor_withdraw_contribution(self, address: str, amount: int): self.log.warning(f"No contributor found for address {address} to withdraw {amount}") def update_contributor_beneficiary(self, address: str, beneficiary_address: str): + self.log.debug(f"Updating contributor beneficiary {address} to {beneficiary_address}") self._upsert_contributor(address) self._contributors[address].beneficiary_address = beneficiary_address self.db_writer.write_update_contribution_contract_contributor(self.address, self._contributors[address]) def update_reserved_contributors(self, reserved_contributors: list[dict[str, str]]): + self.log.debug(f"Updating reserved contributors {reserved_contributors}") for reserved_contributor in reserved_contributors: address = reserved_contributor.get("addr") reserved_amount = reserved_contributor.get("amount") self._upsert_contributor(address) self._contributors[address].reserved = reserved_amount + self.db_writer.write_update_contribution_contract_contributor(self.address, self._contributors[address]) def update_reset(self): + self.log.debug(f"Updating reset for {self.address}") self._contributors = {} + self.db_writer.write_delete_all_contribution_contract_contributors(self.address) self.update_status(0) - self.update_reserved_contributors([]) def process_event(self, raw_event: EventData): event = create_processed_event(raw_event, main_arg=self.address) + self.log.debug(f"Processing sn event: {event}") match event.name: case "OpenForPublicContribution": return self.update_status(1) @@ -147,12 +159,11 @@ class ServiceNodeContribution(ContractWS): "Filled", "WithdrawContribution", "UpdateStakerBeneficiary", - # TODO: uncomment when these events exist in the contract - # "UpdateManualFinalize", - # "UpdateFee", - # "UpdatePubkeys", - # "UpdateReservedContributors", - # "Reset", + "UpdateManualFinalize", + "UpdateFee", + "UpdatePubkeys", + "UpdateReservedContributors", + "Reset", ] def __init__(self, w3: AsyncWeb3, db_writer: DBWriterStaking, db_reader: DBReaderStaking, log: logging, @@ -172,9 +183,13 @@ async def handle_event(self, event: EventData): async def handle_event_sub(self, event: EthSubscriptionContext): return await self.handle_event(self._parse_event(event)) - def create_subscriptions(self, address: ChecksumAddress | list[ChecksumAddress]): + + def get_events(self, address: ChecksumAddress) -> list[AsyncContractEvent]: events = self.factory(address[0] if isinstance(address, list) else address).events - event_list = [events[name] for name in self.event_names] + return [events[name] for name in self.event_names] + + def create_subscriptions(self, address: ChecksumAddress | list[ChecksumAddress], start_block: int = 0): + event_list = self.get_events(address) for event in event_list: event.address = address @@ -185,6 +200,7 @@ def create_subscriptions(self, address: ChecksumAddress | list[ChecksumAddress]) event_abis=self.event_abis, handler_sub=self.handle_event_sub, handler_past=self.handle_event, + start_block=start_block, ) batch_items = 7 diff --git a/src/web3client/contracts_ws/service_node_contribution_factory.py b/src/web3client/contracts_ws/service_node_contribution_factory.py index 239b246..41e917c 100644 --- a/src/web3client/contracts_ws/service_node_contribution_factory.py +++ b/src/web3client/contracts_ws/service_node_contribution_factory.py @@ -2,11 +2,13 @@ from eth_typing import ChecksumAddress from web3 import AsyncWeb3 +from web3.contract.async_contract import AsyncContractEvent from web3.types import EventData from web3.utils.subscriptions import EthSubscriptionContext from src.staking.read import DBReaderStaking from src.staking.write import DBWriterStaking +from src.util.parse import parse_ed25519_pubkey from src.web3client.contracts_ws.contract_ws import ContractWS from src.web3client.contracts_ws.service_node_contribution import ServiceNodeContribution from src.web3client.contracts_ws.subscription import create_subscriptions @@ -18,12 +20,15 @@ class ServiceNodeContributionFactory(ContractWS): event_names = ["NewServiceNodeContributionContract"] def __init__(self, w3: AsyncWeb3, db_writer: DBWriterStaking, db_reader: DBReaderStaking, log: logging, - event_queue: EventQueueManager | None = None): + event_queue: EventQueueManager | None = None, start_block: int = 0, topic_map = None, event_addresses = None): super().__init__(self.name, w3, db_writer, log, event_queue) self.service_node_contribution_contract = ServiceNodeContribution(w3=w3, db_writer=db_writer, db_reader=db_reader, log=log, event_queue=self.event_queue) self.bootstrap_contribution_contract_addresses = [] + self.start_block = start_block + self.topic_map = topic_map + self.event_addresses = event_addresses def add_existing_contribution_contracts(self, addresses: list[str]): self.bootstrap_contribution_contract_addresses.extend(addresses) @@ -32,28 +37,53 @@ def bootstrap_contribution_contracts(self): if len(self.bootstrap_contribution_contract_addresses) == 0: self.log.warning("No bootstrap contribution contract addresses found") return - self.log.info(f"Bootstrapping {len(self.bootstrap_contribution_contract_addresses)} contribution contracts") + self.log.info(f"Bootstrapping {len(self.bootstrap_contribution_contract_addresses)} contribution contracts from block {self.start_block}") self.service_node_contribution_contract.create_subscriptions( - address=self.bootstrap_contribution_contract_addresses) + address=self.bootstrap_contribution_contract_addresses, start_block=self.start_block) + events = self.service_node_contribution_contract.get_events(self.bootstrap_contribution_contract_addresses[0]) + for address in self.bootstrap_contribution_contract_addresses: + self.event_addresses.add(address) + for event in events: + existing_topic = self.topic_map.get(event().topic) + if existing_topic is None: + print(f"Adding topic f{event.name}: {event().topic}") + self.topic_map[event().topic] = (self.service_node_contribution_contract.event_abis[event.name], self.service_node_contribution_contract.handle_event) - async def handle_event(self, event: EventData, is_bootstrap: bool = True): + + async def handle_event(self, event: EventData): + address = event.args.get("contributorContract") + await self._handle_event(event, main_arg=address) + self.db_writer.write_new_contribution_contract(address, event.args.get("operator"), parse_ed25519_pubkey(event.args.get("serviceNodePubkey"))) + events = self.service_node_contribution_contract.get_events(address) + self.event_addresses.add(address) + for event in events: + topic = event().topic + existing_topic = self.topic_map.get(topic) + if existing_topic is None: + self.log.debug(f"Adding new topic {topic}") + self.topic_map[topic] = (self.service_node_contribution_contract.event_abis[event.name], self.service_node_contribution_contract.handle_event) + + + async def handle_event_bootstrap(self, event: EventData): address = event.args.get("contributorContract") await self._handle_event(event, main_arg=address) - self.db_writer.write_new_contribution_contract(address, event.args.get("operator")) - if is_bootstrap: - self.bootstrap_contribution_contract_addresses.append(address) - else: - self.service_node_contribution_contract.create_subscriptions(address=address) + self.db_writer.write_new_contribution_contract(address, event.args.get("operator"), parse_ed25519_pubkey(event.args.get("serviceNodePubkey"))) + self.bootstrap_contribution_contract_addresses.append(address) + async def handle_event_sub(self, event: EthSubscriptionContext): - return await self.handle_event(self._parse_event(event), is_bootstrap=False) + return await self.handle_event(self._parse_event(event)) - def create_subscriptions(self, address: ChecksumAddress): + def get_events(self, address: ChecksumAddress) -> list[AsyncContractEvent]: events = self.factory(address).events + return [events[name] for name in self.event_names] + + def create_subscriptions(self, address: ChecksumAddress): return create_subscriptions( - events=[events[name] for name in self.event_names], + events=self.get_events(address), event_queue=self.event_queue, event_abis=self.event_abis, handler_sub=self.handle_event_sub, - handler_past=self.handle_event, + handler_past=self.handle_event_bootstrap, + start_block=self.start_block, ) diff --git a/src/web3client/contracts_ws/service_node_rewards.py b/src/web3client/contracts_ws/service_node_rewards.py index 9c02266..879367e 100644 --- a/src/web3client/contracts_ws/service_node_rewards.py +++ b/src/web3client/contracts_ws/service_node_rewards.py @@ -2,6 +2,7 @@ from eth_typing import ChecksumAddress from web3 import AsyncWeb3 +from web3.contract.async_contract import AsyncContractEvent from web3.types import EventData from web3.utils.subscriptions import EthSubscriptionContext @@ -44,22 +45,25 @@ async def handle_event(self, event: EventData): async def handle_event_sub(self, event: EthSubscriptionContext): return await self.handle_event(self._parse_event(event)) - def create_subscriptions(self, address: ChecksumAddress): + def get_events(self, address: ChecksumAddress) -> list[AsyncContractEvent]: events = self.factory(address).events + return [ + events.NewSeededServiceNode, + events.NewServiceNodeV2, + events.ServiceNodeExitRequest, + events.ServiceNodeExit, + events.ServiceNodeLiquidated, + events.RewardsClaimed, + events.StakingRequirementUpdated, + events.ClaimThresholdUpdated, + events.ClaimCycleUpdated, + events.LiquidationRatiosUpdated, + events.BLSNonSignerThresholdMaxUpdated, + ] + + def create_subscriptions(self, address: ChecksumAddress): return create_subscriptions( - events=[ - events.NewSeededServiceNode, - events.NewServiceNodeV2, - events.ServiceNodeExitRequest, - events.ServiceNodeExit, - events.ServiceNodeLiquidated, - events.RewardsClaimed, - events.StakingRequirementUpdated, - events.ClaimThresholdUpdated, - events.ClaimCycleUpdated, - events.LiquidationRatiosUpdated, - events.BLSNonSignerThresholdMaxUpdated, - ], + events=self.get_events(address), event_queue=self.event_queue, event_abis=self.event_abis, handler_sub=self.handle_event_sub, diff --git a/src/web3client/contracts_ws/subscription.py b/src/web3client/contracts_ws/subscription.py index 8a27b9d..5e7596c 100644 --- a/src/web3client/contracts_ws/subscription.py +++ b/src/web3client/contracts_ws/subscription.py @@ -15,7 +15,8 @@ def create_subscriptions( event_queue: EventQueueManager, handler_sub: Callable, handler_past: Callable = None, - event_abis: dict[str, any] = None + event_abis: dict[str, any] = None, + start_block: int = None, ): assert isinstance(event_queue, EventQueueManager), "event_queue must be an instance of EventQueueManager" for event in events: @@ -25,6 +26,7 @@ def create_subscriptions( event_queue.add( event=event, handler=handler_past, + start_block=start_block, sub=LogsSubscription( label=label, address=event.address, @@ -36,7 +38,6 @@ def create_subscriptions( if event.name not in event_abis: event_abis[event.name] = event._get_event_abi() - def parse_event(event_abis, event: EthSubscriptionContext): result = event.result name = event.subscription.label diff --git a/src/web3client/contracts_ws/token.py b/src/web3client/contracts_ws/token.py index 030133f..3cb1c91 100644 --- a/src/web3client/contracts_ws/token.py +++ b/src/web3client/contracts_ws/token.py @@ -2,6 +2,7 @@ from eth_typing import ChecksumAddress from web3 import AsyncWeb3 +from web3.contract.async_contract import AsyncContractEvent from web3.types import EventData from web3.utils.subscriptions import EthSubscriptionContext @@ -51,13 +52,16 @@ async def handle_event(self, event: EventData): async def handle_event_sub(self, event: EthSubscriptionContext): return await self.handle_event(self._parse_event(event)) - def create_subscriptions(self, address: ChecksumAddress): + def get_events(self, address: ChecksumAddress) -> list[AsyncContractEvent]: events = self.factory(address).events + return [ + events.Transfer, + events.Approval, + ] + + def create_subscriptions(self, address: ChecksumAddress): return create_subscriptions( - events=[ - events.Transfer, - events.Approval, - ], + events=self.get_events(address), event_queue=self.event_queue, event_abis=self.event_abis, handler_sub=self.handle_event_sub, diff --git a/src/web3client/contracts_ws/token_vesting_staking.py b/src/web3client/contracts_ws/token_vesting_staking.py index fba0c06..e7f3651 100644 --- a/src/web3client/contracts_ws/token_vesting_staking.py +++ b/src/web3client/contracts_ws/token_vesting_staking.py @@ -8,7 +8,7 @@ from src.staking.write import DBWriterStaking from src.web3client.contracts_ws.contract_ws import ContractWS -from src.web3client.contracts_ws.subscription import create_subscriptions +from src.web3client.contracts_ws.subscription import create_subscriptions, create_processed_event from src.web3client.event_queue_manager import EventQueueManager @@ -21,6 +21,9 @@ def __init__(self, w3: AsyncWeb3, db_writer: DBWriterStaking, log: logging, async def handle_event(self, event: EventData): await self._handle_event(event, main_arg=event.address) + if event.get("event") == "BeneficiaryTransferred": + to_address = event.get("args").get("newBeneficiary") + self.db_writer.write_update_vesting_contract_beneficiary(event.address, to_address) async def handle_event_sub(self, event: EthSubscriptionContext): await self.handle_event(self._parse_event(event)) From ad32a250d2503cb976e211ffbacd6863c9fb3c2f Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Wed, 2 Apr 2025 15:58:38 +1100 Subject: [PATCH 117/138] fix: truncate price stale time --- src/price/app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/price/app.py b/src/price/app.py index bd902f9..3c94f50 100644 --- a/src/price/app.py +++ b/src/price/app.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 from dataclasses import dataclass +from math import trunc import flask @@ -86,7 +87,7 @@ def get_token_price_info(self, token: str = None): if data is None: return flask.abort(500, f"Failed to fetch price for token {token}") - stale_time = self.cache.get_stale_timestamp(self.get_token_price_cache_key(token)) + stale_time = trunc(self.cache.get_stale_timestamp(self.get_token_price_cache_key(token))) return { "usd": data.price, From e6decaa8795eb376f89a6fc27410ab5c31ceaa4b Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Wed, 2 Apr 2025 16:36:36 +1100 Subject: [PATCH 118/138] feat: add precision to coingecko api --- src/app_price.py | 1 + src/config_defaults.py | 4 ++-- src/price/app.py | 2 ++ src/price/coingecko.py | 6 +++++- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/app_price.py b/src/app_price.py index c91bc95..83fca26 100644 --- a/src/app_price.py +++ b/src/app_price.py @@ -15,6 +15,7 @@ coingecko_api_rate_poll_rate_seconds=config.backend.prices_api_refetch_interval_seconds, default_token=config.backend.prices_api_default_token, enable_price_fetcher=config.backend.enable_price_fetcher, + coingecko_precision=config.backend.coingecko_precision, ) app = create_app(price_config) diff --git a/src/config_defaults.py b/src/config_defaults.py index a06da19..454af8f 100644 --- a/src/config_defaults.py +++ b/src/config_defaults.py @@ -125,13 +125,13 @@ class Backend: coingecko_api_url: str = "https://api.coingecko.com/api" coingecko_api_token_ids: list[str] = ["ethereum", "chainflip"] coingecko_api_currencies: list[str] = ["usd", "aud"] + coingecko_precision: int = 9 # Creates a request per period limit by IP address (default is 10 requests per 10 minutes) prices_api_rate_limit: int = 10 prices_api_rate_limit_period: int = 600 prices_sqlite_db: str = "ssb-prices.db" - prices_sqlite_schema: str = "price/schema.sql" - prices_api_default_currency: str = "usd" + prices_sqlite_schema: str = "src/price/schema.sql" prices_api_default_token: str = "ethereum" prices_api_refetch_interval_seconds:int = 300 diff --git a/src/price/app.py b/src/price/app.py index 3c94f50..5b26c38 100644 --- a/src/price/app.py +++ b/src/price/app.py @@ -24,6 +24,7 @@ class PriceAppConfig(FlaskAppConfig): coingecko_api_key: str = None coingecko_api_url: str = None coingecko_api_token_ids: list[str] = None + coingecko_precision: int = None # Route Config coingecko_api_rate_poll_rate_seconds: int = None @@ -59,6 +60,7 @@ def __init__(self, config: PriceAppConfig): token_ids=config.coingecko_api_token_ids, include_market_cap=True, include_last_updated_at=True, + precision=config.coingecko_precision, ) self.price_poll_rate_seconds = config.coingecko_api_rate_poll_rate_seconds if config.coingecko_api_rate_poll_rate_seconds is not None else 0 diff --git a/src/price/coingecko.py b/src/price/coingecko.py index 3628bee..5e5d00e 100644 --- a/src/price/coingecko.py +++ b/src/price/coingecko.py @@ -5,7 +5,7 @@ class CoinGeckoTokenPriceRequest: - def __init__(self, logger: logging, key: str, url: str, token_ids: list[str], include_market_cap: bool = True, include_last_updated_at: bool = True): + def __init__(self, logger: logging, key: str, url: str, token_ids: list[str], include_market_cap: bool = True, include_last_updated_at: bool = True, precision: int = 9): self.log = logger self.token_ids = token_ids self.headers = { @@ -24,6 +24,10 @@ def __init__(self, logger: logging, key: str, url: str, token_ids: list[str], in if include_last_updated_at: query_params["include_last_updated_at"] = "true" + if precision is not None: + assert precision > 0 + query_params["precision"] = precision + query_string = "&".join([f"{key}={value}" for key, value in query_params.items()]) self.url = f"{url}/v3/simple/price?{query_string}" From d274c5523165c267f2f848c734c250364bcd9fc0 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 11 Apr 2025 16:29:41 +1000 Subject: [PATCH 119/138] feat: add dynamic price cache management to price api --- src/app_price.py | 2 +- src/price/app.py | 23 +++++++++++++++++------ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/app_price.py b/src/app_price.py index 83fca26..fb1b1e0 100644 --- a/src/app_price.py +++ b/src/app_price.py @@ -12,7 +12,7 @@ coingecko_api_key=config.backend.coingecko_api_key, coingecko_api_url=config.backend.coingecko_api_url, coingecko_api_token_ids=config.backend.coingecko_api_token_ids, - coingecko_api_rate_poll_rate_seconds=config.backend.prices_api_refetch_interval_seconds, + price_poll_rate_seconds=config.backend.prices_api_refetch_interval_seconds, default_token=config.backend.prices_api_default_token, enable_price_fetcher=config.backend.enable_price_fetcher, coingecko_precision=config.backend.coingecko_precision, diff --git a/src/price/app.py b/src/price/app.py index 5b26c38..65cc010 100644 --- a/src/price/app.py +++ b/src/price/app.py @@ -27,7 +27,7 @@ class PriceAppConfig(FlaskAppConfig): coingecko_precision: int = None # Route Config - coingecko_api_rate_poll_rate_seconds: int = None + price_poll_rate_seconds: int = None default_token: str = None @@ -63,19 +63,30 @@ def __init__(self, config: PriceAppConfig): precision=config.coingecko_precision, ) - self.price_poll_rate_seconds = config.coingecko_api_rate_poll_rate_seconds if config.coingecko_api_rate_poll_rate_seconds is not None else 0 + self.price_poll_rate_seconds = config.price_poll_rate_seconds if config.price_poll_rate_seconds is not None else 0 @staticmethod def get_token_price_cache_key(token: str): return f"price-{token}-all" - def get_price_for_token_uncached(self, token: str): - return get_latest_price(self.db_path, token) def get_price_for_token_cached(self, token: str) -> PriceDB | None: - return self.cache.get(self.get_token_price_cache_key(token), getter=self.get_price_for_token_uncached, - getter_args=token, ttl=1) + key = self.get_token_price_cache_key(token) + value = self.cache.get_cached_only(key) + + if value is None: + value = get_latest_price(self.db_path, token) + + if value is None: + return None + + invalidate_timestamp = value.updated_at + self.app_config.price_poll_rate_seconds + + self.cache.set_cache_value(key, value, ttl=self.app_config.price_poll_rate_seconds, invalidate_timestamp=invalidate_timestamp) + + return value + def get_token_price_info(self, token: str = None): if token is None: From 485394cf28e8d2ffd35bca71a88422ac080884c8 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Thu, 15 May 2025 11:26:31 +1000 Subject: [PATCH 120/138] wip: to clean --- .gitmodules | 3 +- session-token-contracts | 2 +- src/app_events.py | 1 + src/config_defaults.py | 1 + src/oxen/rpc.py | 13 +- src/registration/app.py | 2 +- src/staking/app.py | 56 ++--- src/staking/dataclasses.py | 29 ++- src/staking/fetcher.py | 45 +++- src/staking/read.py | 120 ++++++++--- src/staking/schema.sql | 11 +- src/staking/write.py | 168 +++++++++++---- src/util/parse.py | 13 +- .../contracts_ws/service_node_contribution.py | 25 +-- .../contracts_ws/service_node_rewards.py | 40 +++- src/web3client/event_queue_manager.py | 23 +- src/web3client/event_ws.py | 203 ++++++++++++++---- 17 files changed, 573 insertions(+), 182 deletions(-) diff --git a/.gitmodules b/.gitmodules index 0516dd5..7194ee5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,4 @@ [submodule "session-token-contracts"] path = session-token-contracts - url = https://github.com/oxen-io/eth-sn-contracts.git + url = https://github.com/Doy-lee/eth-sn-contracts.git + branch = doyle-update-stagenet-metadata-f-new-event-emission-on-multi-contrib diff --git a/session-token-contracts b/session-token-contracts index b908510..4cbf9bb 160000 --- a/session-token-contracts +++ b/session-token-contracts @@ -1 +1 @@ -Subproject commit b9085104fd695bb8cb8e250a546069a606b1cac1 +Subproject commit 4cbf9bb2e11b5714a64a12efbdd1933a675dd4b1 diff --git a/src/app_events.py b/src/app_events.py index 2f79873..61fa96a 100644 --- a/src/app_events.py +++ b/src/app_events.py @@ -19,6 +19,7 @@ enable_perf=config.backend.performance_logging, log_level_generic=config.backend.log_level_generic, genesis_block=config.backend.genesis_block, + contrib_factory_start_block=config.backend.contrib_factory_start_block, ws_max_run_depth=config.backend.ws_max_run_depth, ws_providers=config.backend.ws_providers, ws_max_size=config.backend.ws_max_size, diff --git a/src/config_defaults.py b/src/config_defaults.py index 454af8f..ccbbc97 100644 --- a/src/config_defaults.py +++ b/src/config_defaults.py @@ -43,6 +43,7 @@ class Backend: # so this is the first block that can be used to scan for events. Using 0 significantly slows down the scan. # genesis_block: int = 114505919 genesis_block: int = 114500919 + contrib_factory_start_block: int = 142785134 """ DB CONFIG diff --git a/src/oxen/rpc.py b/src/oxen/rpc.py index a49ca5c..25b434c 100644 --- a/src/oxen/rpc.py +++ b/src/oxen/rpc.py @@ -97,7 +97,7 @@ def bls_rewards_request(self, eth_address: str) -> FutureJSON: eth_address_for_rpc = eth_address_for_rpc[2:] result = self.FutureJSON( "rpc.bls_rewards_request", - args={"address": eth_address_for_rpc}, + args={"address": eth_address_for_rpc, "height": 0}, timeout=30, ) return result @@ -125,6 +125,17 @@ def get_last_block_header(self) -> FutureJSON: args={"fill_pow_hash": False, "get_tx_hashes": False}, ) + def get_block_headers_range(self, start_height, end_height): + return self.FutureJSON( + "rpc.get_block_headers_range", + args={ + "start_height": start_height, + "end_height": end_height, + "get_tx_hashes": False, + "fill_pow_hash": False, + }, + ) + def get_service_nodes(self) -> FutureJSON: return self.FutureJSON( "rpc.get_service_nodes", diff --git a/src/registration/app.py b/src/registration/app.py index 0a07c2f..650652e 100644 --- a/src/registration/app.py +++ b/src/registration/app.py @@ -21,7 +21,7 @@ class RegistrationAppConfig(FlaskAppConfig): sqlite_schema: str = None # Route Config - coingecko_api_rate_poll_rate_seconds: int = None + price_poll_rate_seconds: int = None default_token: str = None default_currency: str = None diff --git a/src/staking/app.py b/src/staking/app.py index ef2e2cf..f688783 100644 --- a/src/staking/app.py +++ b/src/staking/app.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import dataclasses +import math from dataclasses import dataclass import statistics @@ -9,7 +10,7 @@ from uwsgidecorators import timer from werkzeug.exceptions import GatewayTimeout -from .dataclasses import ArbitrumInfo +from .dataclasses import ArbitrumInfo, RewardsInfo from .read import DBReaderStaking from ..oxen.rpc import OxenRPC from ..registration.read import DBReaderRegistrations @@ -132,7 +133,8 @@ def get_median_operator_fee_cached(): def get_network_info_uncached() -> tuple[dict | None, ArbitrumInfo]: network_info = app.db_reader.get_network_info() - arbitrum_info = app.db_reader.get_arbitrum_info() + # arbitrum_info = app.db_reader.get_arbitrum_info() + arbitrum_info = ArbitrumInfo(0, 0, 0, 0) if network_info is None: return None, arbitrum_info network_info = dataclasses.asdict(network_info) @@ -149,27 +151,15 @@ def get_network_info_cached(): def get_arbitrum_events_cached(): return app.cache.get("arbitrum_events_all", getter=app.get_arbitrum_events, ttl=1) - def get_contribution_contract_contributor_map_uncached(): - _, contribution_contract_events = get_arbitrum_events_cached() - contracts = app.db_reader.get_contribution_contracts() - for address, events in contribution_contract_events.items(): - contracts[address].events.extend(events) - for contributor in contracts[address].contributors: - app.contribution_contract_map.setdefault(contributor.address, []) - app.contribution_contract_map[contributor.address].append(contracts[address]) - return app.contribution_contract_map - - def get_contribution_contract_map_cached(): - return app.cache.get("contribution_contracts", getter=get_contribution_contract_contributor_map_uncached, ttl=1) def get_contribution_contracts_for_address_uncached(address: str): - return get_contribution_contract_map_cached().get(address, []) + return [] def get_contribution_contracts_for_address_cached(address: str): - return app.cache.get(f"contribution_contracts-{address}", getter=get_contribution_contracts_for_address_uncached, getter_args=address, ttl=1) + return app.cache.get(f"contribution_contracts-{address}", getter=get_related_contribution_contracts_for_eth_address_uncached, getter_args=address, ttl=1) def get_vesting_contracts_cached(): - return app.cache.get("vesting_contracts", getter=app.db_reader.get_vesting_contracts, ttl=120) + return app.cache.get("vesting_contracts", getter=app.db_reader.get_vesting_contracts, ttl=4) def get_vesting_contracts_for_beneficiary_cached(beneficiary: str): contracts = [] @@ -314,31 +304,42 @@ def get_contract_addresses_core(): ttl=config.stale_time_seconds_contract_abis)} ) + + def get_contribution_contracts_uncached(): + contracts = app.db_reader.get_contribution_contracts() + addresses = contracts.keys() + contract_events = app.db_reader.get_arbitrum_events_by_main_args(addresses) + for event in contract_events: + contracts[event.main_arg].events.append(event) + return contracts + + def get_contribution_contracts_cached(): - return app.cache.get("contracts", getter=app.db_reader.get_contribution_contracts, ttl=2) + return app.cache.get("contracts", getter=get_contribution_contracts_uncached, ttl=2) @app.route("/contract/contribution") def get_open_contract_details(): return json_res( - {"contracts": get_contribution_contracts_cached(), "added_bls_keys": get_nodes_bls_keys_cached()} + {"contracts": list(get_contribution_contracts_cached().values()), "added_bls_keys": get_nodes_bls_keys_cached()} ) def get_contribution_contracts_for_sn_pubkey_uncached(sn_pubkey: bytes): - cached_contracts = app.db_reader.get_contribution_contracts() + cached_contracts = get_contribution_contracts_cached().values() contracts = [contract for contract in cached_contracts if contract.service_node_pubkey == sn_pubkey] - return contracts + contracts.sort(key=lambda x: x.events[-1].block if len(x.events) > 0 else math.inf, reverse=True) + return contracts[0] if len(contracts) > 0 else None @app.route("/contract/contribution/") def get_contribution_contract_for_sn_pubkey_cached(sn_pubkey: bytes): key = sn_pubkey.hex() return json_res( - {"contracts": app.cache.get("contract-sn-{}".format(key), + {"contract": app.cache.get("contract-sn-{}".format(key), getter=get_contribution_contracts_for_sn_pubkey_uncached, getter_args=key, ttl=2)} ) def get_related_contribution_contracts_for_eth_address_uncached(eth_wal: str): - contracts = get_contribution_contracts_cached() + contracts = get_contribution_contracts_cached().values() related_contracts = [] for contract in contracts: @@ -527,17 +528,16 @@ def get_rewards_info_cached(): def get_rewards_info_for_address_cached(eth_wal: str): address = eth_format(eth_wal) rewards_info = get_rewards_info_cached() - return rewards_info.get(address, 0) + return rewards_info.get(address, RewardsInfo(address=address, amount=0, lifetime_liquidated_stakes=0, + lifetime_locked_stakes=0, lifetime_rewards=0, + lifetime_unlocked_stakes=0, locked_stakes=0, + timelocked_stakes=0)) def get_rewards_info_response(eth_wal: str): return json_res({"rewards": get_rewards_info_for_address_cached(eth_wal)}) def get_rewards_signature_response(eth_wal: str): try: - rewards = get_rewards_info_for_address_cached(eth_wal) - if rewards == 0: - return flask.abort(404, f"No rewards available for {eth_wal}") - return json_res( {"rewards": app.cache.get(f"rewards-sig-{eth_wal}", getter=get_rewards_signature_uncached, getter_args=eth_wal, diff --git a/src/staking/dataclasses.py b/src/staking/dataclasses.py index 1de25ad..f63ca27 100644 --- a/src/staking/dataclasses.py +++ b/src/staking/dataclasses.py @@ -114,6 +114,7 @@ class DBNetworkInfo: node_count: int pulse_target_timestamp: int staking_requirement: int + total_staked: int version: str def __post_init__(self): @@ -128,15 +129,11 @@ def __post_init__(self): @dataclass class DBContributionContract: address: str - created_timestamp: int fee: int - last_added_timestamp: int manual_finalize: bool - node_add_timestamp: int operator_address: str pubkey_bls: str service_node_pubkey: str - service_node_signature: str status: int # Not in db but added after select contributors: list | None @@ -187,8 +184,30 @@ class ArbitrumInfo: @dataclass class RewardsInfo: + # Address of the wallet this object is for. address: str - rewards: int + # Total amount of claimable tokens for the given address. This includes the earnt rewards as well as unlocked + # stakes that are available to be claimed. + amount: int + # Total amount of tokens in the lifetime of the network that have been liquidated from the stakes for this address. + lifetime_liquidated_stakes: int + # Total amount of tokens in the lifetime of the network that has been staked into nodes for this address. + lifetime_locked_stakes: int + # Total amount of tokens in the lifetime of the network that has been earnt from staking into nodes for this address. + lifetime_rewards: int + # Total amount of tokens in the lifetime of the network that has been unlocked from the nodes this address has + # staked into. + lifetime_unlocked_stakes: int + # Amount of tokens currently locked into nodes on the network. This is `lifetime locked - lifetime unlocked`. + locked_stakes: int + # Amount of tokens that have been unstaked from nodes but cannot be claimed until the time lock on those + # individual stakes have been unlocked. + timelocked_stakes: int + # Amount of tokens that have been claimed from the nodes this address has staked into. + claimed_stakes: int = 0 + # Amount of tokens that have been claimed from the rewards that have been earned from the nodes this address has + # staked into. + claimed_rewards: int = 0 @dataclass class Registration: diff --git a/src/staking/fetcher.py b/src/staking/fetcher.py index 7660e28..9da0b1e 100644 --- a/src/staking/fetcher.py +++ b/src/staking/fetcher.py @@ -3,6 +3,8 @@ import subprocess import time +import eth_utils + from ..config_validate import validate_log_config, validate_contract_addresses, validate_web3_client, validate_oxen_rpc from ..staking.arbitrum import ( get_new_contribution_contracts, @@ -21,7 +23,7 @@ from ..oxen.rpc import ServiceNode, OxenRPC, NetworkInfo from ..util import format_seconds from ..log.time_keeper import TimeKeeper -from ..util.parse import parse_bls_pubkey +from ..util.parse import parse_bls_pubkey, eth_format from ..web3client.abi_manager import ABIManager from ..web3client.client import Web3Client from ..web3client.contracts.reward_rate_pool import RewardRatePoolInterface @@ -187,6 +189,8 @@ def run(self): t1_event_loop_exception_count = 0 t2_event_loop_exception_count = 0 try: + network = self.rpc.get_network_info_from_network() + self.update_network_details_and_nodes(network) while True: try: self.log.perf.start("loop") @@ -292,13 +296,13 @@ def update_network_details_and_nodes( network: NetworkInfo, ): self.log.info("Update service node list task start") - parsed_nodes, contributor_stake_map, current_height, node_count, active_node_count = self.fetch_service_node_list() + parsed_nodes, contributor_stake_map, current_height, node_count, total_staked, active_node_count = self.fetch_service_node_list() self.db_writer.write_nodes_to_staging_db( current_height, parsed_nodes, contributor_stake_map ) - self.db_writer.write_network_info_to_db(network=network, node_count=node_count, + self.db_writer.write_network_info_to_db(network=network, node_count=node_count, total_staked=total_staked, active_node_count=active_node_count) rewards_info = self.get_rewards_info() @@ -312,6 +316,7 @@ def fetch_service_node_list(self): parsed_nodes = [] contributions = [] active_node_count = 0 + total_staked = 0 try: res = self.rpc.get_service_nodes().get() @@ -379,6 +384,9 @@ def fetch_service_node_list(self): else None ) + node_staked = node.get("total_contributed", 0) + total_staked += node_staked + assert_all_dict_values_are_within_sqlite_integer_range(node) for contributor in node.get("contributors", []): @@ -416,7 +424,7 @@ def fetch_service_node_list(self): self.log.exception(e) finally: self.log.perf.end("update_service_node_list") - return parsed_nodes, contributions, current_height, len(parsed_nodes), active_node_count + return parsed_nodes, contributions, current_height, len(parsed_nodes), total_staked, active_node_count def update_exit_list(self): self.log.perf.start("update_exit_list") @@ -455,24 +463,43 @@ def get_rewards_info(self): self.log.debug("Update rewards details task start") rewards_info = [] try: - # Get the accrued rewards values for each wallet accrued_rewards_json = self.rpc.get_accrued_rewards().get() assert accrued_rewards_json is not None, "Accrued rewards request failed" assert accrued_rewards_json["status"] == "OK", "Accrued rewards request failed {}".format( accrued_rewards_json) + assert "balances" in accrued_rewards_json, "Accrued rewards request failed, 'balances' key was missing: {}".format( accrued_rewards_json) # Populate (Binary ETH wallet address -> accrued_rewards) table - for address_hex, rewards in accrued_rewards_json.get("balances").items(): + for balance in accrued_rewards_json.get("balances", []): # Ignore non-ethereum addresses (e.g. left oxen rewards, not relevant) - address = address_hex if address_hex.startswith("0x") else "0x" + address_hex - if len(address) != 42: + address = balance.get("address") + if address is None or not eth_utils.is_address(address): self.log.warning("Invalid address {}".format(address)) continue - rewards_info.append(RewardsInfo(address, rewards)) + address = eth_format(address) + + amount = balance.get("amount") + lifetime_liquidated_stakes = balance.get("lifetime_liquidated_stakes") + lifetime_locked_stakes = balance.get("lifetime_locked_stakes") + lifetime_rewards = balance.get("lifetime_rewards") + lifetime_unlocked_stakes = balance.get("lifetime_unlocked_stakes") + locked_stakes = balance.get("locked_stakes") + timelocked_stakes = balance.get("timelocked_stakes") + + rewards_info.append(RewardsInfo( + address=address, + amount=amount, + lifetime_liquidated_stakes=lifetime_liquidated_stakes, + lifetime_locked_stakes=lifetime_locked_stakes, + lifetime_rewards=lifetime_rewards, + lifetime_unlocked_stakes=lifetime_unlocked_stakes, + locked_stakes=locked_stakes, + timelocked_stakes=timelocked_stakes, + )) except Exception as e: self.log.error("Error fetching and parsing rewards details") diff --git a/src/staking/read.py b/src/staking/read.py index 2b45113..5ffc2ff 100644 --- a/src/staking/read.py +++ b/src/staking/read.py @@ -3,18 +3,21 @@ from ..db.read import DBReader from .dataclasses import DBNode, DBContributionMain, DBNetworkInfo, DBContributionContract, \ - DBContributionContractContribution, SmartContractABI, ArbitrumInfo, VestingContract + DBContributionContractContribution, SmartContractABI, ArbitrumInfo, VestingContract, RewardsInfo +from ..db.util import sql_connect_in_read_mode +from ..log import Log from ..util.parse import eth_format from ..web3client.event_scanner import ProcessedEvent -class DBReaderStaking(DBReader): - def __init__(self, db_path: str, log_level: int, perf: bool = False, disable_db_file_rewrite: bool = False): - super().__init__(db_path, log_level, perf, disable_db_file_rewrite) +class DBReaderStaking: + def __init__(self, db_path: str, log_level: int, perf: bool = False): + self.log = Log("db_reader", log_level, enable_perf=perf).logger + self.db_path = db_path def get_last_fetched_network_block_height(self) -> int: self.log.perf.start("get_last_fetched_network_block_height") - with closing(self.connect()) as connection: + with closing(sql_connect_in_read_mode(self.db_path)) as connection: with closing(connection.cursor()) as cursor: cursor.execute("SELECT MAX(fetched_block_height) FROM service_nodes_staging") (fetched_block_height,) = cursor.fetchone() @@ -26,7 +29,7 @@ def get_last_fetched_network_block_height(self) -> int: def get_last_commited_network_block_height(self) -> int: self.log.perf.start("get_last_commited_network_block_height") - with closing(self.connect()) as connection: + with closing(sql_connect_in_read_mode(self.db_path)) as connection: with closing(connection.cursor()) as cursor: cursor.execute("SELECT MAX(fetched_block_height) FROM service_nodes_main") (commited_block_height,) = cursor.fetchone() @@ -40,7 +43,7 @@ def get_last_commited_network_block_height(self) -> int: def get_network_info(self): self.log.perf.start("get_network_info") - with closing(sqlite3.connect(self.db_path, uri=True)) as connection: + with closing(sql_connect_in_read_mode(self.db_path)) as connection: with closing(connection.cursor()) as cursor: cursor.execute("SELECT * FROM network_info LIMIT 1") network_info = DBNetworkInfo(*cursor.fetchone()) @@ -51,7 +54,7 @@ def get_network_info(self): def get_last_fetched_arbitrum_event_block_height(self) -> int: self.log.perf.start("get_last_fetched_arbitrum_event_block_height") - with closing(self.connect()) as connection: + with closing(sql_connect_in_read_mode(self.db_path)) as connection: with closing(connection.cursor()) as cursor: cursor.execute("SELECT MAX(block) FROM arbitrum_events") (fetched_block_height,) = cursor.fetchone() @@ -65,7 +68,7 @@ def get_last_fetched_arbitrum_event_block_height(self) -> int: def get_contribution_contract_contributors(self, address:str): self.log.perf.start("get_contribution_contract_contributors") - with closing(self.connect()) as connection: + with closing(sql_connect_in_read_mode(self.db_path)) as connection: with closing(connection.cursor()) as cursor: cursor.execute("""SELECT * FROM contribution_contracts_contributions WHERE contract_address = ?""", (address,)) contributors = [DBContributionContractContribution(*contribution) for contribution in cursor.fetchall()] @@ -75,7 +78,7 @@ def get_contribution_contract_contributors(self, address:str): def get_contribution_contracts(self): self.log.perf.start("get_contribution_contracts") - with closing(self.connect()) as connection: + with closing(sql_connect_in_read_mode(self.db_path)) as connection: with closing(connection.cursor()) as cursor: cursor.execute("""SELECT * FROM contribution_contracts""") contracts = cursor.fetchall() @@ -101,9 +104,38 @@ def get_contribution_contracts(self): self.log.perf.end("get_contribution_contracts") return parsed_contracts + def get_contribution_contracts_non_finalized(self): + self.log.perf.start("get_contribution_contracts") + with closing(sql_connect_in_read_mode(self.db_path)) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute("""SELECT * FROM contribution_contracts""") + contracts = cursor.fetchall() + + parsed_contracts = {} + for contract in contracts: + contract_dict = DBContributionContract(*contract, contributors=[], events=[]) + parsed_contracts[contract_dict.address] = contract_dict + + cursor.execute( + """ + SELECT * FROM contribution_contracts_contributions where status < 3 + """ + ) + contributions = cursor.fetchall() + for contribution in contributions: + contribution_dict = DBContributionContractContribution(*contribution) + parsed_contracts[contribution_dict.contract_address].contributors.append( + contribution_dict + ) + + self.log.debug("Parsed contribution contracts: {}".format(len(parsed_contracts))) + self.log.perf.end("get_contribution_contracts") + return parsed_contracts + + def get_contribution_contract_addresses(self): self.log.perf.start("get_contribution_contracts") - with closing(self.connect()) as connection: + with closing(sql_connect_in_read_mode(self.db_path)) as connection: with closing(connection.cursor()) as cursor: cursor.execute( """ @@ -117,7 +149,7 @@ def get_contribution_contract_addresses(self): def get_nodes(self): self.log.perf.start("get_nodes") - with closing(self.connect()) as connection: + with closing(sql_connect_in_read_mode(self.db_path)) as connection: with closing(connection.cursor()) as cursor: parsed_nodes = {} @@ -189,7 +221,7 @@ def get_nodes(self): def get_contribution_addresses(self): self.log.perf.start("get_contribution_addresses") - with closing(self.connect()) as connection: + with closing(sql_connect_in_read_mode(self.db_path)) as connection: with closing(connection.cursor()) as cursor: addresses = set() cursor.execute("""SELECT address, beneficiary from service_nodes_contributions_main""") @@ -206,20 +238,28 @@ def get_contribution_addresses(self): def get_rewards_info(self): self.log.perf.start("get_rewards_info") - with closing(self.connect()) as connection: + with closing(sql_connect_in_read_mode(self.db_path)) as connection: with closing(connection.cursor()) as cursor: cursor.execute("SELECT * FROM rewards_info") - rewards_info = { - eth_format(address_hex): rewards - for address_hex, rewards in cursor.fetchall() - } + rewards = [RewardsInfo(*info) for info in cursor.fetchall()] + rewards_info = {info.address: info for info in rewards} self.log.debug("Rewards info: {}".format(len(rewards_info))) self.log.perf.end("get_rewards_info") return rewards_info + def get_rewards_info_for_address(self, address: str): + self.log.perf.start("get_rewards_info_for_address") + with closing(sql_connect_in_read_mode(self.db_path)) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute("SELECT * FROM rewards_info WHERE address = ?", (address,)) + info = RewardsInfo(*cursor.fetchone()) + self.log.debug("Rewards info: {}".format(info)) + self.log.perf.end("get_rewards_info_for_address") + return info + def get_smart_contract_abis(self): self.log.perf.start("get_smart_contract_abis") - with closing(self.connect()) as connection: + with closing(sql_connect_in_read_mode(self.db_path)) as connection: with closing(connection.cursor()) as cursor: cursor.execute( """ @@ -233,7 +273,7 @@ def get_smart_contract_abis(self): def get_smart_contract_abi(self, name: str): self.log.perf.start("get_smart_contract_abi") - with closing(self.connect()) as connection: + with closing(sql_connect_in_read_mode(self.db_path)) as connection: with closing(connection.cursor()) as cursor: cursor.execute( """ @@ -248,7 +288,7 @@ def get_smart_contract_abi(self, name: str): def get_smart_contract_names(self) -> list[str]: self.log.perf.start("get_smart_contract_names") - with closing(self.connect()) as connection: + with closing(sql_connect_in_read_mode(self.db_path)) as connection: with closing(connection.cursor()) as cursor: cursor.execute( """ @@ -262,7 +302,7 @@ def get_smart_contract_names(self) -> list[str]: def get_smart_contract_addresses(self): self.log.perf.start("get_smart_contract_addresses") - with closing(self.connect()) as connection: + with closing(sql_connect_in_read_mode(self.db_path)) as connection: with closing(connection.cursor()) as cursor: cursor.execute( """ @@ -278,7 +318,7 @@ def get_smart_contract_addresses(self): def get_smart_contract_addresses_core(self): self.log.perf.start("get_smart_contract_addresses_core") - with closing(self.connect()) as connection: + with closing(sql_connect_in_read_mode(self.db_path)) as connection: with closing(connection.cursor()) as cursor: cursor.execute( """ @@ -294,7 +334,7 @@ def get_smart_contract_addresses_core(self): def get_smart_contract_address(self, name: str): self.log.perf.start("get_smart_contract_address") - with closing(self.connect()) as connection: + with closing(sql_connect_in_read_mode(self.db_path)) as connection: with closing(connection.cursor()) as cursor: cursor.execute( """ @@ -310,7 +350,7 @@ def get_smart_contract_address(self, name: str): def get_arbitrum_events(self, from_block = 0, names: list = None): self.log.perf.start("get_arbitrum_events") assert from_block >= 0, "from_block must be >= 0" - with closing(self.connect()) as connection: + with closing(sql_connect_in_read_mode(self.db_path)) as connection: with closing(connection.cursor()) as cursor: self.log.debug(f"Getting events from block {from_block} with names {names}") if names is None: @@ -334,7 +374,7 @@ def get_arbitrum_events(self, from_block = 0, names: list = None): def get_arbitrum_events_by_name(self, name: str, from_block = 0): self.log.perf.start("get_arbitrum_events_by_name") assert from_block >= 0, "from_block must be >= 0" - with closing(self.connect()) as connection: + with closing(sql_connect_in_read_mode(self.db_path)) as connection: with closing(connection.cursor()) as cursor: cursor.execute( """ @@ -347,10 +387,26 @@ def get_arbitrum_events_by_name(self, name: str, from_block = 0): self.log.perf.end("get_arbitrum_events_by_name") return events + def get_arbitrum_events_by_main_args(self, main_args: list[str]): + self.log.perf.start("get_arbitrum_events_by_main_arg") + with closing(sql_connect_in_read_mode(self.db_path)) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute( + """ + SELECT * FROM arbitrum_events WHERE main_arg IN ({}) + """.format(",".join(["?"] * len(main_args))), + (tuple(main_args)), + ) + events = [ProcessedEvent(*event) for event in cursor.fetchall()] + self.log.debug("Arbitrum events: {}".format(len(events))) + self.log.perf.end("get_arbitrum_events_by_main_arg") + return events + + def get_arbitrum_event_main_args_by_name(self, name: str, from_block = 0): self.log.perf.start("get_arbitrum_event_main_args_by_name") assert from_block >= 0, "from_block must be >= 0" - with closing(self.connect()) as connection: + with closing(sql_connect_in_read_mode(self.db_path)) as connection: with closing(connection.cursor()) as cursor: cursor.execute( """ @@ -369,7 +425,7 @@ def get_arbitrum_events_page(self, args=None): if args is None: args = [1000, 0] self.log.perf.start("get_arbitrum_events") - with closing(self.connect()) as connection: + with closing(sql_connect_in_read_mode(self.db_path)) as connection: with closing(connection.cursor()) as cursor: limit = args[0] if len(args) > 0 else 1000 skip = args[1] if len(args) > 1 else 0 @@ -391,7 +447,7 @@ def get_arbitrum_events_page(self, args=None): def get_arbitrum_info(self): self.log.perf.start("get_arbitrum_info") - with closing(self.connect()) as connection: + with closing(sql_connect_in_read_mode(self.db_path)) as connection: with closing(connection.cursor()) as cursor: cursor.execute("SELECT * FROM arbitrum_info ORDER BY block DESC LIMIT 1") info = ArbitrumInfo(*cursor.fetchone()) @@ -402,7 +458,7 @@ def get_arbitrum_info(self): def get_arbitrum_events_for_stake_contrat_id(self, contract_id: int): self.log.perf.start("get_events_for_stake_contrat_id") - with closing(self.connect()) as connection: + with closing(sql_connect_in_read_mode(self.db_path)) as connection: with closing(connection.cursor()) as cursor: cursor.execute( """ @@ -417,7 +473,7 @@ def get_arbitrum_events_for_stake_contrat_id(self, contract_id: int): def get_vesting_contracts(self) -> list[VestingContract]: self.log.perf.start("get_vesting_contracts") - with closing(self.connect()) as connection: + with closing(sql_connect_in_read_mode(self.db_path)) as connection: with closing(connection.cursor()) as cursor: cursor.execute( """ @@ -431,7 +487,7 @@ def get_vesting_contracts(self) -> list[VestingContract]: def has_vesting_contracts(self) -> bool: self.log.perf.start("has_vesting_contracts") - with closing(self.connect()) as connection: + with closing(sql_connect_in_read_mode(self.db_path)) as connection: with closing(connection.cursor()) as cursor: cursor.execute( """ diff --git a/src/staking/schema.sql b/src/staking/schema.sql index cb757f3..cb6662a 100644 --- a/src/staking/schema.sql +++ b/src/staking/schema.sql @@ -140,6 +140,7 @@ CREATE TABLE network_info ( node_count INTEGER NOT NULL, pulse_target_timestamp INTEGER NOT NULL, staking_requirement INTEGER NOT NULL, + total_staked INTEGER NOT NULL, version TEXT NOT NULL ); @@ -147,7 +148,15 @@ CREATE INDEX network_info_block_height_idx ON network_info(block_height DESC); CREATE TABLE rewards_info ( address BLOB NOT NULL PRIMARY KEY, - rewards INTEGER NOT NULL + amount INTEGER NOT NULL, + lifetime_liquidated_stakes INTEGER NOT NULL, + lifetime_locked_stakes INTEGER NOT NULL, + lifetime_rewards INTEGER NOT NULL, + lifetime_unlocked_stakes INTEGER NOT NULL, + locked_stakes INTEGER NOT NULL, + timelocked_stakes INTEGER NOT NULL, + claimed_stakes INTEGER NOT NULL DEFAULT 0, + claimed_rewards INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE arbitrum_info ( diff --git a/src/staking/write.py b/src/staking/write.py index bb8a4d7..20ef0bd 100644 --- a/src/staking/write.py +++ b/src/staking/write.py @@ -4,7 +4,8 @@ from web3 import Web3 -from ..db.write import DBWriter +from ..db.util import sql_connect_in_write_mode +from ..log import Log from ..staking.arbitrum import ContributionContractDetails from ..staking.dataclasses import RewardsInfo, DBNodeExit, VestingContract from ..oxen.rpc import ServiceNode, NetworkInfo @@ -12,9 +13,10 @@ from ..web3client.event_scanner import ProcessedEvent -class DBWriterStaking(DBWriter): +class DBWriterStaking: def __init__(self, db_path: str, log_level: int, perf: bool = False): - super().__init__(db_path, log_level, perf) + self.log = Log("db_writer", log_level, enable_perf=perf).logger + self.db_path = db_path self.defer_writing_arbitrum_events = False self.deferred_arbitrum_events = [] @@ -27,7 +29,7 @@ def write_nodes_to_staging_db( ): self.log.perf.start("write_to_db") - with closing(self.connect()) as connection: + with closing(sql_connect_in_write_mode(self.db_path)) as connection: connection.execute("BEGIN") with closing(connection.cursor()) as cursor: self.log.debug("Inserting {} service nodes".format(len(parsed_nodes))) @@ -160,7 +162,7 @@ def write_nodes_to_main_db(self, immutable_height: int): those nodes from the staging db. """ self.log.perf.start("write_nodes_to_main_db") - with closing(self.connect()) as connection: + with closing(sql_connect_in_write_mode(self.db_path)) as connection: connection.execute("BEGIN") with closing(connection.cursor()) as cursor: self.log.perf.start("write_nodes_to_main_db -> select nodes") @@ -319,7 +321,7 @@ def write_nodes_to_main_db(self, immutable_height: int): def write_exit_list_to_db(self, exit_list: list[DBNodeExit]): self.log.perf.start("write_exit_list_to_db") - with closing(self.connect()) as connection: + with closing(sql_connect_in_write_mode(self.db_path)) as connection: connection.execute("BEGIN") with closing(connection.cursor()) as cursor: self.log.debug("Updating nodes in main with {} exit events".format(len(exit_list))) @@ -356,10 +358,11 @@ def write_network_info_to_db( self, network: NetworkInfo, node_count: int, + total_staked: int, active_node_count: int, ): self.log.perf.start("write_network_info_to_db") - with closing(self.connect()) as connection: + with closing(sql_connect_in_write_mode(self.db_path)) as connection: with closing(connection.cursor()) as cursor: cursor.execute( """ @@ -378,9 +381,10 @@ def write_network_info_to_db( nettype, pulse_target_timestamp, staking_requirement, + total_staked, version ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( 1, @@ -397,6 +401,7 @@ def write_network_info_to_db( network.nettype, network.pulse_target_timestamp, network.staking_requirement, + total_staked, network.version, ), ) @@ -405,7 +410,7 @@ def write_network_info_to_db( def write_rewards_info_to_db(self, rewards_info: list[RewardsInfo]): self.log.perf.start("write_rewards_info_to_db") - with closing(self.connect()) as connection: + with closing(sql_connect_in_write_mode(self.db_path)) as connection: connection.execute("BEGIN") with closing(connection.cursor()) as cursor: self.log.debug("Inserting {} rewards info".format(len(rewards_info))) @@ -413,13 +418,36 @@ def write_rewards_info_to_db(self, rewards_info: list[RewardsInfo]): cursor.executemany( """ - INSERT OR REPLACE INTO rewards_info (address, rewards) - VALUES (?, ?) + INSERT INTO rewards_info ( + address, + amount, + lifetime_liquidated_stakes, + lifetime_locked_stakes, + lifetime_rewards, + lifetime_unlocked_stakes, + locked_stakes, + timelocked_stakes + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(address) DO UPDATE SET + amount = excluded.amount, + lifetime_liquidated_stakes = excluded.lifetime_liquidated_stakes, + lifetime_locked_stakes = excluded.lifetime_locked_stakes, + lifetime_rewards = excluded.lifetime_rewards, + lifetime_unlocked_stakes = excluded.lifetime_unlocked_stakes, + locked_stakes = excluded.locked_stakes, + timelocked_stakes = excluded.timelocked_stakes; """, ( ( info.address, - info.rewards, + info.amount, + info.lifetime_liquidated_stakes, + info.lifetime_locked_stakes, + info.lifetime_rewards, + info.lifetime_unlocked_stakes, + info.locked_stakes, + info.timelocked_stakes ) for info in rewards_info ), @@ -435,13 +463,54 @@ def write_rewards_info_to_db(self, rewards_info: list[RewardsInfo]): connection.commit() self.log.perf.end("write_rewards_info_to_db") + def write_update_rewards_claim_amounts(self, address: str, claimed_stakes: int, claimed_rewards: int): + self.log.perf.start("write_update_rewards_claim_amounts") + with closing(sql_connect_in_write_mode(self.db_path)) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + self.log.debug(f"Updating rewards claim amounts for {address}") + self.log.perf.start("write_update_rewards_claim_amounts -> update rewards claim amounts") + cursor.execute( + """ + UPDATE rewards_info SET claimed_stakes = ?, claimed_rewards = ? WHERE address = ? + """, + (claimed_stakes, claimed_rewards, address), + ) + updated_rows = cursor.rowcount + self.log.perf.end("write_update_rewards_claim_amounts -> update rewards claim amounts") + self.log.debug( + "Updated {} rows in rewards_info".format(updated_rows) + ) + connection.commit() + self.log.perf.end("write_update_rewards_claim_amounts") + + def write_reset_all_rewards_claim_amounts(self): + self.log.perf.start("write_reset_all_rewards_claim_amounts") + with closing(sql_connect_in_write_mode(self.db_path)) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + self.log.debug("Updating rewards claim amounts for all addresses") + self.log.perf.start("write_reset_all_rewards_claim_amounts -> update rewards claim amounts") + cursor.execute( + """ + UPDATE rewards_info SET claimed_stakes = 0, claimed_rewards = 0 + """ + ) + updated_rows = cursor.rowcount + self.log.perf.end("write_reset_all_rewards_claim_amounts -> update rewards claim amounts") + self.log.debug( + "Updated {} rows in rewards_info".format(updated_rows) + ) + connection.commit() + self.log.perf.end("write_reset_all_rewards_claim_amounts") + def write_arbitrum_event_to_db(self, event: ProcessedEvent): if self.defer_writing_arbitrum_events: self.log.debug(f"Deferring arbitrum event write: {event}") self.deferred_arbitrum_events.append(event) return self.log.perf.start("write_arbitrum_event_to_db") - with closing(self.connect()) as connection: + with closing(sql_connect_in_write_mode(self.db_path)) as connection: connection.execute("BEGIN") with closing(connection.cursor()) as cursor: self.log.debug("Inserting event into arbitrum_events") @@ -482,6 +551,7 @@ def write_deferred_arbitrum_events_to_db(self): return events, self.deferred_arbitrum_events = self.deferred_arbitrum_events, [] + events.sort(key=lambda x: (x.block, x.log_index)) try: self.log.info(f"Writing {len(events)} deferred arbitrum events to db") @@ -494,7 +564,7 @@ def write_deferred_arbitrum_events_to_db(self): def write_arbitrum_events_to_db(self, events: list[ProcessedEvent]): self.log.perf.start("write_arbitrum_events_to_db") - with closing(self.connect()) as connection: + with closing(sql_connect_in_write_mode(self.db_path)) as connection: connection.execute("BEGIN") with closing(connection.cursor()) as cursor: self.log.debug("Inserting {} events into arbitrum_events".format(len(events))) @@ -537,26 +607,27 @@ def write_arbitrum_events_to_db(self, events: list[ProcessedEvent]): connection.commit() self.log.perf.end("write_arbitrum_events_to_db") - def write_new_contribution_contract(self, address: str, operator_address: str): + def write_new_contribution_contract(self, address: str, operator_address: str, service_node_pubkey: str): self.log.perf.start("write_new_contribution_contract") - with closing(self.connect()) as connection: + with closing(sql_connect_in_write_mode(self.db_path)) as connection: connection.execute("BEGIN") with closing(connection.cursor()) as cursor: self.log.debug(f"Inserting new contribution contract") cursor.execute(""" INSERT OR REPLACE INTO contribution_contracts ( address, - operator_address + operator_address, + service_node_pubkey ) - VALUES (?, ?) - """, (address, operator_address)) + VALUES (?, ?, ?) + """, (address, operator_address, service_node_pubkey)) connection.commit() self.log.perf.end("write_new_contribution_contract") def write_update_contribution_contract_status(self, address: str, status: int): self.log.perf.start("write_update_contribution_contract_status") - with closing(self.connect()) as connection: + with closing(sql_connect_in_write_mode(self.db_path)) as connection: connection.execute("BEGIN") with closing(connection.cursor()) as cursor: self.log.debug(f"Updating contribution contract status to {status}") @@ -572,7 +643,7 @@ def write_update_contribution_contract_status(self, address: str, status: int): def write_update_contribution_contract_manual_finalize(self, address: str, manual_finalize: bool): self.log.perf.start("write_update_contribution_contract_manual_finalize") - with closing(self.connect()) as connection: + with closing(sql_connect_in_write_mode(self.db_path)) as connection: connection.execute("BEGIN") with closing(connection.cursor()) as cursor: self.log.debug(f"Updating contribution contract manual_finalize to {manual_finalize}") @@ -588,7 +659,7 @@ def write_update_contribution_contract_manual_finalize(self, address: str, manua def write_update_contribution_contract_fee(self, address: str, fee: int): self.log.perf.start("write_update_contribution_contract_fee") - with closing(self.connect()) as connection: + with closing(sql_connect_in_write_mode(self.db_path)) as connection: connection.execute("BEGIN") with closing(connection.cursor()) as cursor: self.log.debug(f"Updating contribution contract fee to {fee}") @@ -604,15 +675,19 @@ def write_update_contribution_contract_fee(self, address: str, fee: int): def write_update_contribution_contract_pubkeys(self, address: str, pubkey_bls: str, service_node_pubkey: str): self.log.perf.start("write_update_contribution_contract_pubkeys") - with closing(self.connect()) as connection: + with closing(sql_connect_in_write_mode(self.db_path)) as connection: connection.execute("BEGIN") with closing(connection.cursor()) as cursor: self.log.debug(f"Updating contribution contract pubkeys") cursor.execute( """ - INSERT OR UPDATE contribution_contracts SET pubkey_bls = ?, service_node_pubkey = ? WHERE address = ? + INSERT INTO contribution_contracts (address, pubkey_bls, service_node_pubkey) + VALUES (?, ?, ?) + ON CONFLICT(address) DO UPDATE SET + pubkey_bls = excluded.pubkey_bls, + service_node_pubkey = excluded.service_node_pubkey; """, - (pubkey_bls, service_node_pubkey, address), + (address, pubkey_bls, service_node_pubkey), ) connection.commit() @@ -620,7 +695,7 @@ def write_update_contribution_contract_pubkeys(self, address: str, pubkey_bls: s def write_update_contribution_contract_contributor(self, contract_address: str, contributor): self.log.perf.start("write_update_contribution_contract_contributor") - with closing(self.connect()) as connection: + with closing(sql_connect_in_write_mode(self.db_path)) as connection: connection.execute("BEGIN") with closing(connection.cursor()) as cursor: self.log.debug(f"Updating contribution contract contributor") @@ -638,7 +713,7 @@ def write_update_contribution_contract_contributor(self, contract_address: str, ( contributor.address, contributor.amount, - contributor.beneficiary_address, + contributor.beneficiary, contract_address, contributor.reserved ), @@ -646,9 +721,9 @@ def write_update_contribution_contract_contributor(self, contract_address: str, connection.commit() - def write_delete_contribution_contract_contributor(self, contract_address: str, contributor): + def write_delete_contribution_contract_contributor(self, contract_address: str, contributor_address: str): self.log.perf.start("write_delete_contribution_contract_contributor") - with closing(self.connect()) as connection: + with closing(sql_connect_in_write_mode(self.db_path)) as connection: connection.execute("BEGIN") with closing(connection.cursor()) as cursor: self.log.debug(f"Deleting contribution contract contributor") @@ -656,18 +731,33 @@ def write_delete_contribution_contract_contributor(self, contract_address: str, """ DELETE FROM contribution_contracts_contributions WHERE address = ? AND contract_address = ? """, - (contributor.address, contract_address), + (contributor_address, contract_address), ) connection.commit() self.log.perf.end("write_delete_contribution_contract_contributor") + def write_delete_all_contribution_contract_contributors(self, contract_address: str): + self.log.perf.start("write_delete_all_contribution_contract_contributors") + with closing(sql_connect_in_write_mode(self.db_path)) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + self.log.debug(f"Deleting all contribution contract contributors") + cursor.execute( + """ + DELETE FROM contribution_contracts_contributions WHERE contract_address = ? + """, + (contract_address,), + ) + connection.commit() + self.log.perf.end("write_delete_all_contribution_contract_contributors") + def write_contribution_contracts_to_db( self, contracts: list[ContributionContractDetails], contributions_list: list ): self.log.perf.start("write_contribution_contracts_to_db") - with closing(self.connect()) as connection: + with closing(sql_connect_in_write_mode(self.db_path)) as connection: connection.execute("BEGIN") with closing(connection.cursor()) as cursor: self.log.debug("Inserting {} contribution contracts".format(len(contracts))) @@ -776,7 +866,7 @@ def write_contribution_contracts_to_db( def write_smart_contract_abis_to_db(self, abis: list[ABIData]): self.log.perf.start("write_smart_contract_abis_to_db") - with closing(self.connect()) as connection: + with closing(sql_connect_in_write_mode(self.db_path)) as connection: connection.execute("BEGIN") with closing(connection.cursor()) as cursor: self.log.debug("Inserting {} smart contract abis".format(len(abis))) @@ -818,7 +908,7 @@ def write_smart_contract_details_to_db( contracts, ): self.log.perf.start("write_smart_contract_details_to_db") - with closing(self.connect()) as connection: + with closing(sql_connect_in_write_mode(self.db_path)) as connection: connection.execute("BEGIN") with closing(connection.cursor()) as cursor: self.log.debug("Inserting {} smart contract details".format(len(contracts))) @@ -853,7 +943,7 @@ def write_smart_contract_details_to_db( def write_arbitrum_info_to_db(self, current_block, service_node_rewards_balance, reward_rate_pool_balance): self.log.perf.start("write_arbitrum_info_to_db") - with closing(self.connect()) as connection: + with closing(sql_connect_in_write_mode(self.db_path)) as connection: connection.execute("BEGIN") with closing(connection.cursor()) as cursor: self.log.debug( @@ -877,7 +967,7 @@ def write_arbitrum_info_to_db(self, current_block, service_node_rewards_balance, def write_vesting_contracts(self, vesting_contracts: list[VestingContract]): self.log.perf.start("write_vesting_contracts") - with closing(self.connect()) as connection: + with closing(sql_connect_in_write_mode(self.db_path)) as connection: connection.execute("BEGIN") with closing(connection.cursor()) as cursor: self.log.debug("Inserting {} vesting contracts".format(len(vesting_contracts))) @@ -928,7 +1018,7 @@ def write_vesting_contracts(self, vesting_contracts: list[VestingContract]): def delete_all_vesting_contracts(self): self.log.perf.start("delete_all_vesting_contracts") - with closing(self.connect()) as connection: + with closing(sql_connect_in_write_mode(self.db_path)) as connection: connection.execute("BEGIN") with closing(connection.cursor()) as cursor: cursor.execute("DELETE FROM vesting_contracts") @@ -941,7 +1031,7 @@ def delete_all_vesting_contracts(self): def write_update_vesting_contract_beneficiary(self, address: str, beneficiary: str): self.log.perf.start("write_update_vesting_contract_beneficiary") - with closing(self.connect()) as connection: + with closing(sql_connect_in_write_mode(self.db_path)) as connection: connection.execute("BEGIN") with closing(connection.cursor()) as cursor: self.log.debug("Updating vesting contract {} beneficiary to {}".format(address, beneficiary)) @@ -966,7 +1056,7 @@ def write_update_vesting_contract_beneficiary(self, address: str, beneficiary: s def delete_all_events(self): self.log.perf.start("delete_all_events") - with closing(self.connect()) as connection: + with closing(sql_connect_in_write_mode(self.db_path)) as connection: connection.execute("BEGIN") with closing(connection.cursor()) as cursor: self.log.debug("Deleting all events from the db") @@ -984,7 +1074,7 @@ def delete_all_events(self): def delete_all_contrib_contracts_and_contributors(self): self.log.perf.start("delete_all_contrib_contracts_and_contributors") - with closing(self.connect()) as connection: + with closing(sql_connect_in_write_mode(self.db_path)) as connection: connection.execute("BEGIN") with closing(connection.cursor()) as cursor: cursor.execute("""Delete from contribution_contracts_contributions""") diff --git a/src/util/parse.py b/src/util/parse.py index c42eab3..4dc907a 100644 --- a/src/util/parse.py +++ b/src/util/parse.py @@ -12,13 +12,12 @@ eth_regex = "0x[0-9a-fA-F]{40}" - def parse_bls_pubkey(bls_pubkey: dict): x, y = bls_pubkey["X"], bls_pubkey["Y"] return f"{x:064x}{y:064x}" def parse_ed25519_pubkey(ed25519_pubkey: int): - return f"{ed25519_pubkey:032x}" + return f"{ed25519_pubkey:064x}" def raw_eth_addr(k, v): if re.fullmatch(eth_regex, v): @@ -34,15 +33,15 @@ def get_relative_time_from_ms(ms: int, short: bool = False, include_suffix = Fal prefix, suffix = "", "" if ms < 1000: - time = f"{ms} {"ms" if short else "milliseconds"}" + time = "{} {}".format(ms, ("ms" if short else "milliseconds")) elif ms < 1000 * 60: - time = f"{ms // 1000} {"s" if short else "seconds"}" + time = "{} {}".format(ms // 1000, ("s" if short else "seconds")) elif ms < 1000 * 60 * 60: - time = f"{ms // (1000 * 60)} {"m" if short else "minutes"}" + time = "{} {}".format(ms // (1000 * 60), ("m" if short else "minutes")) elif ms < 1000 * 60 * 60 * 24: - time = f"{ms // (1000 * 60 * 60)} {"h" if short else "hours"}" + time = "{} {}".format(ms // (1000 * 60 * 60), ("h" if short else "hours")) else: - time = f"{ms // (1000 * 60 * 60 * 24)} {"d" if short else "days"}" + time = "{} {}".format(ms // (1000 * 60 * 60 * 24), ("d" if short else "days")) return f"{prefix}{time}{suffix}" diff --git a/src/web3client/contracts_ws/service_node_contribution.py b/src/web3client/contracts_ws/service_node_contribution.py index e54d566..01462c0 100644 --- a/src/web3client/contracts_ws/service_node_contribution.py +++ b/src/web3client/contracts_ws/service_node_contribution.py @@ -19,7 +19,7 @@ class ContributionContractContributor: address: str = None amount: int = 0 - beneficiary_address: str = None + beneficiary: str = None reserved: int = 0 @@ -62,13 +62,14 @@ def update_pubkeys(self, new_bls_pubkey: dict, new_ed25519_pubkey: int): service_node_pubkey = parse_ed25519_pubkey(new_ed25519_pubkey) self.db_writer.write_update_contribution_contract_pubkeys(self.address, pubkey_bls, service_node_pubkey) - def _upsert_contributor(self, address: str): - self._contributors.setdefault(address, ContributionContractContributor(address)) + def _upsert_contributor(self, address: str, beneficiary: str | None): + self._contributors.setdefault(address, ContributionContractContributor(address=address, beneficiary=beneficiary)) - def update_contributor_new_contribution(self, address: str, amount: int): - self.log.debug(f"Updating contributor add {address} with amount {amount}") - self._upsert_contributor(address) + def update_contributor_new_contribution(self, address: str, beneficiary: str, amount: int): + self.log.debug(f"Updating contributor add {address} with beneficiary {beneficiary} and amount {amount}") + self._upsert_contributor(address, beneficiary) self._contributors[address].address = address + self._contributors[address].beneficiary = beneficiary self._contributors[address].amount += amount self.db_writer.write_update_contribution_contract_contributor(self.address, self._contributors[address]) @@ -84,10 +85,10 @@ def update_contributor_withdraw_contribution(self, address: str, amount: int): else: self.log.warning(f"No contributor found for address {address} to withdraw {amount}") - def update_contributor_beneficiary(self, address: str, beneficiary_address: str): - self.log.debug(f"Updating contributor beneficiary {address} to {beneficiary_address}") - self._upsert_contributor(address) - self._contributors[address].beneficiary_address = beneficiary_address + def update_contributor_beneficiary(self, address: str, beneficiary: str): + self.log.debug(f"Updating contributor beneficiary {address} to {beneficiary}") + self._upsert_contributor(address, beneficiary) + self._contributors[address].beneficiary = beneficiary self.db_writer.write_update_contribution_contract_contributor(self.address, self._contributors[address]) def update_reserved_contributors(self, reserved_contributors: list[dict[str, str]]): @@ -95,7 +96,7 @@ def update_reserved_contributors(self, reserved_contributors: list[dict[str, str for reserved_contributor in reserved_contributors: address = reserved_contributor.get("addr") reserved_amount = reserved_contributor.get("amount") - self._upsert_contributor(address) + self._upsert_contributor(address, None) self._contributors[address].reserved = reserved_amount self.db_writer.write_update_contribution_contract_contributor(self.address, self._contributors[address]) @@ -119,7 +120,7 @@ def process_event(self, raw_event: EventData): return self.update_status(3) case "NewContribution": - return self.update_contributor_new_contribution(event.args.get("contributor"), event.args.get("amount")) + return self.update_contributor_new_contribution(event.args.get("contributor"), event.args.get("beneficiary"), event.args.get("amount")) case "WithdrawContribution": return self.update_contributor_withdraw_contribution(event.args.get("contributor"), diff --git a/src/web3client/contracts_ws/service_node_rewards.py b/src/web3client/contracts_ws/service_node_rewards.py index 879367e..6eaf447 100644 --- a/src/web3client/contracts_ws/service_node_rewards.py +++ b/src/web3client/contracts_ws/service_node_rewards.py @@ -6,18 +6,52 @@ from web3.types import EventData from web3.utils.subscriptions import EthSubscriptionContext +from src.staking.read import DBReaderStaking from src.staking.write import DBWriterStaking from src.web3client.contracts_ws.contract_ws import ContractWS from src.web3client.contracts_ws.subscription import create_subscriptions from src.web3client.event_queue_manager import EventQueueManager +def handle_claim(event: EventData, db_writer: DBWriterStaking, db_reader: DBReaderStaking, log: logging): + address = event.args.recipientAddress + amount = event.args.amount + + reward_info = db_reader.get_rewards_info_for_address(address) + + assert reward_info is not None, f"Rewards info not found for address {address}" + + claimed_stakes = reward_info.claimed_stakes + claimed_rewards = reward_info.claimed_rewards + + remaining = amount + stakes_avail = reward_info.lifetime_unlocked_stakes - reward_info.lifetime_liquidated_stakes - claimed_stakes + if remaining > stakes_avail: + remaining -= stakes_avail + claimed_stakes += stakes_avail + else: + claimed_stakes += remaining + remaining = 0 + + rewards_avail = reward_info.lifetime_rewards - claimed_rewards + if remaining > rewards_avail: + remaining -= rewards_avail + claimed_rewards += rewards_avail + else: + claimed_rewards += remaining + remaining = 0 + + assert remaining == 0, f"Remaining rewards {remaining} is not equal to 0" + + db_writer.write_update_rewards_claim_amounts(address, claimed_stakes, claimed_rewards) + class ServiceNodeRewards(ContractWS): name = "ServiceNodeRewards" - def __init__(self, w3: AsyncWeb3, db_writer: DBWriterStaking, log: logging, + def __init__(self, w3: AsyncWeb3, db_writer: DBWriterStaking, db_reader: DBReaderStaking, log: logging, event_queue: EventQueueManager | None = None): super().__init__(self.name, w3, db_writer, log, event_queue) + self.db_reader = db_reader @staticmethod def get_main_arg(event: EventData): @@ -40,6 +74,10 @@ def get_main_arg(event: EventData): async def handle_event(self, event: EventData): main_arg = ServiceNodeRewards.get_main_arg(event) assert main_arg is not None + + if event.event == "RewardsClaimed": + handle_claim(event, self.db_writer, self.db_reader, self.log) + return await self._handle_event(event, main_arg=main_arg) async def handle_event_sub(self, event: EthSubscriptionContext): diff --git a/src/web3client/event_queue_manager.py b/src/web3client/event_queue_manager.py index 7a2c74f..602d2a1 100644 --- a/src/web3client/event_queue_manager.py +++ b/src/web3client/event_queue_manager.py @@ -59,24 +59,36 @@ async def process_event_queue(self, start_block: int | None = None): responses = [] for past_event in queue: from_block = past_event.start_block if past_event.start_block else start_block - self.log.debug( + self.log.info( f"Fetching past events for {past_event.event.event_name} from block {from_block} to block {block_current} ({block_current - from_block} blocks ~{get_relative_time_from_ms(get_time_of_arbitrum_blocks_ms(block_current - from_block))})") for recent in await past_event.event.get_logs(from_block=from_block, to_block=block_current): - responses.append(past_event.handler(recent)) + responses.append((recent, past_event.handler)) self.processed_events += 1 - await asyncio.gather(*responses) + # sort responses by block then log index + responses = sorted(responses, key=lambda x: (x[0].get("blockNumber"), x[0].get("logIndex"))) + + handlers = [] + for (data, handler) in responses: + handlers.append(handler(data)) + + await asyncio.gather(*handlers) else: self.log.debug("No past events to fetch") + return block_current + async def process_sub_queue(self): sub_queue, self.sub_queue = self.sub_queue, [] + return if len(sub_queue) > 0: await self.w3.subscription_manager.subscribe(sub_queue) self.processed_subs += len(sub_queue) self.log.info(f"Subscribed to {len(sub_queue)} subscriptions") + for sub in sub_queue: + self.log.info(f"Subscribed to {sub.label} for topics {sub.topics}") else: self.log.debug("No subscriptions to subscribe to") @@ -84,9 +96,10 @@ async def run(self): # The max depth ensures the loop won't get stuck in an infinite loop. This is just a safety measure as it should # not be possible due to the queue population dependencies. run_depth = 0 + last_scanned_block = 0 while run_depth <= self.max_run_depth and (len(self.sub_queue) > 0 or len(self.event_queue) > 0): await self.process_sub_queue() - await self.process_event_queue() + last_scanned_block = await self.process_event_queue() run_depth += 1 logging.debug( @@ -95,3 +108,5 @@ async def run(self): if run_depth > self.max_run_depth: self.log.warning( f"Reached max run depth of {self.max_run_depth}. This may indicate a problem with the event scanner. Events may have been missed.") + + return last_scanned_block \ No newline at end of file diff --git a/src/web3client/event_ws.py b/src/web3client/event_ws.py index 5a3dbf7..b6349a1 100644 --- a/src/web3client/event_ws.py +++ b/src/web3client/event_ws.py @@ -2,11 +2,17 @@ import logging import time from datetime import datetime +from math import ceil + from attr import dataclass from eth_typing import ChecksumAddress from eth_utils import is_checksum_address, to_checksum_address from web3 import AsyncWeb3, WebSocketProvider +from web3._utils.events import get_event_data +from web3.auto.gethdev import async_w3 +from web3.types import LogReceipt +from web3.utils.subscriptions import NewHeadsSubscriptionContext, NewHeadsSubscription, LogsSubscription from src.config_validate import validate_log_config, validate_contract_addresses from src.db.util import is_db_initialized, init_db @@ -22,7 +28,6 @@ from src.web3client.contracts_ws.service_node_rewards import ServiceNodeRewards from src.web3client.contracts_ws.token import Token from src.web3client.contracts_ws.token_vesting_staking import TokenVestingStaking -from src.web3client.contrib_contract_details import load_contributor_contract_details from src.web3client.event_queue_manager import EventQueueManager log = Log("event_ws", enable_perf=True).logger @@ -31,6 +36,8 @@ event_queue: EventQueueManager | None = None sn_contrib_factory: ServiceNodeContributionFactory | None = None +topic_map = {} +event_addresses = set() @dataclass(init=False) class VestingContractDetails: @@ -58,14 +65,15 @@ def __init__(self, beneficiary, vesting_address, amount, start, end, transferabl self.rewards_contract = rewards_contract self.sn_contrib_factory = sn_contrib_factory - assert is_checksum_address(self.beneficiary) - assert is_checksum_address(self.vesting_address) - assert self.amount > 0 - assert self.start < self.end - assert self.transferable_beneficiary is not None - assert is_checksum_address(self.revoker) - assert is_checksum_address(self.SESH) - assert is_checksum_address(self.rewards_contract) + # TODO: decide if we want this + # assert is_checksum_address(self.beneficiary) + # assert is_checksum_address(self.vesting_address) + # assert self.amount > 0 + # assert self.start < self.end + # assert self.transferable_beneficiary is not None + # assert is_checksum_address(self.revoker) + # assert is_checksum_address(self.SESH) + # assert is_checksum_address(self.rewards_contract) assert is_checksum_address(self.sn_contrib_factory) @@ -78,6 +86,7 @@ class EventScannerConfig: ws_providers: list[str] ws_max_size: int genesis_block: int + contrib_factory_start_block: int addr_token: ChecksumAddress addr_sn_contrib_factory: ChecksumAddress addr_sn_rewards: ChecksumAddress @@ -122,7 +131,7 @@ async def load_vesting_staking_contracts(w3: AsyncWeb3, details: list[VestingCon known_details = details[i // contract_interface.batch_items] beneficiary = res[i] revoker = res[i + 1] - amount = res[i + 2] + # amount = res[i + 2] transferable_beneficiary = res[i + 3] start = res[i + 4] end = res[i + 5] @@ -130,34 +139,35 @@ async def load_vesting_staking_contracts(w3: AsyncWeb3, details: list[VestingCon rewards_contract = res[i + 7] sn_contrib_factory = res[i + 8] - assert revoker == known_details.revoker, f"Expected {known_details.revoker}, got {revoker}" + # assert revoker == known_details.revoker, f"Expected {known_details.revoker}, got {revoker}" - if start < now: - assert amount == known_details.amount, f"Expected {known_details.amount}, got {amount}" + # if start < now: + # assert amount == known_details.amount, f"Expected {known_details.amount}, got {amount}" # TODO: consider checking staked amounts and asserting those - assert transferable_beneficiary == known_details.transferable_beneficiary, f"Expected {known_details.transferable_beneficiary}, got {transferable_beneficiary}" - if not transferable_beneficiary: - assert beneficiary == known_details.beneficiary, f"Expected {known_details.beneficiary}, got {beneficiary}" + # assert transferable_beneficiary == known_details.transferable_beneficiary, f"Expected {known_details.transferable_beneficiary}, got {transferable_beneficiary}" + # if not transferable_beneficiary: + # assert beneficiary == known_details.beneficiary, f"Expected {known_details.beneficiary}, got {beneficiary}" # TODO: consider checking if beneficiary has changed and is correct - assert start == known_details.start, f"Expected {known_details.start}, got {start}" - assert end == known_details.end, f"Expected {known_details.end}, got {end}" - assert SESH == known_details.SESH, f"Expected {known_details.SESH}, got {SESH}" - assert rewards_contract == known_details.rewards_contract, f"Expected {known_details.rewards_contract}, got {rewards_contract}" - assert sn_contrib_factory == known_details.sn_contrib_factory, f"Expected {known_details.sn_contrib_factory}, got {sn_contrib_factory}" + # assert start == known_details.start, f"Expected {known_details.start}, got {start}" + # assert end == known_details.end, f"Expected {known_details.end}, got {end}" + # assert SESH == known_details.SESH, f"Expected {known_details.SESH}, got {SESH}" + # assert rewards_contract == known_details.rewards_contract, f"Expected {known_details.rewards_contract}, got {rewards_contract}" + # assert sn_contrib_factory == known_details.sn_contrib_factory, f"Expected {known_details.sn_contrib_factory}, got {sn_contrib_factory}" contracts.append(VestingContract( address=known_details.vesting_address, - beneficiary=beneficiary, + beneficiary=known_details.beneficiary, initial_amount=known_details.amount, initial_beneficiary=known_details.beneficiary, - revoker=revoker, - time_end=end, - time_start=start, - transferable_beneficiary=transferable_beneficiary, + revoker=known_details.revoker, + time_end=known_details.end, + time_start=known_details.start, + transferable_beneficiary=known_details.transferable_beneficiary, )) + global_db_writer.write_vesting_contracts(contracts) # Verify that the contracts are the same coming out of the db @@ -195,37 +205,129 @@ async def init_global_contracts(w3: AsyncWeb3, config: EventScannerConfig): await load_vesting_staking_contracts(w3, details=config.vesting_contract_details) sn_contrib_factory = ServiceNodeContributionFactory(w3=w3, db_writer=global_db_writer, db_reader=global_db_reader, - log=log, event_queue=event_queue) + log=log, event_queue=event_queue, start_block=max(config.contrib_factory_start_block, start_block), topic_map=topic_map, event_addresses=event_addresses) sn_contrib_factory.create_subscriptions(address=config.addr_sn_contrib_factory) - - ServiceNodeRewards(w3=w3, db_writer=global_db_writer, log=log, event_queue=event_queue).create_subscriptions( - config.addr_sn_rewards, - ) - - RewardRatePool(w3=w3, db_writer=global_db_writer, log=log, event_queue=event_queue).create_subscriptions( + event_addresses.add(config.addr_sn_contrib_factory) + for event in sn_contrib_factory.get_events(config.addr_sn_contrib_factory): + topic_map[event().topic] = (sn_contrib_factory.event_abis[event.name], sn_contrib_factory.handle_event) + + snr = ServiceNodeRewards(w3=w3, db_writer=global_db_writer, db_reader=global_db_reader, log=log, event_queue=event_queue) + snr.create_subscriptions(config.addr_sn_rewards) + event_addresses.add(config.addr_sn_rewards) + for event in snr.get_events(config.addr_sn_rewards): + topic_map[event().topic] = (snr.event_abis[event.name], snr.handle_event) + + rrp = RewardRatePool(w3=w3, db_writer=global_db_writer, log=log, event_queue=event_queue) + rrp.create_subscriptions( config.addr_reward_rate_pool ) + event_addresses.add(config.addr_reward_rate_pool) + for event in rrp.get_events(config.addr_reward_rate_pool): + topic_map[event().topic] = (rrp.event_abis[event.name], rrp.handle_event) - Ownable2StepUpgradeable(w3=w3, db_writer=global_db_writer, log=log, event_queue=event_queue).create_subscriptions( + osu = Ownable2StepUpgradeable(w3=w3, db_writer=global_db_writer, log=log, event_queue=event_queue) + osu.create_subscriptions( [config.addr_sn_contrib_factory, config.addr_sn_rewards, config.addr_reward_rate_pool] ) + for event in osu.get_events([config.addr_sn_contrib_factory, config.addr_sn_rewards, config.addr_reward_rate_pool]): + topic_map[event().topic] = (osu.event_abis[event.name], osu._handle_event) + - PausableUpgradeable(w3=w3, db_writer=global_db_writer, log=log, event_queue=event_queue).create_subscriptions( + pu = PausableUpgradeable(w3=w3, db_writer=global_db_writer, log=log, event_queue=event_queue) + pu.create_subscriptions( [config.addr_sn_contrib_factory, config.addr_sn_rewards] ) + for event in pu.get_events([config.addr_sn_contrib_factory, config.addr_sn_rewards]): + topic_map[event().topic] = (pu.event_abis[event.name], pu._handle_event) - IERC1967(w3=w3, db_writer=global_db_writer, log=log, event_queue=event_queue).create_subscriptions( + ierc = IERC1967(w3=w3, db_writer=global_db_writer, log=log, event_queue=event_queue) + ierc.create_subscriptions( [config.addr_sn_contrib_factory, config.addr_sn_rewards, config.addr_reward_rate_pool] ) + for event in ierc.get_events([config.addr_sn_contrib_factory, config.addr_sn_rewards, config.addr_reward_rate_pool]): + topic_map[event().topic] = (ierc.event_abis[event.name], ierc._handle_event) if config.ws_watch_token_events: log.warning("Watching all token events, this may greatly increase the rescan time (ws_watch_token_events=True)") - Token(w3=w3, db_writer=global_db_writer, log=log, event_queue=event_queue).create_subscriptions( - config.addr_token - ) + tok = Token(w3=w3, db_writer=global_db_writer, log=log, event_queue=event_queue) + tok.create_subscriptions(config.addr_token) + + for event in tok.get_events(config.addr_token): + topic_map[event().topic] = (tok.event_abis[event.name], tok.handle_event) + + for topic in topic_map.keys(): + log.info(f"Added topic: {topic}") + + + +async def get_latency(w3: AsyncWeb3): + """ + Gets the latency of ws requests. In nanoseconds. + """ + start = time.time_ns() + await w3.eth.get_block_number() + return time.time_ns() - start + + +def get_batch_size(latency: int, block_time: int = 250): + """ + Calculates the batch size based on the block time and latency. + Assumes a latency of double the given latency to help ensure the batch size is not too small. + """ + est_exec_time = latency * 2 + if est_exec_time < block_time: + return 1 + else: + return ceil(est_exec_time / block_time) + + + + +async def handle_logs(logs, depth): + assert depth < 2 + depth += 1 + deploy_topic = sn_contrib_factory.factory("0x36Ee2Da54a7E727cC996A441826BBEdda6336B71").events["NewServiceNodeContributionContract"]().topic + for event in logs: + for _topic in event.get("topics", []): + topic = _topic.to_0x_hex() + if topic in topic_map: + abi, handler = topic_map[topic] + if event["address"] in event_addresses: + data = get_event_data(async_w3.codec, abi, event) + await handler(data) + + if topic == deploy_topic: + tx_index = event["transactionIndex"] + log_index = event["logIndex"] + new_logs = [] + for log in logs: + if log["transactionIndex"] == tx_index and log["logIndex"] != log_index: + new_logs.append(log) + await handle_logs(new_logs, depth) + +block_batch_size = 1 +next_block = 0 +last_block = 0 + +async def new_heads_handler(handler_context: NewHeadsSubscriptionContext): + global next_block, last_block + block = handler_context.result["number"] + if block >= next_block: + logs = [] + for event in await handler_context.async_w3.eth.get_logs({ + "fromBlock": last_block + 1, + "toBlock": block, + }): + logs.append(event) + + await handle_logs(logs, 0) + last_block = block + next_block = block + block_batch_size + async def monitor_events(w3: AsyncWeb3, run_once_as_script=False): + global block_batch_size, last_block, next_block existing_sn_contract_addresses = global_db_reader.get_arbitrum_event_main_args_by_name( "NewServiceNodeContributionContract") for address in existing_sn_contract_addresses: @@ -236,7 +338,7 @@ async def monitor_events(w3: AsyncWeb3, run_once_as_script=False): sn_contrib_factory.add_existing_contribution_contracts(existing_sn_contract_addresses) await event_queue.run() sn_contrib_factory.bootstrap_contribution_contracts() - await event_queue.run() + last_block = await event_queue.run() # WIP: To support old contracts we will need this but it isnt working yet TODO: DELETE THIS AFTER EVENTS ARE AVAILABLE # await load_contributor_contract_details(w3, existing_sn_contract_addresses) @@ -250,6 +352,26 @@ async def monitor_events(w3: AsyncWeb3, run_once_as_script=False): f"Expected all queues to be empty." \ f"Deferred DB write events: {len(global_db_writer.deferred_arbitrum_events)}" + latencies = [] + for _ in range(10): + latencies.append(await get_latency(w3)) + + max_latency_ns = max(latencies) + block_batch_size = get_batch_size(latency=ceil(max_latency_ns / 1_000_000)) + next_block = last_block + block_batch_size + + log.info(f"Metrics for ws scanner - latency: {max_latency_ns}, batch size: {block_batch_size}, last block: {last_block}, next block: {next_block}") + + + await w3.subscription_manager.subscribe( + [ + NewHeadsSubscription( + label="new-heads-mainnet", + handler=new_heads_handler + ) + ] + ) + log.info(f"Created {len(w3.subscription_manager.subscriptions)} subscriptions") log.perf.end("startup_till_processing_websocket_subscriptions") if run_once_as_script: @@ -283,6 +405,7 @@ async def start(config: EventScannerConfig): if config.db_reset_events_on_startup: log.warning("Deleting events database on startup (db_reset_events_on_startup=True)") global_db_writer.delete_all_events() + global_db_writer.write_reset_all_rewards_claim_amounts() if config.db_reset_contrib_on_startup: log.warning("Deleting contrib contracts database on startup (db_reset_contrib_on_startup=True)") From 2f1b4963c81b409bca39f27db42678428cb17210 Mon Sep 17 00:00:00 2001 From: doylet Date: Thu, 15 May 2025 13:25:51 +1000 Subject: [PATCH 121/138] Update readme and requirements with latest dependencies --- README.md | 47 ++++++++++++++++------------------------------- requirements.txt | 3 ++- 2 files changed, 18 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 5ad726b..0426ed1 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,14 @@ The `liboxenc-dev` and `liboxenmq-dev` packages require the development headers by setting up the [Oxen Deb Repository](https://deb.oxen.io). Follow those instructions then they can be installed with `apt`. -To run the backend on **Ubuntu >= 24.04**: +The backend has been tested on Ubuntu 24.04. It requires Python >=3.11.x and +`pnpm` from NodeJS >=20.12.x . To get started with this repository ensure you +have the necessary dependencies and the submodules have been synchronised. ```shell apt install build-essential python3-pip python3-dev pybind11-dev liboxenc-dev liboxenmq-dev -python3 -m pip install eth_utils web3 PyNaCl Flask uWSGI +python3 -m pip install --requirement requirements.txt +git submodule update --init --recursive ``` **Python bindings for oxen-mq & oxen-encoding** @@ -23,40 +26,26 @@ Instructions available at: ### Structure -The backend is split into three parts: +The backend has multiple parts: -- **Fetcher**: `fetcher.py` is the main server that handles all the fetching, processing, and main database writing. -- **Api**: `api.py` is the main API server that handles most requests. -- **Registrations API**: `api_registrations.py` is the registration API server that handles all registration requests - and registration database management. - -You can just run whichever service you want, but the intended usage is to run all three: - -- The fetcher will create and update the main database with network, node, contract, and arbitrum information. -- The API is purely read-only and is a glorified wrapper for the main database with caching. -- The registration API is used to store and retrieve registration information for nodes. +- **Events**: `app_events.py` retrieves events emitted by contracts on Arbitrum and stores them to the database +- **Fetcher**: `app_fetcher.py` retrieves data from the Session and Arbitrum network +- **Price**: `app_price.py` polls Coingecko for pricing information TODO: Merge this into staking +- **Registrations**: `app_registrations.py` handles HTTP requests for Session node registrations TODO: Merge this into staking +- **Snapshot**: TODO: Remove this class, snapshot should mean copying the DB file or using sqlite's native backup +- **Staking**: Serves endpoints for managing the state of staking into the Session network via the staking portal website ### Instance Before running the Fetcher or API, `oxend` must be running and its address/smart contracts configured in `config.py`. -### Running the Fetcher - -The fetcher is a pure python script that runs in a loop and fetches data from the smart contracts and the oxend RPC. You -can simply run it with `python3 fetcher.py`. - -### Running the API +### Running the backend stack -It's possible to run the API in flask directly, but you'll want to use uwsgi in production. Both methods are detailed -below: +Run the backend using UWSGI (example runs it on port 4455 with 4 request handlers): - FLASK_APP=sent flask run --reload --debugger - uwsgi --http 127.0.0.1:5000 --master -p 4 -w api --callable app + uwsgi --http 127.0.0.1:4455 --master -p 4 -w src.app_staking --callable app --mule=src/app_events.py --mule=src/app_fetcher.py -You may optionally append `--fs-reload api.py` to the `uwsgi` invocation to -automatically restart the server when `api.py` is modified. - -After the server is running, visit `127.0.0.1:5000/info` to verify that the server is up and +After the server is running, visit `127.0.0.1:4455/info` to verify that the server is up and responding correctly with a payload like the following: ```json @@ -75,10 +64,6 @@ responding correctly with a payload like the following: } ``` -### Running the Registrations API - -**Follow the same instructions as the API above. Replacing `api` with `registrations` in the commands.** - ## Setting up an oxend instance The default configuration for mainnet.py, testnet.py, devnet.py look for mainnet.sock, testnet.sock, diff --git a/requirements.txt b/requirements.txt index 5afa05a..abc1a8d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,4 +9,5 @@ eth-typing==5.0.0 eth_abi==5.1.0 requests==2.32.3 attrs==24.2.0 -pytest==8.3.4 \ No newline at end of file +pytest==8.3.4 +py-solc-x==2.0.3 From aab080d088a2701d563a8243471f5876eada39c6 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 19 May 2025 14:47:40 +1000 Subject: [PATCH 122/138] fix: update token contracts submodule to use the foundation repo --- .gitmodules | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index 7194ee5..726b286 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,3 @@ [submodule "session-token-contracts"] path = session-token-contracts - url = https://github.com/Doy-lee/eth-sn-contracts.git - branch = doyle-update-stagenet-metadata-f-new-event-emission-on-multi-contrib + url = https://github.com/session-foundation/session-token-contracts.git From 1050b82efe64915b6b4dc35fb94cb0a75ae64c57 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 19 May 2025 14:48:10 +1000 Subject: [PATCH 123/138] fix: update requirements.txt with oxenmq, oxenc, and web3 requirements --- requirements.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index abc1a8d..81e6e30 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,13 @@ -oxenmq==1.0.5 +oxenmq>=1.0.5 Flask==3.0.3 -oxenc==1.0.4 +oxenc>=1.0.4 PyNaCl==1.5.0 eth-utils==5.0.0 Werkzeug==3.0.4 -web3==7.8.0 +web3==7.11.1 eth-typing==5.0.0 eth_abi==5.1.0 requests==2.32.3 attrs==24.2.0 pytest==8.3.4 -py-solc-x==2.0.3 +py-solc-x==2.0.3 \ No newline at end of file From 66b2135f42538d265bdd5328a7f0c7ed4422ab69 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 19 May 2025 14:48:43 +1000 Subject: [PATCH 124/138] chore: update config_defaults.py with mainnet values --- src/config_defaults.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/config_defaults.py b/src/config_defaults.py index ccbbc97..49f883e 100644 --- a/src/config_defaults.py +++ b/src/config_defaults.py @@ -31,19 +31,21 @@ class Backend: WEB3 Config """ web3_provider_urls: list[str] = ["http://localhost:8545"] # Default hardhat private chain node address) - web3_provider_urls_eth: list[str] = ["http://localhost:8545"] web3_caller_address: str | None = None web3_private_key: str | None = None - addr_reward_rate_pool: str = "0x0000000000000000000000000000000000000000" - addr_token: str = "0x0000000000000000000000000000000000000000" + addr_reward_rate_pool: str = "0x11f040E89dFAbBA9070FFE6145E914AC68DbFea0" + addr_token: str = "0x10Ea9E5303670331Bdddfa66A4cEA47dae4fcF3b" addr_sn_contrib: str = "0x0000000000000000000000000000000000000000" - addr_sn_contrib_factory: str = "0x0000000000000000000000000000000000000000" - addr_sn_rewards: str = "0x0000000000000000000000000000000000000000" + addr_sn_contrib_factory: str = "0x8129bE2D5eF7ACd39483C19F28DE86b7EF19DBCA" + addr_sn_rewards: str = "0xC2B9fC251aC068763EbDfdecc792E3352E351c00" # All block scanning will take this as the starting block, no log events can occur before a contract is deployed # so this is the first block that can be used to scan for events. Using 0 significantly slows down the scan. - # genesis_block: int = 114505919 - genesis_block: int = 114500919 - contrib_factory_start_block: int = 142785134 + genesis_block: int = 336099450 + contrib_factory_start_block: int = 336099450 + # How many blocks until getLogs can be called + refresh_block_interval: int = 8 # Measured in Arb blocks + # Max block range for a getLogs call + get_logs_cap: int = 100000 """ DB CONFIG @@ -57,7 +59,6 @@ class Backend: """ API CONFIG """ - rpc_api: str = "" rpc_api_cache: int = 2 rpc_api_usage_logging: bool = False rpc_api_usage_logging_interval: int = 300 @@ -91,7 +92,7 @@ class Backend: """ VESTING """ - vesting_contract_details_csv: str = "vesting.csv" + vesting_contract_details_csv: None | str = None reset_vesting_contracts_on_startup: bool = False """ @@ -141,6 +142,8 @@ class Backend: mainnet_backend.oxen_wallet_regex = f'L[{B58_ALPHABET}]{{94}}"' mainnet_backend.rpc_shared = "ipc://oxend/mainnet.sock" mainnet_backend.sqlite_db = "ssb-mainnet.db" +mainnet_backend.ws_providers = ["ws://10.24.0.1/arb/ws"] +mainnet_backend.web3_provider_urls = ["http://10.24.0.1/arb"] # Session testnet contracts testnet_backend = Backend() @@ -169,6 +172,8 @@ class Backend: stagenet_backend.sqlite_db = "ssb-stagenet.db" stagenet_backend.ws_providers = ["ws://10.24.0.1/arb_sepolia/ws"] stagenet_backend.web3_provider_urls = ["http://10.24.0.1/arb_sepolia"] +stagenet_backend.genesis_block = 114500919 +stagenet_backend.contrib_factory_start_block = 142785134 # Assign the active backend to be used in the sent-staking-backend backend = stagenet_backend From f69b82d95d637d9966bc1c31fd60c3e43e3f5198 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 19 May 2025 14:50:19 +1000 Subject: [PATCH 125/138] feat: add daily rolling rewards endpoint --- src/app_staking.py | 1 - src/staking/app.py | 25 +++++++++++--------- src/staking/dataclasses.py | 6 +++++ src/staking/fetcher.py | 2 ++ src/staking/read.py | 12 +++++++++- src/staking/schema.sql | 12 ++++++++++ src/staking/write.py | 48 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 93 insertions(+), 13 deletions(-) diff --git a/src/app_staking.py b/src/app_staking.py index 13dac81..0965b4d 100644 --- a/src/app_staking.py +++ b/src/app_staking.py @@ -10,7 +10,6 @@ sqlite_schema=config.backend.sqlite_schema, sqlite_db_registrations=config.backend.registration_sqlite_db, sqlite_schema_registrations=config.backend.registration_sqlite_schema, - rpc_api=config.backend.rpc_api, rpc_api_cache=config.backend.rpc_api_cache, rpc_shared=config.backend.rpc_shared, rpc_shared_cache=config.backend.rpc_shared_cache, diff --git a/src/staking/app.py b/src/staking/app.py index f688783..fcbc3f3 100644 --- a/src/staking/app.py +++ b/src/staking/app.py @@ -10,7 +10,7 @@ from uwsgidecorators import timer from werkzeug.exceptions import GatewayTimeout -from .dataclasses import ArbitrumInfo, RewardsInfo +from .dataclasses import ArbitrumInfo, RewardsInfo, DailyRewardInfoNode from .read import DBReaderStaking from ..oxen.rpc import OxenRPC from ..registration.read import DBReaderRegistrations @@ -30,7 +30,6 @@ class StakingAppConfig(FlaskAppConfig): sqlite_db_registrations: str = None sqlite_schema_registrations: str = None - rpc_api: str = None rpc_api_cache: int = None rpc_shared: str = None rpc_shared_cache: int = None @@ -54,7 +53,6 @@ def __init__(self, config: StakingAppConfig): perf=config.enable_perf, ) - rpc_url = config.rpc_api if config.rpc_api else config.rpc_shared rpc_cache = ( config.rpc_api_cache if config.rpc_api_cache @@ -63,7 +61,7 @@ def __init__(self, config: StakingAppConfig): self.rpc = OxenRPC( logger=self.log, - rpc_url=rpc_url, + rpc_url=config.rpc_shared, cache_seconds=rpc_cache, usage_tracking=config.rpc_api_usage_logging, ) @@ -148,13 +146,6 @@ def get_next_block_timestamp_est(): def get_network_info_cached(): return app.cache.get("network_info", getter=get_network_info_uncached, ttl=1) - def get_arbitrum_events_cached(): - return app.cache.get("arbitrum_events_all", getter=app.get_arbitrum_events, ttl=1) - - - def get_contribution_contracts_for_address_uncached(address: str): - return [] - def get_contribution_contracts_for_address_cached(address: str): return app.cache.get(f"contribution_contracts-{address}", getter=get_related_contribution_contracts_for_eth_address_uncached, getter_args=address, ttl=1) @@ -562,6 +553,18 @@ def get_rewards(eth_wal: str): return flask.abort(405) # Method not allowed + def get_daily_rewards_info(eth_wal: str): + return app.db_reader.get_daily_rewards_info_for_address(eth_wal, 0) + + def get_daily_rewards_info_cached(eth_wal: str): + return app.cache.get(f"daily-rewards-info-{eth_wal}", getter=get_daily_rewards_info, getter_args=eth_wal, + invalidate_timestamp=get_next_block_timestamp_est()) + + @app.route("/daily-rewards/") + def get_daily_rewards(eth_wal: str): + return json_res({"rewards": get_daily_rewards_info_cached(eth_wal)}) + + """ ////////////////////////////////////////////////////////////// // // diff --git a/src/staking/dataclasses.py b/src/staking/dataclasses.py index f63ca27..e96a242 100644 --- a/src/staking/dataclasses.py +++ b/src/staking/dataclasses.py @@ -209,6 +209,12 @@ class RewardsInfo: # staked into. claimed_rewards: int = 0 +@dataclass +class DailyRewardInfoNode: + block: int + lifetime_rewards: int + timestamp: int + @dataclass class Registration: operator: bytes diff --git a/src/staking/fetcher.py b/src/staking/fetcher.py index 9da0b1e..e47e930 100644 --- a/src/staking/fetcher.py +++ b/src/staking/fetcher.py @@ -307,6 +307,8 @@ def update_network_details_and_nodes( rewards_info = self.get_rewards_info() self.db_writer.write_rewards_info_to_db(rewards_info) + now = time.time() + self.db_writer.update_daily_rolling_rewards(rewards_info, current_height, now) self.log.info("Scheduled task finish") diff --git a/src/staking/read.py b/src/staking/read.py index 5ffc2ff..b259adf 100644 --- a/src/staking/read.py +++ b/src/staking/read.py @@ -3,7 +3,8 @@ from ..db.read import DBReader from .dataclasses import DBNode, DBContributionMain, DBNetworkInfo, DBContributionContract, \ - DBContributionContractContribution, SmartContractABI, ArbitrumInfo, VestingContract, RewardsInfo + DBContributionContractContribution, SmartContractABI, ArbitrumInfo, VestingContract, RewardsInfo, \ + DailyRewardInfoNode from ..db.util import sql_connect_in_read_mode from ..log import Log from ..util.parse import eth_format @@ -252,11 +253,20 @@ def get_rewards_info_for_address(self, address: str): with closing(sql_connect_in_read_mode(self.db_path)) as connection: with closing(connection.cursor()) as cursor: cursor.execute("SELECT * FROM rewards_info WHERE address = ?", (address,)) + if cursor.rowcount == 0: + return None info = RewardsInfo(*cursor.fetchone()) self.log.debug("Rewards info: {}".format(info)) self.log.perf.end("get_rewards_info_for_address") return info + def get_daily_rewards_info_for_address(self, address: str, from_block: int = 0): + with closing(sql_connect_in_read_mode(self.db_path)) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute("SELECT block, lifetime_rewards, timestamp FROM daily_rewards_info WHERE address = ? AND block >= ?", (address, from_block)) + info = [DailyRewardInfoNode(*node) for node in cursor.fetchall()] + return info + def get_smart_contract_abis(self): self.log.perf.start("get_smart_contract_abis") with closing(sql_connect_in_read_mode(self.db_path)) as connection: diff --git a/src/staking/schema.sql b/src/staking/schema.sql index cb6662a..9f12f6a 100644 --- a/src/staking/schema.sql +++ b/src/staking/schema.sql @@ -159,6 +159,18 @@ CREATE TABLE rewards_info ( claimed_rewards INTEGER NOT NULL DEFAULT 0 ); +CREATE TABLE daily_rewards_info ( + address BLOB NOT NULL, + block INTEGER NOT NULL, + lifetime_rewards INTEGER NOT NULL, + timestamp FLOAT NOT NULL, + + PRIMARY KEY (address, block) +); + +CREATE INDEX daily_rewards_info_timestamp_asc ON daily_rewards_info(timestamp ASC); +CREATE INDEX daily_rewards_info_timestamp_desc ON daily_rewards_info(timestamp DESC); + CREATE TABLE arbitrum_info ( block INTEGER PRIMARY KEY NOT NULL, timestamp FLOAT NOT NULL DEFAULT ((julianday('now') - 2440587.5)*86400.0), /* unix epoch */ diff --git a/src/staking/write.py b/src/staking/write.py index 20ef0bd..1821899 100644 --- a/src/staking/write.py +++ b/src/staking/write.py @@ -463,6 +463,54 @@ def write_rewards_info_to_db(self, rewards_info: list[RewardsInfo]): connection.commit() self.log.perf.end("write_rewards_info_to_db") + def update_daily_rolling_rewards(self, rewards_info: list[RewardsInfo], block: int, timestamp: int): + self.log.perf.start("update_daily_rolling_rewards") + with closing(sql_connect_in_write_mode(self.db_path)) as connection: + connection.execute("BEGIN") + with closing(connection.cursor()) as cursor: + self.log.debug("Deleting expired daily rolling rewards") + + # Keep 26 hours of rewards for a safe buffer, we only care about the last 24 hours + delete_before_timestamp = timestamp - 26 * 60 * 60 + + cursor.execute( + """ + DELETE FROM daily_rewards_info WHERE timestamp < ? + """ + , (delete_before_timestamp,)) + + self.log.debug("Updating daily rolling rewards") + cursor.executemany( + """ + INSERT INTO daily_rewards_info ( + address, + block, + lifetime_rewards, + timestamp + ) + VALUES (?, ?, ?, ?) ON CONFLICT DO NOTHING; + """, + ( + ( + info.address, + block, + info.lifetime_rewards, + timestamp + ) + for info in rewards_info + ), + ) + + inserted_rewards_rows = cursor.rowcount + + self.log.debug( + "Inserted {} rows into daily_rewards_info".format(inserted_rewards_rows) + ) + + connection.commit() + self.log.perf.end("update_daily_rolling_rewards") + + def write_update_rewards_claim_amounts(self, address: str, claimed_stakes: int, claimed_rewards: int): self.log.perf.start("write_update_rewards_claim_amounts") with closing(sql_connect_in_write_mode(self.db_path)) as connection: From bcf78483192a9208029c98c56648446c7be94622 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 19 May 2025 14:51:21 +1000 Subject: [PATCH 126/138] feat: add block cap and getLogs chunking to event scanner and new heads handler --- src/app_events.py | 2 + .../{subscription.py => contract_utils.py} | 16 +-- src/web3client/contracts_ws/contract_ws.py | 2 +- src/web3client/contracts_ws/ierc_1967.py | 7 +- .../ownable_2_step_upgradeable.py | 7 +- .../contracts_ws/pausable_upgradeable.py | 7 +- .../contracts_ws/reward_rate_pool.py | 7 +- .../contracts_ws/service_node_contribution.py | 7 +- .../service_node_contribution_factory.py | 9 +- .../contracts_ws/service_node_rewards.py | 7 +- src/web3client/contracts_ws/token.py | 7 +- .../contracts_ws/token_vesting_staking.py | 7 +- src/web3client/event_queue_manager.py | 70 +++++----- src/web3client/event_ws.py | 130 +++++++++--------- 14 files changed, 136 insertions(+), 149 deletions(-) rename src/web3client/contracts_ws/{subscription.py => contract_utils.py} (77%) diff --git a/src/app_events.py b/src/app_events.py index 61fa96a..f2a214c 100644 --- a/src/app_events.py +++ b/src/app_events.py @@ -27,6 +27,8 @@ addr_sn_contrib_factory=config.backend.addr_sn_contrib_factory, addr_sn_rewards=config.backend.addr_sn_rewards, addr_reward_rate_pool=config.backend.addr_reward_rate_pool, + refresh_block_interval=config.backend.refresh_block_interval, + get_logs_cap=config.backend.get_logs_cap, sqlite_db=config.backend.sqlite_db, sqlite_schema=config.backend.sqlite_schema, db_reset_events_on_startup=config.backend.db_reset_events_on_startup, diff --git a/src/web3client/contracts_ws/subscription.py b/src/web3client/contracts_ws/contract_utils.py similarity index 77% rename from src/web3client/contracts_ws/subscription.py rename to src/web3client/contracts_ws/contract_utils.py index 5e7596c..1dc738d 100644 --- a/src/web3client/contracts_ws/subscription.py +++ b/src/web3client/contracts_ws/contract_utils.py @@ -3,36 +3,26 @@ from web3._utils.events import get_event_data from web3.contract.async_contract import AsyncContractEvent from web3.types import EventData -from web3.utils.subscriptions import LogsSubscription, EthSubscriptionContext +from web3.utils.subscriptions import EthSubscriptionContext from src.staking.write import DBWriterStaking from src.web3client.event_queue_manager import EventQueueManager from src.web3client.event_scanner import ProcessedEvent -def create_subscriptions( +def queue_past_events_for_scanning( events: list[AsyncContractEvent], event_queue: EventQueueManager, - handler_sub: Callable, handler_past: Callable = None, event_abis: dict[str, any] = None, start_block: int = None, ): assert isinstance(event_queue, EventQueueManager), "event_queue must be an instance of EventQueueManager" for event in events: - label_address = event.address[0] if isinstance(event.address, list) else event.address - label = f"{event.name}_{label_address}" - - event_queue.add( + event_queue.add_event( event=event, handler=handler_past, start_block=start_block, - sub=LogsSubscription( - label=label, - address=event.address, - topics=[event().topic], - handler=handler_sub, - ), ) if event.name not in event_abis: diff --git a/src/web3client/contracts_ws/contract_ws.py b/src/web3client/contracts_ws/contract_ws.py index a20d51f..22eaac2 100644 --- a/src/web3client/contracts_ws/contract_ws.py +++ b/src/web3client/contracts_ws/contract_ws.py @@ -6,7 +6,7 @@ from src.staking.write import DBWriterStaking from src.web3client.contract_factory import ContractFactory -from src.web3client.contracts_ws.subscription import parse_event, write_event_to_db +from src.web3client.contracts_ws.contract_utils import parse_event, write_event_to_db from src.web3client.event_queue_manager import EventQueueManager diff --git a/src/web3client/contracts_ws/ierc_1967.py b/src/web3client/contracts_ws/ierc_1967.py index df6f031..5a65a83 100644 --- a/src/web3client/contracts_ws/ierc_1967.py +++ b/src/web3client/contracts_ws/ierc_1967.py @@ -6,7 +6,7 @@ from src.staking.write import DBWriterStaking from src.web3client.contracts_ws.contract_ws import ContractWS -from src.web3client.contracts_ws.subscription import create_subscriptions +from src.web3client.contracts_ws.contract_utils import queue_past_events_for_scanning from src.web3client.event_queue_manager import EventQueueManager @@ -25,16 +25,15 @@ def get_events(self, address: ChecksumAddress | list[ChecksumAddress]) -> list[A events.BeaconUpgraded, ] - def create_subscriptions(self, address: ChecksumAddress | list[ChecksumAddress]): + def queue_past_events_for_scanning(self, address: ChecksumAddress | list[ChecksumAddress]): event_list = self.get_events(address) for event in event_list: event.address = address - return create_subscriptions( + return queue_past_events_for_scanning( events=event_list, event_queue=self.event_queue, event_abis=self.event_abis, - handler_sub=self._handle_event_sub, handler_past=self._handle_event, ) diff --git a/src/web3client/contracts_ws/ownable_2_step_upgradeable.py b/src/web3client/contracts_ws/ownable_2_step_upgradeable.py index c5b09bf..999c2cd 100644 --- a/src/web3client/contracts_ws/ownable_2_step_upgradeable.py +++ b/src/web3client/contracts_ws/ownable_2_step_upgradeable.py @@ -6,7 +6,7 @@ from src.staking.write import DBWriterStaking from src.web3client.contracts_ws.contract_ws import ContractWS -from src.web3client.contracts_ws.subscription import create_subscriptions +from src.web3client.contracts_ws.contract_utils import queue_past_events_for_scanning from src.web3client.event_queue_manager import EventQueueManager @@ -25,16 +25,15 @@ def get_events(self, address: ChecksumAddress | list[ChecksumAddress]) -> list[A events.Initialized, ] - def create_subscriptions(self, address: ChecksumAddress | list[ChecksumAddress]): + def queue_past_events_for_scanning(self, address: ChecksumAddress | list[ChecksumAddress]): event_list = self.get_events(address) for event in event_list: event.address = address - return create_subscriptions( + return queue_past_events_for_scanning( events=event_list, event_queue=self.event_queue, event_abis=self.event_abis, - handler_sub=self._handle_event_sub, handler_past=self._handle_event, ) diff --git a/src/web3client/contracts_ws/pausable_upgradeable.py b/src/web3client/contracts_ws/pausable_upgradeable.py index 551c840..1e039e2 100644 --- a/src/web3client/contracts_ws/pausable_upgradeable.py +++ b/src/web3client/contracts_ws/pausable_upgradeable.py @@ -6,7 +6,7 @@ from src.staking.write import DBWriterStaking from src.web3client.contracts_ws.contract_ws import ContractWS -from src.web3client.contracts_ws.subscription import create_subscriptions +from src.web3client.contracts_ws.contract_utils import queue_past_events_for_scanning from src.web3client.event_queue_manager import EventQueueManager @@ -24,16 +24,15 @@ def get_events(self, address: ChecksumAddress | list[ChecksumAddress]) -> list[A events.Unpaused, ] - def create_subscriptions(self, address: ChecksumAddress | list[ChecksumAddress]): + def queue_past_events_for_scanning(self, address: ChecksumAddress | list[ChecksumAddress]): event_list = self.get_events(address) for event in event_list: event.address = address - return create_subscriptions( + return queue_past_events_for_scanning( events=event_list, event_queue=self.event_queue, event_abis=self.event_abis, - handler_sub=self._handle_event_sub, handler_past=self._handle_event, ) diff --git a/src/web3client/contracts_ws/reward_rate_pool.py b/src/web3client/contracts_ws/reward_rate_pool.py index 17460e2..026eb74 100644 --- a/src/web3client/contracts_ws/reward_rate_pool.py +++ b/src/web3client/contracts_ws/reward_rate_pool.py @@ -8,7 +8,7 @@ from src.staking.write import DBWriterStaking from src.web3client.contracts_ws.contract_ws import ContractWS -from src.web3client.contracts_ws.subscription import create_subscriptions +from src.web3client.contracts_ws.contract_utils import queue_past_events_for_scanning from src.web3client.event_queue_manager import EventQueueManager @@ -39,11 +39,10 @@ def get_events(self, address: ChecksumAddress) -> list[AsyncContractEvent]: events = self.factory(address).events return [events.FundsReleased] - def create_subscriptions(self, address: ChecksumAddress): - return create_subscriptions( + def queue_past_events_for_scanning(self, address: ChecksumAddress): + return queue_past_events_for_scanning( events=self.get_events(address), event_queue=self.event_queue, event_abis=self.event_abis, - handler_sub=self.handle_event_sub, handler_past=self.handle_event, ) diff --git a/src/web3client/contracts_ws/service_node_contribution.py b/src/web3client/contracts_ws/service_node_contribution.py index 01462c0..3552dd1 100644 --- a/src/web3client/contracts_ws/service_node_contribution.py +++ b/src/web3client/contracts_ws/service_node_contribution.py @@ -11,7 +11,7 @@ from src.staking.write import DBWriterStaking from src.util.parse import parse_ed25519_pubkey, parse_bls_pubkey from src.web3client.contracts_ws.contract_ws import ContractWS -from src.web3client.contracts_ws.subscription import create_subscriptions, create_processed_event +from src.web3client.contracts_ws.contract_utils import queue_past_events_for_scanning, create_processed_event from src.web3client.event_queue_manager import EventQueueManager @@ -189,17 +189,16 @@ def get_events(self, address: ChecksumAddress) -> list[AsyncContractEvent]: events = self.factory(address[0] if isinstance(address, list) else address).events return [events[name] for name in self.event_names] - def create_subscriptions(self, address: ChecksumAddress | list[ChecksumAddress], start_block: int = 0): + def queue_past_events_for_scanning(self, address: ChecksumAddress | list[ChecksumAddress], start_block: int = 0): event_list = self.get_events(address) for event in event_list: event.address = address - return create_subscriptions( + return queue_past_events_for_scanning( events=event_list, event_queue=self.event_queue, event_abis=self.event_abis, - handler_sub=self.handle_event_sub, handler_past=self.handle_event, start_block=start_block, ) diff --git a/src/web3client/contracts_ws/service_node_contribution_factory.py b/src/web3client/contracts_ws/service_node_contribution_factory.py index 41e917c..70d1b3d 100644 --- a/src/web3client/contracts_ws/service_node_contribution_factory.py +++ b/src/web3client/contracts_ws/service_node_contribution_factory.py @@ -11,7 +11,7 @@ from src.util.parse import parse_ed25519_pubkey from src.web3client.contracts_ws.contract_ws import ContractWS from src.web3client.contracts_ws.service_node_contribution import ServiceNodeContribution -from src.web3client.contracts_ws.subscription import create_subscriptions +from src.web3client.contracts_ws.contract_utils import queue_past_events_for_scanning from src.web3client.event_queue_manager import EventQueueManager @@ -38,7 +38,7 @@ def bootstrap_contribution_contracts(self): self.log.warning("No bootstrap contribution contract addresses found") return self.log.info(f"Bootstrapping {len(self.bootstrap_contribution_contract_addresses)} contribution contracts from block {self.start_block}") - self.service_node_contribution_contract.create_subscriptions( + self.service_node_contribution_contract.queue_past_events_for_scanning( address=self.bootstrap_contribution_contract_addresses, start_block=self.start_block) events = self.service_node_contribution_contract.get_events(self.bootstrap_contribution_contract_addresses[0]) for address in self.bootstrap_contribution_contract_addresses: @@ -78,12 +78,11 @@ def get_events(self, address: ChecksumAddress) -> list[AsyncContractEvent]: events = self.factory(address).events return [events[name] for name in self.event_names] - def create_subscriptions(self, address: ChecksumAddress): - return create_subscriptions( + def queue_past_events_for_scanning(self, address: ChecksumAddress): + return queue_past_events_for_scanning( events=self.get_events(address), event_queue=self.event_queue, event_abis=self.event_abis, - handler_sub=self.handle_event_sub, handler_past=self.handle_event_bootstrap, start_block=self.start_block, ) diff --git a/src/web3client/contracts_ws/service_node_rewards.py b/src/web3client/contracts_ws/service_node_rewards.py index 6eaf447..1746de7 100644 --- a/src/web3client/contracts_ws/service_node_rewards.py +++ b/src/web3client/contracts_ws/service_node_rewards.py @@ -9,7 +9,7 @@ from src.staking.read import DBReaderStaking from src.staking.write import DBWriterStaking from src.web3client.contracts_ws.contract_ws import ContractWS -from src.web3client.contracts_ws.subscription import create_subscriptions +from src.web3client.contracts_ws.contract_utils import queue_past_events_for_scanning from src.web3client.event_queue_manager import EventQueueManager def handle_claim(event: EventData, db_writer: DBWriterStaking, db_reader: DBReaderStaking, log: logging): @@ -99,11 +99,10 @@ def get_events(self, address: ChecksumAddress) -> list[AsyncContractEvent]: events.BLSNonSignerThresholdMaxUpdated, ] - def create_subscriptions(self, address: ChecksumAddress): - return create_subscriptions( + def queue_past_events_for_scanning(self, address: ChecksumAddress): + return queue_past_events_for_scanning( events=self.get_events(address), event_queue=self.event_queue, event_abis=self.event_abis, - handler_sub=self.handle_event_sub, handler_past=self.handle_event, ) diff --git a/src/web3client/contracts_ws/token.py b/src/web3client/contracts_ws/token.py index 3cb1c91..66bc2a2 100644 --- a/src/web3client/contracts_ws/token.py +++ b/src/web3client/contracts_ws/token.py @@ -8,7 +8,7 @@ from src.staking.write import DBWriterStaking from src.web3client.contracts_ws.contract_ws import ContractWS -from src.web3client.contracts_ws.subscription import create_subscriptions +from src.web3client.contracts_ws.contract_utils import queue_past_events_for_scanning from src.web3client.event_queue_manager import EventQueueManager @@ -59,11 +59,10 @@ def get_events(self, address: ChecksumAddress) -> list[AsyncContractEvent]: events.Approval, ] - def create_subscriptions(self, address: ChecksumAddress): - return create_subscriptions( + def queue_past_events_for_scanning(self, address: ChecksumAddress): + return queue_past_events_for_scanning( events=self.get_events(address), event_queue=self.event_queue, event_abis=self.event_abis, - handler_sub=self.handle_event_sub, handler_past=self.handle_event, ) diff --git a/src/web3client/contracts_ws/token_vesting_staking.py b/src/web3client/contracts_ws/token_vesting_staking.py index e7f3651..c714911 100644 --- a/src/web3client/contracts_ws/token_vesting_staking.py +++ b/src/web3client/contracts_ws/token_vesting_staking.py @@ -8,7 +8,7 @@ from src.staking.write import DBWriterStaking from src.web3client.contracts_ws.contract_ws import ContractWS -from src.web3client.contracts_ws.subscription import create_subscriptions, create_processed_event +from src.web3client.contracts_ws.contract_utils import queue_past_events_for_scanning, create_processed_event from src.web3client.event_queue_manager import EventQueueManager @@ -28,7 +28,7 @@ async def handle_event(self, event: EventData): async def handle_event_sub(self, event: EthSubscriptionContext): await self.handle_event(self._parse_event(event)) - def create_subscriptions(self, address: ChecksumAddress | list[ChecksumAddress]): + def queue_past_events_for_scanning(self, address: ChecksumAddress | list[ChecksumAddress]): events = self.factory(address[0] if isinstance(address, list) else address).events event_list = [ events.TokensReleased, @@ -41,11 +41,10 @@ def create_subscriptions(self, address: ChecksumAddress | list[ChecksumAddress]) for event in event_list: event.address = address - return create_subscriptions( + return queue_past_events_for_scanning( events=events, event_queue=self.event_queue, event_abis=self.event_abis, - handler_sub=self.handle_event_sub, handler_past=self.handle_event, ) diff --git a/src/web3client/event_queue_manager.py b/src/web3client/event_queue_manager.py index 602d2a1..56ff271 100644 --- a/src/web3client/event_queue_manager.py +++ b/src/web3client/event_queue_manager.py @@ -1,10 +1,10 @@ import asyncio import logging from dataclasses import dataclass +from math import ceil from typing import Callable from web3 import AsyncWeb3 -from web3.utils.subscriptions import LogsSubscription from src.util.parse import get_relative_time_from_ms from src.web3client.util import get_time_of_arbitrum_blocks_ms @@ -18,15 +18,14 @@ class PastEventsFetchQueueItem: class EventQueueManager: - def __init__(self, w3: AsyncWeb3, log: logging, start_block: int = 0, max_run_depth: int = 10): + def __init__(self, w3: AsyncWeb3, log: logging, start_block: int = 0, max_run_depth: int = 10, get_logs_cap: int = 0): self.processed_events = 0 - self.processed_subs = 0 - self.sub_queue = [] self.event_queue = [] self.w3 = w3 self.log = log self.start_block = start_block self.max_run_depth = max_run_depth + self.get_logs_cap = get_logs_cap log.info(f"Event queue manager starting with start block {start_block}") log.debug(f"Event queue manager max run depth {max_run_depth}") @@ -35,34 +34,51 @@ def add_event(self, event: any, handler: Callable, start_block: int | None = Non start_block = self.start_block self.event_queue.append(PastEventsFetchQueueItem(event=event, handler=handler, start_block=start_block)) - def add_subscription(self, sub: LogsSubscription): - self.sub_queue.append(sub) - - def add(self, event: any, handler: Callable, sub: LogsSubscription, start_block: int | None = None): - self.add_subscription(sub) - self.add_event(event, handler, start_block) - - async def process_event_queue(self, start_block: int | None = None): + async def process_event_queue(self, start_block: int | None = None, current_block: int = 0): if start_block is None: start_block = self.start_block # Once a websocket subscription is made, any events will be queued to be processed by the websocket consumer. This # past event scan should be done right after the websocket subscription is made so the block number at this time # can be used for all past event scans. - block_current = await self.w3.eth.block_number + block_current = await self.w3.eth.block_number if current_block == 0 else current_block queue, self.event_queue = self.event_queue, [] if len(queue) > 0: - self.log.info(f"Fetching past events for {len(queue)} events") + get_logs_calls = 0 + for e in queue: + from_block = e.start_block if e.start_block else start_block + e.get_logs_calls=(ceil((block_current - from_block) / self.get_logs_cap) if self.get_logs_cap > 0 else 1) + get_logs_calls += e.get_logs_calls + + start_log_message = f"Fetching past events for {len(queue)} events, this will take {get_logs_calls} getLogs calls with get_logs_cap set to {self.get_logs_cap}" + + if get_logs_calls > 500: + start_log_message += f" (this may take a while)" + self.log.warning(start_log_message) + else: + self.log.info(start_log_message) responses = [] for past_event in queue: from_block = past_event.start_block if past_event.start_block else start_block self.log.info( f"Fetching past events for {past_event.event.event_name} from block {from_block} to block {block_current} ({block_current - from_block} blocks ~{get_relative_time_from_ms(get_time_of_arbitrum_blocks_ms(block_current - from_block))})") - for recent in await past_event.event.get_logs(from_block=from_block, to_block=block_current): - responses.append((recent, past_event.handler)) + + fetched_block = start_block - 1 + + to_block = block_current if self.get_logs_cap == 0 else min(block_current, from_block + self.get_logs_cap) + + self.log.info(f"Fetching logs for {past_event.event.event_name} with get_logs_cap set to {self.get_logs_cap} will take {past_event.get_logs_calls} getLogs calls") + + while fetched_block < to_block: + self.log.info(f"Fetching logs for {past_event.event.event_name} from block {from_block} to block {to_block} ({to_block - from_block} blocks ~{get_relative_time_from_ms(get_time_of_arbitrum_blocks_ms(to_block - from_block))})") + for recent in await past_event.event.get_logs(from_block=from_block, to_block=to_block): + responses.append((recent, past_event.handler)) + fetched_block = to_block + from_block = to_block + 1 + to_block = block_current if self.get_logs_cap == 0 else min(block_current, from_block + self.get_logs_cap) self.processed_events += 1 # sort responses by block then log index @@ -79,31 +95,17 @@ async def process_event_queue(self, start_block: int | None = None): return block_current - async def process_sub_queue(self): - sub_queue, self.sub_queue = self.sub_queue, [] - return - - if len(sub_queue) > 0: - await self.w3.subscription_manager.subscribe(sub_queue) - self.processed_subs += len(sub_queue) - self.log.info(f"Subscribed to {len(sub_queue)} subscriptions") - for sub in sub_queue: - self.log.info(f"Subscribed to {sub.label} for topics {sub.topics}") - else: - self.log.debug("No subscriptions to subscribe to") - - async def run(self): + async def run(self, current_block: int = 0): # The max depth ensures the loop won't get stuck in an infinite loop. This is just a safety measure as it should # not be possible due to the queue population dependencies. run_depth = 0 last_scanned_block = 0 - while run_depth <= self.max_run_depth and (len(self.sub_queue) > 0 or len(self.event_queue) > 0): - await self.process_sub_queue() - last_scanned_block = await self.process_event_queue() + while run_depth <= self.max_run_depth and len(self.event_queue) > 0: + last_scanned_block = await self.process_event_queue(current_block=current_block) run_depth += 1 logging.debug( - f"Processed lifetime total of {self.processed_events} events and {self.processed_subs} subscriptions") + f"Processed lifetime total of {self.processed_events} events") if run_depth > self.max_run_depth: self.log.warning( diff --git a/src/web3client/event_ws.py b/src/web3client/event_ws.py index b6349a1..b236742 100644 --- a/src/web3client/event_ws.py +++ b/src/web3client/event_ws.py @@ -2,7 +2,7 @@ import logging import time from datetime import datetime -from math import ceil +from functools import partial from attr import dataclass @@ -11,8 +11,7 @@ from web3 import AsyncWeb3, WebSocketProvider from web3._utils.events import get_event_data from web3.auto.gethdev import async_w3 -from web3.types import LogReceipt -from web3.utils.subscriptions import NewHeadsSubscriptionContext, NewHeadsSubscription, LogsSubscription +from web3.utils.subscriptions import NewHeadsSubscriptionContext, NewHeadsSubscription from src.config_validate import validate_log_config, validate_contract_addresses from src.db.util import is_db_initialized, init_db @@ -91,6 +90,8 @@ class EventScannerConfig: addr_sn_contrib_factory: ChecksumAddress addr_sn_rewards: ChecksumAddress addr_reward_rate_pool: ChecksumAddress + get_logs_cap: int + refresh_block_interval: int sqlite_db: str sqlite_schema: str db_reset_events_on_startup: bool @@ -190,7 +191,7 @@ async def load_vesting_staking_contracts(w3: AsyncWeb3, details: list[VestingCon f"Added vesting contract {contract.address} with initial balance {contract.initial_amount} for {contract.beneficiary}") log.info(f"Subscribing to {len(address_list)} vesting contracts") - contract_interface.create_subscriptions(address=address_list) + contract_interface.queue_past_events_for_scanning(address=address_list) async def init_global_contracts(w3: AsyncWeb3, config: EventScannerConfig): @@ -200,25 +201,26 @@ async def init_global_contracts(w3: AsyncWeb3, config: EventScannerConfig): start_block = last_event_block + 1 if last_event_block else config.genesis_block log.info(f"Last block for an event from the database: {last_event_block}, starting from block {start_block}") - event_queue = EventQueueManager(w3=w3, log=log, start_block=start_block, max_run_depth=config.ws_max_run_depth) + event_queue = EventQueueManager(w3=w3, log=log, start_block=start_block, max_run_depth=config.ws_max_run_depth, get_logs_cap=config.get_logs_cap) - await load_vesting_staking_contracts(w3, details=config.vesting_contract_details) + if len(config.vesting_contract_details) > 0 : + await load_vesting_staking_contracts(w3, details=config.vesting_contract_details) sn_contrib_factory = ServiceNodeContributionFactory(w3=w3, db_writer=global_db_writer, db_reader=global_db_reader, log=log, event_queue=event_queue, start_block=max(config.contrib_factory_start_block, start_block), topic_map=topic_map, event_addresses=event_addresses) - sn_contrib_factory.create_subscriptions(address=config.addr_sn_contrib_factory) + sn_contrib_factory.queue_past_events_for_scanning(address=config.addr_sn_contrib_factory) event_addresses.add(config.addr_sn_contrib_factory) for event in sn_contrib_factory.get_events(config.addr_sn_contrib_factory): topic_map[event().topic] = (sn_contrib_factory.event_abis[event.name], sn_contrib_factory.handle_event) snr = ServiceNodeRewards(w3=w3, db_writer=global_db_writer, db_reader=global_db_reader, log=log, event_queue=event_queue) - snr.create_subscriptions(config.addr_sn_rewards) + snr.queue_past_events_for_scanning(config.addr_sn_rewards) event_addresses.add(config.addr_sn_rewards) for event in snr.get_events(config.addr_sn_rewards): topic_map[event().topic] = (snr.event_abis[event.name], snr.handle_event) rrp = RewardRatePool(w3=w3, db_writer=global_db_writer, log=log, event_queue=event_queue) - rrp.create_subscriptions( + rrp.queue_past_events_for_scanning( config.addr_reward_rate_pool ) event_addresses.add(config.addr_reward_rate_pool) @@ -226,7 +228,7 @@ async def init_global_contracts(w3: AsyncWeb3, config: EventScannerConfig): topic_map[event().topic] = (rrp.event_abis[event.name], rrp.handle_event) osu = Ownable2StepUpgradeable(w3=w3, db_writer=global_db_writer, log=log, event_queue=event_queue) - osu.create_subscriptions( + osu.queue_past_events_for_scanning( [config.addr_sn_contrib_factory, config.addr_sn_rewards, config.addr_reward_rate_pool] ) for event in osu.get_events([config.addr_sn_contrib_factory, config.addr_sn_rewards, config.addr_reward_rate_pool]): @@ -234,14 +236,14 @@ async def init_global_contracts(w3: AsyncWeb3, config: EventScannerConfig): pu = PausableUpgradeable(w3=w3, db_writer=global_db_writer, log=log, event_queue=event_queue) - pu.create_subscriptions( + pu.queue_past_events_for_scanning( [config.addr_sn_contrib_factory, config.addr_sn_rewards] ) for event in pu.get_events([config.addr_sn_contrib_factory, config.addr_sn_rewards]): topic_map[event().topic] = (pu.event_abis[event.name], pu._handle_event) ierc = IERC1967(w3=w3, db_writer=global_db_writer, log=log, event_queue=event_queue) - ierc.create_subscriptions( + ierc.queue_past_events_for_scanning( [config.addr_sn_contrib_factory, config.addr_sn_rewards, config.addr_reward_rate_pool] ) for event in ierc.get_events([config.addr_sn_contrib_factory, config.addr_sn_rewards, config.addr_reward_rate_pool]): @@ -250,7 +252,7 @@ async def init_global_contracts(w3: AsyncWeb3, config: EventScannerConfig): if config.ws_watch_token_events: log.warning("Watching all token events, this may greatly increase the rescan time (ws_watch_token_events=True)") tok = Token(w3=w3, db_writer=global_db_writer, log=log, event_queue=event_queue) - tok.create_subscriptions(config.addr_token) + tok.queue_past_events_for_scanning(config.addr_token) for event in tok.get_events(config.addr_token): topic_map[event().topic] = (tok.event_abis[event.name], tok.handle_event) @@ -269,24 +271,10 @@ async def get_latency(w3: AsyncWeb3): return time.time_ns() - start -def get_batch_size(latency: int, block_time: int = 250): - """ - Calculates the batch size based on the block time and latency. - Assumes a latency of double the given latency to help ensure the batch size is not too small. - """ - est_exec_time = latency * 2 - if est_exec_time < block_time: - return 1 - else: - return ceil(est_exec_time / block_time) - - - - -async def handle_logs(logs, depth): +async def handle_logs(config: EventScannerConfig, handler_context: NewHeadsSubscriptionContext, logs, depth): assert depth < 2 depth += 1 - deploy_topic = sn_contrib_factory.factory("0x36Ee2Da54a7E727cC996A441826BBEdda6336B71").events["NewServiceNodeContributionContract"]().topic + deploy_topic = sn_contrib_factory.factory(config.addr_sn_contrib_factory).events["NewServiceNodeContributionContract"]().topic for event in logs: for _topic in event.get("topics", []): topic = _topic.to_0x_hex() @@ -297,56 +285,78 @@ async def handle_logs(logs, depth): await handler(data) if topic == deploy_topic: - tx_index = event["transactionIndex"] - log_index = event["logIndex"] + block = event["blockNumber"] + contract_address = event.args.get("contributorContract") new_logs = [] - for log in logs: - if log["transactionIndex"] == tx_index and log["logIndex"] != log_index: - new_logs.append(log) - await handle_logs(new_logs, depth) + for new_event in await handler_context.async_w3.eth.get_logs({ + "fromBlock": block, + "toBlock": block, + "address": contract_address, + }): + logs.append(new_event) + await handle_logs(config, handler_context, new_logs, depth) -block_batch_size = 1 next_block = 0 last_block = 0 -async def new_heads_handler(handler_context: NewHeadsSubscriptionContext): +async def new_heads_handler(config: EventScannerConfig, handler_context: NewHeadsSubscriptionContext): global next_block, last_block block = handler_context.result["number"] if block >= next_block: logs = [] + cutoff = last_block + config.get_logs_cap + was_cut_off = False + if block > cutoff: + block = cutoff + was_cut_off = True + + block = min(block, last_block + config.get_logs_cap) for event in await handler_context.async_w3.eth.get_logs({ "fromBlock": last_block + 1, "toBlock": block, + "address": list(event_addresses), }): logs.append(event) - await handle_logs(logs, 0) + await handle_logs(config, handler_context, logs, 0) last_block = block - next_block = block + block_batch_size + next_block = block + (1 if was_cut_off else config.refresh_block_interval) -async def monitor_events(w3: AsyncWeb3, run_once_as_script=False): - global block_batch_size, last_block, next_block +async def monitor_events(config: EventScannerConfig, w3: AsyncWeb3, run_once_as_script=False): + global last_block, next_block existing_sn_contract_addresses = global_db_reader.get_arbitrum_event_main_args_by_name( "NewServiceNodeContributionContract") for address in existing_sn_contract_addresses: assert is_checksum_address( address), f"Invalid existing NewServiceNodeContributionContract contract address: {address}" + # Note: We need the current block just before we subscribe to new heads, this block number is used to fetch all + # past events up to and including this "current block". Once we get this block we immediately subscribe to new + # heads, which will start filling up the websocket queue with all the blocks we might miss while we scan for + # past events. Once past events are scanned for we'll have a large queue of blocks to catch up on, which we will + # start processing in the subscription queue. + current_block = await w3.eth.get_block_number() + await w3.subscription_manager.subscribe( + [ + NewHeadsSubscription( + label="new-heads-mainnet", + handler=partial(new_heads_handler, config) + ) + ] + ) + + log.info(f"Created {len(w3.subscription_manager.subscriptions)} subscriptions") + global_db_writer.defer_writing_arbitrum_events = True sn_contrib_factory.add_existing_contribution_contracts(existing_sn_contract_addresses) - await event_queue.run() - sn_contrib_factory.bootstrap_contribution_contracts() - last_block = await event_queue.run() - - # WIP: To support old contracts we will need this but it isnt working yet TODO: DELETE THIS AFTER EVENTS ARE AVAILABLE - # await load_contributor_contract_details(w3, existing_sn_contract_addresses) + last_block = max(config.contrib_factory_start_block, await event_queue.run(current_block=current_block)) global_db_writer.defer_writing_arbitrum_events = False global_db_writer.write_deferred_arbitrum_events_to_db() - assert len(event_queue.sub_queue) == 0 and len( + assert len( event_queue.event_queue) == 0 and len( global_db_writer.deferred_arbitrum_events) == 0, \ f"Expected all queues to be empty." \ @@ -357,22 +367,10 @@ async def monitor_events(w3: AsyncWeb3, run_once_as_script=False): latencies.append(await get_latency(w3)) max_latency_ns = max(latencies) - block_batch_size = get_batch_size(latency=ceil(max_latency_ns / 1_000_000)) - next_block = last_block + block_batch_size + next_block = last_block + config.refresh_block_interval - log.info(f"Metrics for ws scanner - latency: {max_latency_ns}, batch size: {block_batch_size}, last block: {last_block}, next block: {next_block}") - - - await w3.subscription_manager.subscribe( - [ - NewHeadsSubscription( - label="new-heads-mainnet", - handler=new_heads_handler - ) - ] - ) + log.info(f"Metrics for ws scanner - latency: {max_latency_ns}, batch size: {config.refresh_block_interval}, last block: {last_block}, next block: {next_block}") - log.info(f"Created {len(w3.subscription_manager.subscriptions)} subscriptions") log.perf.end("startup_till_processing_websocket_subscriptions") if run_once_as_script: log.info("run_once_as_script is True, exiting...") @@ -418,11 +416,15 @@ async def start(config: EventScannerConfig): log.info("Starting event scanner") provider = config.ws_providers[0] async for w3 in AsyncWeb3( - WebSocketProvider(provider, websocket_kwargs={"max_size": config.ws_max_size}, request_timeout=300) + WebSocketProvider(provider, websocket_kwargs={"max_size": config.ws_max_size}, + request_timeout=300, + # one hour of arbitrum blocks in the queue, the high number is required because blocks + # pile up in the queue while rescanning the chain. + subscription_response_queue_size=3600*4) ): # TODO: investigate if this properly handles disconnects await init_global_contracts(w3, config) - await asyncio.create_task(monitor_events(w3=w3, run_once_as_script=config.run_once_as_script)) + await asyncio.create_task(monitor_events(config=config, w3=w3, run_once_as_script=config.run_once_as_script)) if config.run_once_as_script: log.info("Exiting websocket disconnection loop as run_once_as_script is True") break From ef46433a1e2949a191a3af6543483441172adfc6 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 19 May 2025 14:52:04 +1000 Subject: [PATCH 127/138] chore: add utility setup scripts --- scripts/install-local-deps.sh | 12 ++++++++++++ scripts/install-system-deps.sh | 8 ++++++++ scripts/setup-venv.sh | 6 ++++++ 3 files changed, 26 insertions(+) create mode 100755 scripts/install-local-deps.sh create mode 100755 scripts/install-system-deps.sh create mode 100755 scripts/setup-venv.sh diff --git a/scripts/install-local-deps.sh b/scripts/install-local-deps.sh new file mode 100755 index 0000000..8fe94fc --- /dev/null +++ b/scripts/install-local-deps.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +set -e + +mkdir dep-tmp +git clone https://github.com/oxen-io/oxen-pyoxenc ./dep-tmp/oxenc +git clone https://github.com/oxen-io/oxen-pyoxenmq ./dep-tmp/oxenmq +pip install ./dep-tmp/oxenc ./dep-tmp/oxenmq + +rm -rf dep-tmp + +pip install -r requirements.txt diff --git a/scripts/install-system-deps.sh b/scripts/install-system-deps.sh new file mode 100755 index 0000000..dc8dda5 --- /dev/null +++ b/scripts/install-system-deps.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -e + +sudo curl -so /etc/apt/trusted.gpg.d/oxen.gpg https://deb.oxen.io/pub.gpg +echo "deb https://deb.oxen.io $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/oxen.list +sudo apt update +sudo apt install build-essential python3-pip python3-dev pybind11-dev liboxenc-dev liboxenmq-dev python3.12-venv diff --git a/scripts/setup-venv.sh b/scripts/setup-venv.sh new file mode 100755 index 0000000..a030d01 --- /dev/null +++ b/scripts/setup-venv.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +set -e + +python3 -m venv .venv +. .venv/bin/activate From 195ff2453e5b1df0b54dcdef77b09ba3b36fcfe2 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Mon, 19 May 2025 15:58:12 +1000 Subject: [PATCH 128/138] feat: add price range endpoint --- src/price/app.py | 42 ++++++++++++++++++++++++++++++++++++++---- src/price/read.py | 12 ++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/price/app.py b/src/price/app.py index 65cc010..23e732d 100644 --- a/src/price/app.py +++ b/src/price/app.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +import time from dataclasses import dataclass from math import trunc @@ -6,7 +7,7 @@ from ..util.flask_utils import FlaskApp, json_response, FlaskAppConfig from .coingecko import CoinGeckoTokenPriceRequest -from .read import get_latest_price +from .read import get_latest_price, get_prices_since from .dataclasses import PriceDB from .write import write_prices_to_db from ..db.util import is_db_initialized, init_db @@ -70,6 +71,10 @@ def __init__(self, config: PriceAppConfig): def get_token_price_cache_key(token: str): return f"price-{token}-all" + @staticmethod + def get_token_prices_cache_key_range(token: str, timestamp: int): + return f"price-range-{token}-{timestamp}" + def get_price_for_token_cached(self, token: str) -> PriceDB | None: key = self.get_token_price_cache_key(token) @@ -81,9 +86,7 @@ def get_price_for_token_cached(self, token: str) -> PriceDB | None: if value is None: return None - invalidate_timestamp = value.updated_at + self.app_config.price_poll_rate_seconds - - self.cache.set_cache_value(key, value, ttl=self.app_config.price_poll_rate_seconds, invalidate_timestamp=invalidate_timestamp) + self.cache.set_cache_value(key, value, invalidate_timestamp=value.updated_at + self.app_config.price_poll_rate_seconds) return value @@ -109,6 +112,17 @@ def get_token_price_info(self, token: str = None): "t_stale": stale_time, } + def get_prices_for_token_cached(self, token: str, range_seconds: int) -> list[PriceDB]: + key = self.get_token_prices_cache_key_range(token, range_seconds) + value = self.cache.get_cached_only(key) + + if value is None: + value = get_prices_since(self.db_path, token, trunc(time.time()) - range_seconds) + + if len(value) > 0: + self.cache.set_cache_value(key, value, invalidate_timestamp=value[0].updated_at + self.app_config.price_poll_rate_seconds) + + return value def create_app(config: PriceAppConfig) -> App: app = App(config) @@ -138,6 +152,26 @@ def route_get_token_price_for_token(token: str): "price": app.get_token_price_info(token) }) + def get_token_price_range_response(token: str, range_seconds: int): + return json_response({ + "prices": app.get_prices_for_token_cached(token, range_seconds) + }) + + @app.route("/prices//") + def route_get_token_prices_for_token_range(token: str, period: str): + match period: + case "1h": + return get_token_price_range_response(token, 60 * 60) + case "1d": + return get_token_price_range_response(token, 60 * 60 * 24) + case "7d": + return get_token_price_range_response(token, 60 * 60 * 24 * 7) + case "30d": + return get_token_price_range_response(token, 60 * 60 * 24 * 30) + case _: + return flask.abort(400, f"Invalid period {period}") + + if config.enable_price_fetcher: app.log.info("Polling for price info every {} seconds".format(app.price_poll_rate_seconds)) try: diff --git a/src/price/read.py b/src/price/read.py index 2391e83..3c61372 100644 --- a/src/price/read.py +++ b/src/price/read.py @@ -15,3 +15,15 @@ def get_latest_price(db_path: str, token: str): ) price = PriceDB(*cursor.fetchone()) return price + +def get_prices_since(db_path: str, token: str, timestamp: int): + with closing(sql_connect_in_read_mode(db_path)) as connection: + with closing(connection.cursor()) as cursor: + cursor.execute( + """ + SELECT * FROM prices WHERE token = ? AND updated_at >= ? ORDER BY updated_at DESC + """, + (token, timestamp), + ) + prices = [PriceDB(*price) for price in cursor.fetchall()] + return prices From 04127bcb95672dbfffbbf55e3a5ece3276ff7b52 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Wed, 21 May 2025 10:42:13 +1000 Subject: [PATCH 129/138] fix: seeded nodes in fetcher --- src/staking/fetcher.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/staking/fetcher.py b/src/staking/fetcher.py index e47e930..7b904cf 100644 --- a/src/staking/fetcher.py +++ b/src/staking/fetcher.py @@ -328,8 +328,13 @@ def fetch_service_node_list(self): nodes: list[ServiceNode] = res.get("service_node_states") self.log.debug("Fetched {} service nodes".format(len(nodes))) - new_sn_events = self.db_reader.get_arbitrum_events_by_name("NewServiceNodeV2", + new_sn_events = self.db_reader.get_arbitrum_events_by_name("NewSeededServiceNode", + from_block=self.last_new_sn_event + 1) + new_v2_events = self.db_reader.get_arbitrum_events_by_name("NewServiceNodeV2", from_block=self.last_new_sn_event + 1) + + new_sn_events.extend(new_v2_events) + if len(new_sn_events) == 0: self.log.warning("No new service node events found, waiting for new events") return From 2ac209152266df2ce0c6687ea0a20d6e6217de49 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Wed, 21 May 2025 10:53:09 +1000 Subject: [PATCH 130/138] fix: seed node pubkey arg --- src/staking/fetcher.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/staking/fetcher.py b/src/staking/fetcher.py index 7b904cf..fd9c2f8 100644 --- a/src/staking/fetcher.py +++ b/src/staking/fetcher.py @@ -328,17 +328,21 @@ def fetch_service_node_list(self): nodes: list[ServiceNode] = res.get("service_node_states") self.log.debug("Fetched {} service nodes".format(len(nodes))) - new_sn_events = self.db_reader.get_arbitrum_events_by_name("NewSeededServiceNode", + new_seed_events = self.db_reader.get_arbitrum_events_by_name("NewSeededServiceNode", from_block=self.last_new_sn_event + 1) - new_v2_events = self.db_reader.get_arbitrum_events_by_name("NewServiceNodeV2", + new_sn_events = self.db_reader.get_arbitrum_events_by_name("NewServiceNodeV2", from_block=self.last_new_sn_event + 1) - new_sn_events.extend(new_v2_events) - - if len(new_sn_events) == 0: + if len(new_seed_events)== 0 and len(new_sn_events) == 0: self.log.warning("No new service node events found, waiting for new events") return + + for event in new_seed_events: + self.contract_id_map[parse_bls_pubkey(event.args["ed25519Pubkey"])] = event.args["serviceNodeID"] + if event.block > self.last_new_sn_event: + self.last_new_sn_event = event.block + for event in new_sn_events: self.contract_id_map[parse_bls_pubkey(event.args["pubkey"])] = event.args["serviceNodeID"] if event.block > self.last_new_sn_event: From ed129c401585204e3ed37f4e14026969cc9d306e Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Wed, 21 May 2025 10:55:26 +1000 Subject: [PATCH 131/138] fix: seed node bls pubkey arg --- src/staking/fetcher.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/staking/fetcher.py b/src/staking/fetcher.py index fd9c2f8..e940859 100644 --- a/src/staking/fetcher.py +++ b/src/staking/fetcher.py @@ -337,9 +337,8 @@ def fetch_service_node_list(self): self.log.warning("No new service node events found, waiting for new events") return - for event in new_seed_events: - self.contract_id_map[parse_bls_pubkey(event.args["ed25519Pubkey"])] = event.args["serviceNodeID"] + self.contract_id_map[parse_bls_pubkey(event.args["blsPubkey"])] = event.args["serviceNodeID"] if event.block > self.last_new_sn_event: self.last_new_sn_event = event.block From 4ccd968863bf3e6dbf752fcd9403ed99e9852fd1 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Tue, 3 Jun 2025 14:55:57 +1000 Subject: [PATCH 132/138] fix: add and use fetched time for stale time for token prices --- src/price/app.py | 42 +++++++++++++++++++++++++++------------- src/price/coingecko.py | 5 ++++- src/price/dataclasses.py | 1 + src/price/read.py | 2 +- src/price/schema.sql | 4 +++- src/price/write.py | 5 +++-- 6 files changed, 41 insertions(+), 18 deletions(-) diff --git a/src/price/app.py b/src/price/app.py index 23e732d..bac9907 100644 --- a/src/price/app.py +++ b/src/price/app.py @@ -2,6 +2,7 @@ import time from dataclasses import dataclass from math import trunc +from numbers import Number import flask @@ -72,8 +73,8 @@ def get_token_price_cache_key(token: str): return f"price-{token}-all" @staticmethod - def get_token_prices_cache_key_range(token: str, timestamp: int): - return f"price-range-{token}-{timestamp}" + def get_token_prices_cache_key_range(token: str, timestamp: int, resolution: int): + return f"price-range-{token}-{timestamp}-{resolution}" def get_price_for_token_cached(self, token: str) -> PriceDB | None: @@ -86,7 +87,7 @@ def get_price_for_token_cached(self, token: str) -> PriceDB | None: if value is None: return None - self.cache.set_cache_value(key, value, invalidate_timestamp=value.updated_at + self.app_config.price_poll_rate_seconds) + self.cache.set_cache_value(key, value, invalidate_timestamp=value.fetched_at + self.app_config.price_poll_rate_seconds) return value @@ -112,8 +113,8 @@ def get_token_price_info(self, token: str = None): "t_stale": stale_time, } - def get_prices_for_token_cached(self, token: str, range_seconds: int) -> list[PriceDB]: - key = self.get_token_prices_cache_key_range(token, range_seconds) + def get_prices_for_token_cached(self, token: str, range_seconds: int, resolution_seconds: int) -> list[PriceDB]: + key = self.get_token_prices_cache_key_range(token, range_seconds, resolution_seconds) value = self.cache.get_cached_only(key) if value is None: @@ -122,7 +123,22 @@ def get_prices_for_token_cached(self, token: str, range_seconds: int) -> list[Pr if len(value) > 0: self.cache.set_cache_value(key, value, invalidate_timestamp=value[0].updated_at + self.app_config.price_poll_rate_seconds) - return value + resolved_prices = [] + + # NOTE: prices are in descending order or when they were updated + + next_price_resolution = value[0].updated_at + 1 + for price in value: + if price.updated_at > next_price_resolution: + continue + + resolved_prices.append({ + "price": price.price, + "t": price.updated_at, + }) + next_price_resolution = price.updated_at - resolution_seconds + + return resolved_prices def create_app(config: PriceAppConfig) -> App: app = App(config) @@ -152,22 +168,22 @@ def route_get_token_price_for_token(token: str): "price": app.get_token_price_info(token) }) - def get_token_price_range_response(token: str, range_seconds: int): + def get_token_price_range_response(token: str, range_seconds: int, resolution_seconds: int): return json_response({ - "prices": app.get_prices_for_token_cached(token, range_seconds) + "prices": app.get_prices_for_token_cached(token, range_seconds, resolution_seconds), }) - @app.route("/prices//") + @app.route("/prices//") def route_get_token_prices_for_token_range(token: str, period: str): match period: case "1h": - return get_token_price_range_response(token, 60 * 60) + return get_token_price_range_response(token, 60 * 60, config.price_poll_rate_seconds) case "1d": - return get_token_price_range_response(token, 60 * 60 * 24) + return get_token_price_range_response(token, 60 * 60 * 24, config.price_poll_rate_seconds) case "7d": - return get_token_price_range_response(token, 60 * 60 * 24 * 7) + return get_token_price_range_response(token, 60 * 60 * 24 * 7, 60 * 60) case "30d": - return get_token_price_range_response(token, 60 * 60 * 24 * 30) + return get_token_price_range_response(token, 60 * 60 * 24 * 30, 1) case _: return flask.abort(400, f"Invalid period {period}") diff --git a/src/price/coingecko.py b/src/price/coingecko.py index 5e5d00e..622d06d 100644 --- a/src/price/coingecko.py +++ b/src/price/coingecko.py @@ -1,4 +1,6 @@ import logging +import time + import requests from .dataclasses import PriceDB @@ -84,13 +86,14 @@ def format_for_db(self, response: dict): self.log.warning(f"Token {token} not found in CoinGecko API response") continue + fetched_at = int(time.time()) updated_at = response[token].get("last_updated_at", None) market_cap = response[token].get(f"usd_market_cap", None) price = response[token].get("usd", None) if price is None: self.log.warning(f"USD price not found in CoinGecko API response for token {token}") - result.append(PriceDB(token=token, price=price, market_cap=market_cap, updated_at=updated_at)) + result.append(PriceDB(token=token, price=price, market_cap=market_cap, updated_at=updated_at, fetched_at=fetched_at)) return result diff --git a/src/price/dataclasses.py b/src/price/dataclasses.py index 878595c..2caaa13 100644 --- a/src/price/dataclasses.py +++ b/src/price/dataclasses.py @@ -7,3 +7,4 @@ class PriceDB: price: float market_cap: float updated_at: int + fetched_at: int diff --git a/src/price/read.py b/src/price/read.py index 3c61372..39cfe9c 100644 --- a/src/price/read.py +++ b/src/price/read.py @@ -9,7 +9,7 @@ def get_latest_price(db_path: str, token: str): with closing(connection.cursor()) as cursor: cursor.execute( """ - SELECT * FROM prices WHERE token = ? ORDER BY updated_at DESC LIMIT 1 + SELECT * FROM prices WHERE token = ? ORDER BY fetched_at DESC LIMIT 1 """, (token,), ) diff --git a/src/price/schema.sql b/src/price/schema.sql index 8473522..b28c594 100644 --- a/src/price/schema.sql +++ b/src/price/schema.sql @@ -4,8 +4,10 @@ CREATE TABLE prices ( token TEXT NOT NULL, price FLOAT NOT NULL, market_cap FLOAT NOT NULL, - updated_at INTEGER NOT NULL + updated_at INTEGER NOT NULL, + fetched_at INTEGER NOT NULL ); CREATE INDEX prices_updated_at_idx ON prices(updated_at DESC); CREATE INDEX prices_token_at_idx ON prices(token, updated_at DESC); +CREATE INDEX prices_token_fetched_at_idx ON prices(token, fetched_at DESC); diff --git a/src/price/write.py b/src/price/write.py index 21bcfa1..cd3afc2 100644 --- a/src/price/write.py +++ b/src/price/write.py @@ -10,8 +10,8 @@ def write_prices_to_db(db_path: str, prices: list[PriceDB]): with closing(connection.cursor()) as cursor: cursor.executemany( """ - INSERT INTO prices (token, price, market_cap, updated_at) - VALUES (?, ?, ?, ?) + INSERT INTO prices (token, price, market_cap, updated_at, fetched_at) + VALUES (?, ?, ?, ?, ?) """, ( ( @@ -19,6 +19,7 @@ def write_prices_to_db(db_path: str, prices: list[PriceDB]): price.price, price.market_cap, price.updated_at, + price.fetched_at, ) for price in prices ), From 92b9745d47b75b0edd1c6cfa48fe5a4fc3920a07 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 25 Jul 2025 13:29:25 +1000 Subject: [PATCH 133/138] fix: add network mock validation for registrations --- src/registration/app.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/registration/app.py b/src/registration/app.py index 650652e..a070bd2 100644 --- a/src/registration/app.py +++ b/src/registration/app.py @@ -97,10 +97,13 @@ def store_registration(sn_pubkey: bytes): stake requires an additional interaction through a multi-contributor contract while solo registrations can call the staking contract directly. """ - try: + # TODO: replace with network type validation + def check_network(k,v): + return v params = parse_query_params( { + "network": check_network, "pubkey_bls": byte_decoder(64), "sig_ed25519": byte_decoder(64), "sig_bls": byte_decoder(128), @@ -112,6 +115,7 @@ def store_registration(sn_pubkey: bytes): check_reg_keys_sigs(params) except ValueError as e: + app.log.exception(e) return json_response({"error": f"Invalid registration: {e}"}) app.db_writer.write_registration_to_db(params) From 0ecc93f011bd7a73ff17052e8f0615ed8fff136e Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 25 Jul 2025 13:30:39 +1000 Subject: [PATCH 134/138] fix: combine new service nodes with seeded service nodes and handle legacy addresses --- src/staking/app.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/staking/app.py b/src/staking/app.py index fcbc3f3..172beec 100644 --- a/src/staking/app.py +++ b/src/staking/app.py @@ -182,9 +182,16 @@ def route_get_nodes(): def get_added_bls_keys(): events_exit = app.db_reader.get_arbitrum_events_by_name("ServiceNodeExit") sn_ids_exited = set([event.args["serviceNodeID"] for event in events_exit]) - + new_seed_events = app.db_reader.get_arbitrum_events_by_name("NewSeededServiceNode") + new_sn_v2_events = app.db_reader.get_arbitrum_events_by_name("NewServiceNodeV2") contract_id_map = {} - for event in app.db_reader.get_arbitrum_events_by_name("NewServiceNodeV2"): + + for event in new_seed_events: + sn_id = event.args["serviceNodeID"] + if sn_id not in sn_ids_exited: + contract_id_map[parse_bls_pubkey(event.args["blsPubkey"])] = sn_id + + for event in new_sn_v2_events: sn_id = event.args["serviceNodeID"] if sn_id not in sn_ids_exited: contract_id_map[parse_bls_pubkey(event.args["pubkey"])] = sn_id @@ -212,12 +219,13 @@ def get_related_stakes_for_eth_address_uncached(address: ChecksumAddress): related_nodes = [] for node in nodes: - if eth_format(node.operator_address) == address: - related_nodes.append(node) - elif node.contributors is not None: - for contributor in node.contributors: + for contributor in node.contributors: + try: if eth_format(contributor.address) == address: related_nodes.append(node) + except Exception as e: + app.log.exception(e) + continue return related_nodes From 3e2104f46c3f522d7602cb43a13df672b74c6903 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 25 Jul 2025 13:31:33 +1000 Subject: [PATCH 135/138] fix: multicontributor contract bootstrapping --- src/staking/fetcher.py | 12 +++-- .../contracts_ws/service_node_contribution.py | 2 +- .../contracts_ws/service_node_rewards.py | 7 ++- src/web3client/event_ws.py | 52 ++++++++++++------- 4 files changed, 46 insertions(+), 27 deletions(-) diff --git a/src/staking/fetcher.py b/src/staking/fetcher.py index e940859..1eabaf5 100644 --- a/src/staking/fetcher.py +++ b/src/staking/fetcher.py @@ -335,7 +335,7 @@ def fetch_service_node_list(self): if len(new_seed_events)== 0 and len(new_sn_events) == 0: self.log.warning("No new service node events found, waiting for new events") - return + return parsed_nodes, contributions, current_height, len(parsed_nodes), total_staked, active_node_count for event in new_seed_events: self.contract_id_map[parse_bls_pubkey(event.args["blsPubkey"])] = event.args["serviceNodeID"] @@ -355,13 +355,15 @@ def fetch_service_node_list(self): pubkey_bls = node.get("pubkey_bls") contract_id = self.contract_id_map.get(pubkey_bls) + if contract_id is None: + continue node["contract_id"] = contract_id if node["contract_id"] is None: self.log.warning( "Contract ID not found for node with BLS pubkey: {}".format(pubkey_bls) ) - assert node["contract_id"] is not None + #assert node["contract_id"] is not None # Remove some fields that might appear if field:all is passed to the rpc if "portions_for_operator" in node: @@ -450,8 +452,10 @@ def update_exit_list(self): pubkey_bls = entry.get("info").get("bls_public_key") if pubkey_bls is None: - self.log.warning(f"info.bls_public_key is None for bls_exit_liquidation_list entry: {entry}") - continue + pubkey_bls = entry.get("info").get("pubkey_bls") + if pubkey_bls is None: + self.log.warning(f"info.bls_public_key is None for bls_exit_liquidation_list entry: {entry}") + continue exit_type = entry.get("type") exit_events.append( diff --git a/src/web3client/contracts_ws/service_node_contribution.py b/src/web3client/contracts_ws/service_node_contribution.py index 3552dd1..29344ed 100644 --- a/src/web3client/contracts_ws/service_node_contribution.py +++ b/src/web3client/contracts_ws/service_node_contribution.py @@ -79,7 +79,7 @@ def update_contributor_withdraw_contribution(self, address: str, amount: int): self._contributors[address].amount -= amount if self._contributors[address].amount == 0: del self._contributors[address] - self.db_writer.write_delete_contribution_contract_contributor(self.address) + self.db_writer.write_delete_contribution_contract_contributor(self.address, address) else: self.db_writer.write_update_contribution_contract_contributor(self.address, self._contributors[address]) else: diff --git a/src/web3client/contracts_ws/service_node_rewards.py b/src/web3client/contracts_ws/service_node_rewards.py index 1746de7..347f589 100644 --- a/src/web3client/contracts_ws/service_node_rewards.py +++ b/src/web3client/contracts_ws/service_node_rewards.py @@ -16,7 +16,10 @@ def handle_claim(event: EventData, db_writer: DBWriterStaking, db_reader: DBRead address = event.args.recipientAddress amount = event.args.amount - reward_info = db_reader.get_rewards_info_for_address(address) + try: + reward_info = db_reader.get_rewards_info_for_address(address) + except Exception as e: + return assert reward_info is not None, f"Rewards info not found for address {address}" @@ -40,7 +43,7 @@ def handle_claim(event: EventData, db_writer: DBWriterStaking, db_reader: DBRead claimed_rewards += remaining remaining = 0 - assert remaining == 0, f"Remaining rewards {remaining} is not equal to 0" + # assert remaining == 0, f"Remaining rewards {remaining} is not equal to 0" db_writer.write_update_rewards_claim_amounts(address, claimed_stakes, claimed_rewards) diff --git a/src/web3client/event_ws.py b/src/web3client/event_ws.py index b236742..2f4a852 100644 --- a/src/web3client/event_ws.py +++ b/src/web3client/event_ws.py @@ -110,7 +110,7 @@ def __post_init__(self): async def load_vesting_staking_contracts(w3: AsyncWeb3, details: list[VestingContractDetails]): contract_interface = TokenVestingStaking(w3=w3, db_writer=global_db_writer, log=log, event_queue=event_queue) - token_contract = Token(w3=w3, db_writer=global_db_writer, log=log).factory(details[0].SESH) + #token_contract = Token(w3=w3, db_writer=global_db_writer, log=log).factory(details[0].SESH) if global_db_reader.has_vesting_contracts(): if len(details) > 0: @@ -125,20 +125,20 @@ async def load_vesting_staking_contracts(w3: AsyncWeb3, details: list[VestingCon address_list = [contract.vesting_address for contract in details] now = time.time() - res = await contract_interface.batch_get_details(address_list, token_contract) - + #res = await contract_interface.batch_get_details(address_list, token_contract + res = [] contracts = [] - for i in range(0, len(res), contract_interface.batch_items): - known_details = details[i // contract_interface.batch_items] - beneficiary = res[i] - revoker = res[i + 1] + for known_details in details: + #known_details = details[i // contract_interface.batch_items] + #beneficiary = res[i] + #revoker = res[i + 1] # amount = res[i + 2] - transferable_beneficiary = res[i + 3] - start = res[i + 4] - end = res[i + 5] - SESH = res[i + 6] - rewards_contract = res[i + 7] - sn_contrib_factory = res[i + 8] + #transferable_beneficiary = res[i + 3] + #start = res[i + 4] + #end = res[i + 5] + #SESH = res[i + 6] + #rewards_contract = res[i + 7] + #sn_contrib_factory = res[i + 8] # assert revoker == known_details.revoker, f"Expected {known_details.revoker}, got {revoker}" @@ -286,14 +286,14 @@ async def handle_logs(config: EventScannerConfig, handler_context: NewHeadsSubsc if topic == deploy_topic: block = event["blockNumber"] - contract_address = event.args.get("contributorContract") + contract_address = data.args.get("contributorContract") new_logs = [] for new_event in await handler_context.async_w3.eth.get_logs({ "fromBlock": block, "toBlock": block, "address": contract_address, }): - logs.append(new_event) + new_logs.append(new_event) await handle_logs(config, handler_context, new_logs, depth) next_block = 0 @@ -326,11 +326,6 @@ async def new_heads_handler(config: EventScannerConfig, handler_context: NewHead async def monitor_events(config: EventScannerConfig, w3: AsyncWeb3, run_once_as_script=False): global last_block, next_block - existing_sn_contract_addresses = global_db_reader.get_arbitrum_event_main_args_by_name( - "NewServiceNodeContributionContract") - for address in existing_sn_contract_addresses: - assert is_checksum_address( - address), f"Invalid existing NewServiceNodeContributionContract contract address: {address}" # Note: We need the current block just before we subscribe to new heads, this block number is used to fetch all # past events up to and including this "current block". Once we get this block we immediately subscribe to new @@ -350,7 +345,24 @@ async def monitor_events(config: EventScannerConfig, w3: AsyncWeb3, run_once_as_ log.info(f"Created {len(w3.subscription_manager.subscriptions)} subscriptions") global_db_writer.defer_writing_arbitrum_events = True + # sn_contrib_factory.add_existing_contribution_contracts(existing_sn_contract_addresses) + last_block = max(config.contrib_factory_start_block, await event_queue.run(current_block=current_block)) + + global_db_writer.defer_writing_arbitrum_events = False + global_db_writer.write_deferred_arbitrum_events_to_db() + + existing_sn_contract_addresses = global_db_reader.get_arbitrum_event_main_args_by_name( + "NewServiceNodeContributionContract") + for address in existing_sn_contract_addresses: + assert is_checksum_address( + address), f"Invalid existing NewServiceNodeContributionContract contract address: {address}" + event_addresses.add(address) + sn_contrib_factory.add_existing_contribution_contracts(existing_sn_contract_addresses) + sn_contrib_factory.bootstrap_contribution_contracts() + + global_db_writer.defer_writing_arbitrum_events = True + last_block = max(config.contrib_factory_start_block, await event_queue.run(current_block=current_block)) global_db_writer.defer_writing_arbitrum_events = False From b9126b94557070a3eba764606e83f02359c7ce86 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Fri, 25 Jul 2025 15:05:01 +1000 Subject: [PATCH 136/138] fix: checksum vesting contract beneficiary addresses --- src/staking/app.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/staking/app.py b/src/staking/app.py index 172beec..08d8010 100644 --- a/src/staking/app.py +++ b/src/staking/app.py @@ -154,8 +154,9 @@ def get_vesting_contracts_cached(): def get_vesting_contracts_for_beneficiary_cached(beneficiary: str): contracts = [] - for contract in get_vesting_contracts_cached(): - if contract.beneficiary == beneficiary: + vesting = get_vesting_contracts_cached() + for contract in vesting: + if eth_format(contract.beneficiary) == beneficiary: contracts.append(contract) return contracts From 7968e29ec926b0649e69f5b67b19fe9e314f96aa Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Wed, 20 Aug 2025 09:51:53 +1000 Subject: [PATCH 137/138] fix: sort arbitrum events in db call --- src/staking/read.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/staking/read.py b/src/staking/read.py index b259adf..2c11aaa 100644 --- a/src/staking/read.py +++ b/src/staking/read.py @@ -202,7 +202,7 @@ def get_nodes(self): placeholder= '?' # For SQLite. See DBAPI paramstyle. placeholders= ', '.join(placeholder for unused in contract_ids) - query= 'SELECT * FROM arbitrum_events WHERE main_arg IN (%s) ORDER BY block DESC' % placeholders + query= 'SELECT * FROM arbitrum_events WHERE main_arg IN (%s) ORDER BY block DESC, log_index DESC' % placeholders cursor.execute(query, contract_ids) for event in cursor.fetchall(): @@ -397,13 +397,13 @@ def get_arbitrum_events_by_name(self, name: str, from_block = 0): self.log.perf.end("get_arbitrum_events_by_name") return events - def get_arbitrum_events_by_main_args(self, main_args: list[str]): + def get_arbitrum_events_by_main_args_desc(self, main_args: list[str]): self.log.perf.start("get_arbitrum_events_by_main_arg") with closing(sql_connect_in_read_mode(self.db_path)) as connection: with closing(connection.cursor()) as cursor: cursor.execute( """ - SELECT * FROM arbitrum_events WHERE main_arg IN ({}) + SELECT * FROM arbitrum_events WHERE main_arg IN ({}) ORDER BY block DESC, log_index DESC """.format(",".join(["?"] * len(main_args))), (tuple(main_args)), ) From 72a4a7fc598581c0dd571c07599cd5e40994afd5 Mon Sep 17 00:00:00 2001 From: Ryan Miller Date: Wed, 20 Aug 2025 09:52:17 +1000 Subject: [PATCH 138/138] feat: add hardfork info and contract nodes endpoints --- src/oxen/rpc.py | 6 ++++++ src/staking/app.py | 51 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/oxen/rpc.py b/src/oxen/rpc.py index 25b434c..92ec927 100644 --- a/src/oxen/rpc.py +++ b/src/oxen/rpc.py @@ -135,6 +135,12 @@ def get_block_headers_range(self, start_height, end_height): "fill_pow_hash": False, }, ) + def get_hard_fork_info(self, height: int) -> FutureJSON: + return self.FutureJSON( + "rpc.hard_fork_info", + args={"height": height} + ) + def get_service_nodes(self) -> FutureJSON: return self.FutureJSON( diff --git a/src/staking/app.py b/src/staking/app.py index 08d8010..fb4fdb4 100644 --- a/src/staking/app.py +++ b/src/staking/app.py @@ -15,7 +15,7 @@ from ..oxen.rpc import OxenRPC from ..registration.read import DBReaderRegistrations from ..util.flask_utils import FlaskApp, json_response, FlaskAppConfig -from ..util.parse import Hex64Converter, EthConverter, eth_format, parse_bls_pubkey +from ..util.parse import Hex64Converter, EthConverter, eth_format, parse_bls_pubkey, parse_ed25519_pubkey from ..web3client.client import Web3Client from ..web3client.contracts_ws.service_node_contribution import ServiceNodeContribution @@ -180,6 +180,53 @@ def get_nodes_cached(): def route_get_nodes(): return json_res({"nodes": get_nodes_cached()}) + def get_latest_node_version_info_uncached(): + network_info, arbitrum_info = get_network_info_cached() + height = network_info.get("height", 0) + return app.rpc.get_hard_fork_info(height).get() + + def get_latest_node_version_info_cached(): + return app.cache.get("latest_node_version_info", getter=get_latest_node_version_info_uncached, ttl=600) + + @app.route("/hf_info") + def route_get_version_info(): + return json_res({ + "version_info": get_latest_node_version_info_cached(), + }) + + def get_contract_nodes_uncached(): + events_exit = app.db_reader.get_arbitrum_events_by_name("ServiceNodeExit") + sn_ids_exited = set([event.args["serviceNodeID"] for event in events_exit]) + new_seed_events = app.db_reader.get_arbitrum_events_by_name("NewSeededServiceNode") + new_sn_v2_events = app.db_reader.get_arbitrum_events_by_name("NewServiceNodeV2") + node_dict = {} + + for event in new_seed_events: + sn_id = event.args["serviceNodeID"] + node_dict[sn_id] = { + "bls": parse_bls_pubkey(event.args.get("blsPubkey")), + "ed25519": parse_ed25519_pubkey(event.args.get("ed25519Pubkey")), + "in": sn_id not in sn_ids_exited + } + + for event in new_sn_v2_events: + sn_id = event.args["serviceNodeID"] + node_dict[sn_id] = { + "bls": parse_bls_pubkey(event.args.get("pubkey")), + "ed25519": parse_ed25519_pubkey(event.args.get("serviceNode").get("serviceNodePubkey")), + "in": sn_id not in sn_ids_exited + } + + return node_dict + + def get_contract_nodes_cached(): + return app.cache.get("contract_nodes", getter=get_contract_nodes_uncached) + + @app.route("/contract_nodes") + def route_get_contract_nodes(): + return json_res({"nodes": get_contract_nodes_cached()}) + + # TODO: get rid of this def get_added_bls_keys(): events_exit = app.db_reader.get_arbitrum_events_by_name("ServiceNodeExit") sn_ids_exited = set([event.args["serviceNodeID"] for event in events_exit]) @@ -308,7 +355,7 @@ def get_contract_addresses_core(): def get_contribution_contracts_uncached(): contracts = app.db_reader.get_contribution_contracts() addresses = contracts.keys() - contract_events = app.db_reader.get_arbitrum_events_by_main_args(addresses) + contract_events = app.db_reader.get_arbitrum_events_by_main_args_desc(addresses) for event in contract_events: contracts[event.main_arg].events.append(event) return contracts