diff --git a/.env_example b/.env_example index a019bf0..b84b77b 100644 --- a/.env_example +++ b/.env_example @@ -8,6 +8,7 @@ WPFP_NETWORK_NAME="chain.network" #WPFP_NETWORK_TIMEOUT_SECS=60 ## Web3 params +WPFP_WEB3_ADDRESS="0x9999999d139bdBFbF25923ba39F63bBFc7593400" #WPFP_WEB3_FINALIZATION_SECS=60 WPFP_WEB3_FROM="" #WPFP_WEB3_GAS=300_000 diff --git a/.github/actions/docker-build/action.yml b/.github/actions/docker-build/action.yml index ad8f382..946b81f 100644 --- a/.github/actions/docker-build/action.yml +++ b/.github/actions/docker-build/action.yml @@ -9,4 +9,5 @@ runs: using: "composite" steps: - name: Build the image itself + shell: bash run: docker build -t ${{ inputs.tag }} -f docker/Dockerfile . diff --git a/package.json b/package.json index 6789287..3a8c0df 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "witnet-price-feeds-poller", - "version": "2.0.0", + "version": "2.1.0", "description": "", "main": "", "repository": "https://github.com/witnet/witnet-price-feeds-poller", @@ -8,7 +8,7 @@ "private": false, "scripts": {}, "dependencies": { - "witnet-requests": "~0.9.2" + "witnet-requests": "~0.9.11" }, "devDependencies": { "@babel/cli": "^7.5.5", diff --git a/price_feeds_config.json b/price_feeds_config.json index 62d2aac..c51154f 100644 --- a/price_feeds_config.json +++ b/price_feeds_config.json @@ -310,6 +310,100 @@ } } }, + "elastos": { + "name": "Elastos", + "networks": { + "elastos.mainnet": { + "mainnet": true, + "address": "0x9999999d139bdBFbF25923ba39F63bBFc7593400", + "blockExplorer": "https://esc.elastos.io/address/{address}", + "color": "#66ff00", + "name": "Elastos Mainnet", + "pollingPeriod": 120000, + "feeds": { + "Price-ELA/USDT-6": { + "label": "₮", + "deviationPercentage": 3.5, + "maxSecsBetweenUpdates": 86400, + "minSecsBetweenUpdates": 900 + } + } + }, + "elastos.testnet": { + "address": "0x4874cb1732eE1167A006E0Ab047D940ACF04D771", + "blockExplorer": "https://esc-testnet.elastos.io/address/{address}", + "color": "#66ff00", + "name": "Elastos Testnet", + "pollingPeriod": 120000, + "feeds": { + "Price-BNB/USD-6": { + "isRouted": true, + "label": "$" + }, + "Price-BNB/USDT-6": { + "label": "₮", + "deviationPercentage": 3.5, + "maxSecsBetweenUpdates": 900, + "minSecsBetweenUpdates": 900 + }, + "Price-BTC/USD-6": { + "label": "$", + "deviationPercentage": 3.5, + "maxSecsBetweenUpdates": 86400, + "minSecsBetweenUpdates": 900 + }, + "Price-BUSD/USD-6": { + "isRouted": true, + "label": "$" + }, + "Price-BUSD/USDT-6": { + "label": "₮", + "deviationPercentage": 3.5, + "maxSecsBetweenUpdates": 86400, + "minSecsBetweenUpdates": 900 + }, + "Price-ELA/USD-6": { + "isRouted": true, + "label": "$" + }, + "Price-ELA/USDT-6": { + "label": "₮", + "deviationPercentage": 3.5, + "maxSecsBetweenUpdates": 86400, + "minSecsBetweenUpdates": 900 + }, + "Price-ETH/USD-6": { + "label": "$", + "deviationPercentage": 3.5, + "maxSecsBetweenUpdates": 86400, + "minSecsBetweenUpdates": 900 + }, + "Price-HT/USD-6": { + "isRouted": true, + "label": "$" + }, + "Price-HT/USDT-6": { + "label": "₮", + "deviationPercentage": 3.5, + "maxSecsBetweenUpdates": 86400, + "minSecsBetweenUpdates": 900 + }, + "Price-USDC/USD-6": { + "label": "$", + "deviationPercentage": 0.1, + "maxSecsBetweenUpdates": 86400, + "minSecsBetweenUpdates": 900 + }, + "Price-USDT/USD-6": { + "label": "$", + "deviationPercentage": 0.1, + "maxSecsBetweenUpdates": 86400, + "minSecsBetweenUpdates": 900 + } + } + } + } + }, "ethereum": { "networks": { "ethereum.goerli": { diff --git a/price_feeds_poller.py b/price_feeds_poller.py index 74319ca..2a97ac8 100644 --- a/price_feeds_poller.py +++ b/price_feeds_poller.py @@ -4,6 +4,7 @@ import contextlib import datetime import os +import re import subprocess import sys import time @@ -93,7 +94,7 @@ def handle_requestUpdate( return [ 0 ] except Exception as ex: - print(f" xx Transaction rejected: {ex}") + print(f" xx Transaction rejected: {unscape(ex)}") return [ 0 ] # Check if transaction was succesful @@ -121,6 +122,7 @@ def handle_loop( feeds_config_file_path, network_name, web3_symbol, + web3_address, web3_from, web3_gas, web3_gas_price, @@ -132,9 +134,9 @@ def handle_loop( witnet_toolkit_timeout_secs ): config = load_price_feeds_config(feeds_config_file_path, network_name) - feeds = feeds_contract(w3, config['address']) + feeds = feeds_contract(w3, web3_address) if feeds.address is None: - print("Fatal: no WitnetPriceFeeds address") + print("Fatal: invalid WitnetPriceFeeds address") exit(1) print(f"\nUsing WitnetBytecodes at {feeds.functions.registry().call()}") @@ -181,6 +183,8 @@ def handle_loop( "revert": 0, "auto_disabled": False, "lastRevertedTx": "", + "lastUpdateFailed": False, + "lastUpdateFailedTimestamp": int(time.time()), "fees": [], "secs": [] }) @@ -189,7 +193,8 @@ def handle_loop( print(f" => Solver addr : {solverAddr}") else: print(f" => RAD hash : {rad_hash}") - print(f" => Deviation : {deviation} %") + print(f" => Deviation : {deviation} %") + print(f" => Bytecode : {bytecode.hex()}") if heartbeat > 0: print(f" => Heartbeat : {heartbeat} seconds") if cooldown > 0: @@ -203,10 +208,10 @@ def handle_loop( break except Exception as ex: if attempt < 4: - print(f" >< Attempt #{attempt}: {ex}") + print(f" >< Attempt #{attempt}: {unscape(ex)}") continue else: - print(f" >< Skipped: Exception: {ex}") + print(f" >< Skipped: Exception: {unscape(ex)}") break if len(caption) > captionMaxLength: @@ -226,18 +231,23 @@ def handle_loop( print() loop_ts = int(time.time()) - balance = w3.eth.getBalance(web3_from) - time_left_secs = time_to_die_secs(balance, pfs) - timer_out = (loop_ts - low_balance_ts) >= 900 - if time_left_secs > 0: - if time_left_secs <= 86400 * 3 and timer_out: - # start warning every 900 seconds if estimated time before draiing funds is less than 3 days - low_balance_ts = loop_ts - print(f"LOW FUNDS !!!: estimated {round(time_left_secs / 3600, 2)} hours before running out of funds") - else: - print(f"Time-To-Die: {round(time_left_secs / 3600, 2)} hours") - - latest_prices = feeds.functions.latestPrices(ids).call() + try: + balance = w3.eth.getBalance(web3_from) + time_left_secs = time_to_die_secs(balance, pfs) + timer_out = (loop_ts - low_balance_ts) >= 900 + if time_left_secs > 0: + if time_left_secs <= 86400 * 3 and timer_out: + # start warning every 900 seconds if estimated time before draiing funds is less than 3 days + low_balance_ts = loop_ts + print(f"LOW FUNDS !!!: estimated {round(time_left_secs / 3600, 2)} hours before running out of funds") + else: + print(f"Time-To-Die: {round(time_left_secs / 3600, 2)} hours") + + latest_prices = feeds.functions.latestPrices(ids).call() + except Exception as ex: + print(f"Main loop exception: {unscape(ex)}") + time.sleep(1) + continue for index in range(len(ids)): @@ -272,12 +282,14 @@ def handle_loop( pf["auto_disabled"] = False pf["isRouted"] = False pf["lastRevertedTx"] = "" + pf["lastUpdateFailed"] = False + pf["lastUpdateFailedTimestamp"] = int(time.time()) pf["pendingUpdate"] = False pf["reverts"] = 0 break except Exception as ex: if attempt < 4: - print(f"{caption} >< refreshing contract state attempt #{attempt}: {ex}") + print(f"{caption} >< refreshing contract state attempt #{attempt}: {unscape(ex)}") time.sleep(1) else: raise ex @@ -287,7 +299,8 @@ def handle_loop( pf["bytecode"] = "" pf["isRouted"] = False pf["lastRevertedTx"] = "" - pf["pendingUpdate"] = False + pf["pendingUpdate"] = False + pf["lastUpdateFailed"] = False if pf["auto_disabled"]: # Skip if this pricefeed is disabled @@ -306,22 +319,22 @@ def handle_loop( # On routed pfs: just check for spontaneous price updates if pf["isRouted"] == True: if latest_price[1] > pf["latestTimestamp"]: - print(f"{caption} <> routed price updated to {latest_price[0] / 10 ** int(caption.strip().split('-')[2])} {config['feeds'][caption]['label']}") + print(f"{caption} <> routed price updated to {latest_price[0] / 10 ** int(caption.strip().split('-')[2])} {config['feeds'][caption.strip()]['label']}") pf["latestTimestamp"] = latest_price[1] - pf["latestUpdateQueryId"] = latest_update_query_id else: - print(f"{caption} .. awaiting eventual routed update.") + print(f"{caption} .. expecting eventual routed update.") continue # If still waiting for an update... if pf["pendingUpdate"] == True: # A new valid result has just been detected: - if status == 2 and latest_price[1] > pf["latestTimestamp"]: - pf["pendingUpdate"] = False + if status == 2 and latest_price[1] >= pf["latestTimestamp"]: + pf["lastUpdateFailed"] = False pf["latestPrice"] = latest_price[0] elapsed_secs = latest_price[1] - pf["latestTimestamp"] pf["latestTimestamp"] = latest_price[1] + pf["pendingUpdate"] = False print(f"{caption} << drTxHash: {latest_price[2].hex()} => updated to {latest_price[0] / 10 ** int(caption.strip().split('-')[2])} {config['feeds'][caption.strip()]['label']} (after {elapsed_secs} secs)") # An invalid result has just been detected: @@ -329,6 +342,8 @@ def handle_loop( pf["pendingUpdate"] = False latest_response = feeds.functions.latestUpdateResponse(id).call() latest_error = feeds.functions.latestUpdateResultError(id).call() + pf["lastUpdateFailed"] = True + pf["lastUpdateFailedTimestamp"] = current_ts print(f"{caption} >< drTxHash: {latest_response[2].hex()} => \"{str(latest_error[1])}\"") else: @@ -342,7 +357,7 @@ def handle_loop( # If no update is pending: elif pf["isRouted"] == False: - if elapsed_secs >= pf["cooldown"] - total_finalization_secs: + if pf["lastUpdateFailed"] == False or current_ts >= pf["lastUpdateFailedTimestamp"] + pf["cooldown"] - total_finalization_secs: last_price = pf["latestPrice"] deviation = 0 @@ -363,7 +378,7 @@ def handle_loop( ) except Exception as ex: # ...if dry run fails, assume 0 deviation as to, at least, guarantee the heartbeat periodicity is met - print(f"{caption} >< Dry-run failed:", ex) + print(f"{caption} >< Dry-run failed:", unscape(ex)) continue deviation = round(100 * ((next_price - last_price) / last_price), 2) @@ -376,7 +391,7 @@ def handle_loop( reason = f"deviation is greater than {pf['deviation']} %" else: - print(f"{caption} .. awaiting heartbeat condition, for another {pf['heartbeat'] - elapsed_secs} secs") + print(f"{caption} .. expecting heartbeat condition for another {pf['heartbeat'] - elapsed_secs} secs") continue print(f"{caption} >> Requesting update after {elapsed_secs} seconds because {reason}:") @@ -421,13 +436,13 @@ def handle_loop( del pf["secs"][0] else: - secs_until_next_check = pf['cooldown'] - elapsed_secs - total_finalization_secs + secs_until_next_check = pf['cooldown'] - current_ts + pf["lastUpdateFailedTimestamp"] - total_finalization_secs if secs_until_next_check > 0: print(f"{caption} .. resting for another {secs_until_next_check} secs before next triggering check") # Capture exceptions while reading state from contract except Exception as ex: - print(f"{caption} .. Exception when getting state from {feeds.address}: {ex}") + print(f"{caption} .. Exception when getting state from {feeds.address}: {unscape(ex)}") # Sleep just enough between loops preemptive_secs = loop_interval_secs - int(time.time()) + loop_ts @@ -444,6 +459,7 @@ def main(args): network_timeout_secs = int(os.getenv('WPFP_NETWORK_TIMEOUT_SECS') or 60) # Read web3 parameters from environment: + web3_address = os.getenv('WPFP_WEB3_ADDRESS') web3_finalization_secs = int(os.getenv('WPFP_WEB3_FINALIZATION_SECS') or 60) web3_from = os.getenv('WPFP_WEB3_FROM') web3_gas = int(os.getenv('WPFP_WEB3_GAS')) if os.getenv('WPFP_WEB3_GAS') else None @@ -529,7 +545,7 @@ def main(args): print(f"Connected to '{network_name}' at block #{current_block} via {web3_provider}") except Exception as ex: - print(f"Fatal: connection failed to {web3_provider}: {ex}") + print(f"Fatal: connection failed to {web3_provider}: {unscape(ex)}") exit(1) # Log Web3 client version @@ -546,6 +562,7 @@ def main(args): config_path, network_name, web3_symbol, + web3_address, web3_from, web3_gas, web3_gas_price, @@ -591,7 +608,7 @@ def dry_run_request(bytecode, timeout_secs): def log_exception_state(addr, reason): # log the error and wait 1 second before next iteration - print(f"Exception while getting state from contract {addr}:\n{reason}") + print(f"Exception while getting state from {addr}:\n{reason}") time.sleep(1) def log_master_balance(csv_filename, addr, balance, txhash): @@ -632,6 +649,32 @@ def time_to_die_secs(balance, pfs): else: return 0 +def unscape(ex): + src = str(ex) + slashes = 0 # count backslashes + dst = "" + for loc in range(0, len(src)): + char = src[loc] + if char == "\\": + slashes += 1 + if slashes == 2: + # remove double backslashes + slashes = 0 + elif slashes == 0: + # normal char + dst += char + else: # slashes == 1 + if char == '"': + # double-quotes + dst += char + elif char == "'": + # remove single-quote + dst += char + else: + dst += "\\" + char # keep backslash-escapes like \n or \t + slashes = 0 + return dst + if __name__ == '__main__': parser = argparse.ArgumentParser(description='Connect to an Ethereum provider.') parser.add_argument('--json_path', dest='json_path', action='store', required=False, @@ -644,4 +687,5 @@ def time_to_die_secs(balance, pfs): help='provide the CSV file in which master address balance will be logged after sending every new transaction') args = parser.parse_args() + main(args)