diff --git a/docs/installation.rst b/docs/installation.rst index 63a651d2..1970c421 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -16,8 +16,9 @@ This library requires `autobahn` and requests to be installed: .. code-block:: bash - easy_install-3.4 install autobahn - easy_install-3.4 install requests + pip3 install autobahn --user + pip3 install requests --user + pip3 install crypto --user The graphene library itself can be installed by issuing: diff --git a/docs/pricefeed.rst b/docs/pricefeed.rst index 79b478ca..5c4c3ff6 100644 --- a/docs/pricefeed.rst +++ b/docs/pricefeed.rst @@ -14,7 +14,7 @@ for the library). apt-get install python3 python3-pip python-autobahn pip3 install requests --upgrade - pip3 install numpy prettytable autobahn + pip3 install numpy prettytable autobahn crypto Cli Wallet ########## diff --git a/grapheneapi/graphenews.py b/grapheneapi/graphenews.py index efcc24b6..08ab889e 100644 --- a/grapheneapi/graphenews.py +++ b/grapheneapi/graphenews.py @@ -3,6 +3,7 @@ import json import asyncio from functools import partial +import ssl try : from autobahn.asyncio.websocket import WebSocketClientFactory @@ -15,17 +16,23 @@ class GrapheneWebsocket(GrapheneAPI): """ Constructor takes host, port, and login credentials """ def __init__(self, host, port, username, password, proto=GrapheneWebsocketProtocol) : + ## initialize API (overwrites __getattr__() + super().__init__(host, port, username, password) + self.username = username self.password = password - self.host = host + hostparts = host.split("/") + if len(hostparts)==1 : + self.host = hostparts[0] + self.ssl = False + else : + self.host = hostparts[2] + self.ssl = (hostparts[0] == "wss:") self.port = port self.proto = proto self.proto.username = self.username self.proto.password = self.password - ## initialize API (overwrites __getattr__() - super().__init__(host, port, username, password) - """ Get an object_id by name """ def object_id(self, name, instance=0) : @@ -92,7 +99,13 @@ def getObject(self, oid) : """ def run_forever(self) : loop = asyncio.get_event_loop() - coro = loop.create_connection(self.factory, self.host, self.port) + if self.ssl : + context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + coro = loop.create_connection(self.factory, self.host, self.port, ssl=context) + else : + coro = loop.create_connection(self.factory, self.host, self.port, ssl=self.ssl) loop.run_until_complete(coro) try : loop.run_forever() diff --git a/scripts/monitor-refcode/README.md b/scripts/monitor-refcode/README.md new file mode 100644 index 00000000..a4f1ba05 --- /dev/null +++ b/scripts/monitor-refcode/README.md @@ -0,0 +1,13 @@ +# Monitor Faucet + +This script monitors deposits and searches for referral codes of length 8 in the memo. +After that, it makes a API call to the faucet to reserve these funds for later use. + +## Configuration + + cp config-example.py config.py + # edit config.py + +## Run + + python3 monitor.py diff --git a/scripts/monitor-refcode/config-example.py b/scripts/monitor-refcode/config-example.py new file mode 100644 index 00000000..1dd03ef1 --- /dev/null +++ b/scripts/monitor-refcode/config-example.py @@ -0,0 +1,17 @@ +""" RPC connection settings """ +host = "" +port = 443 +user = "" +password = "" + +""" Account id to monitor """ +accountID = "2.6.XXXX" + +""" Memo Key of the receiving account """ +memo_wif_key = "" + +""" Last operation ID that you have registered in your backend """ +last_op = "1.11.0" + +""" Faucet settings """ +faucet_url = "http://localhost:3000/api/v1/referral_codes", # URL to the faucet diff --git a/scripts/monitor-refcode/monitor.py b/scripts/monitor-refcode/monitor.py new file mode 100644 index 00000000..ed3ea40f --- /dev/null +++ b/scripts/monitor-refcode/monitor.py @@ -0,0 +1,105 @@ +import sys +import json +from grapheneapi import GrapheneWebsocket, GrapheneWebsocketProtocol +from graphenebase import Memo, PrivateKey, PublicKey +import config +import re + +try : + import requests +except ImportError: + raise ImportError( "Missing dependency: python-requests" ) + +""" PubKey Prefix + Productive network: BTS + Testnetwork: GPH """ +#prefix = "GPH" +prefix = "BTS" + +""" Callback on event + This function will be triggered on a notification of the witness. + If you subsribe (see below) to 2.6.*, the witness node will notify you of + any chances regarding your account_balance """ +class GrapheneMonitor(GrapheneWebsocketProtocol) : + last_op = "1.11.0" + account_id = "1" + def __init__(self) : + super().__init__() + + def printJson(self,d) : print(json.dumps(d,indent=4)) + + def onAccountUpdate(self, data) : + # Get Operation ID that modifies our balance + opID = api.getObject(data["most_recent_op"])["operation_id"] + self.wsexec([self.api_ids["history"], "get_account_history", [self.account_id, self.last_op, 100, "1.11.0"]], self.process_operations) + self.last_op = opID + + def process_operations(self, operations) : + for operation in operations[::-1] : + opID = operation["id"] + block = operation["block_num"] + op = operation["op"][1] + + # Get assets involved in Fee and Transfer + fee_asset = api.getObject(op["fee"]["asset_id"]) + amount_asset = api.getObject(op["amount"]["asset_id"]) + + # Amounts for fee and transfer + fee_amount = op["fee"]["amount"] / float(10**int(fee_asset["precision"])) + amount_amount= op["amount"]["amount"] / float(10**int(amount_asset["precision"])) + + # Get accounts involved + from_account = api.getObject(op["from"]) + to_account = api.getObject(op["to"]) + + # Decode the memo + memo = op["memo"] + try : # if possible + privkey = PrivateKey(config.memo_wif_key) + pubkey = PublicKey(memo["from"], prefix=prefix) + memomsg = Memo.decode_memo(privkey, pubkey, memo["nonce"], memo["message"]) + except Exception as e: # if not possible + memomsg = "--cannot decode-- (%s)" % str(e) + + # Print out + print("last_op: %s | block:%s | from %s -> to: %s | fee: %f %s | amount: %f %s | memo: %s" % ( + opID, block, + from_account["name"], to_account["name"], + fee_amount, fee_asset["symbol"], + amount_amount, amount_asset["symbol"], + memomsg)) + + # Parse the memo + pattern = re.compile('[a-zA-Z0-9]{8}') + searchResults = pattern.search(memomsg) + ref_code = searchResults.group(0) + + # Request to Faucet + headers = {'content-type': 'application/json'} + query = { + "refcode[code]" : ref_code, + "refcode[account]" : from_account["name"], + "refcode[asset_symbol]" : amount_asset["symbol"], + "refcode[asset_amount]" : amount_amount, + } + response = requests.get(faucet_url, + data=json.dumps(query), + headers=headers) + +if __name__ == '__main__': + ## Monitor definitions + protocol = GrapheneMonitor + protocol.last_op = config.last_op ## last operation logged + protocol.account_id = "1.2.%s" % config.accountID.split(".")[2] ## account to monitor + + ## Open Up Graphene Websocket API + api = GrapheneWebsocket(config.host, config.port, config.user, config.password, protocol) + + print(api) + + ## Set Callback for object changes + api.setObjectCallbacks({config.accountID : protocol.onAccountUpdate}) + + ## Run the Websocket connection continuously + api.connect() + api.run_forever() diff --git a/scripts/pricefeeds/config-example.py b/scripts/pricefeeds/config-example.py index d2c35505..f0c31ceb 100644 --- a/scripts/pricefeeds/config-example.py +++ b/scripts/pricefeeds/config-example.py @@ -7,6 +7,8 @@ passwd = "" unlock = "" +ask_confirmation = True + ################################################################################ ## Delegate Feed Publish Parameters ################################################################################ @@ -16,17 +18,18 @@ ################################################################################ ## Fine tuning ################################################################################ -discount = 0.995 -core_exchange_factor = 1.05 # 5% surplus if paying fees in bitassets - -minValidAssetPriceInBTC = 0.00001 -change_min = 0.5 +discount = 0.995 # discoun for shorts +core_exchange_factor = 0.95 # 5% discount if paying fees in bitassets +maintenance_collateral_ratio = 1750 # Call when collateral only pays off 175% the debt +maximum_short_squeeze_ratio = 1100 # Stop calling when collateral only pays off 110% of the debt +change_min = 0.5 # Percentage of price change to force an update +minValidAssetPriceInBTC = 0.00001# minimum valid price for BTS in BTC ## Enable exchanges enable_yunbi = True enable_btc38 = True enable_bter = False -enable_poloniex = True +enable_poloniex = True enable_bittrex = True enable_btcavg = True diff --git a/scripts/pricefeeds/pricefeeds.py b/scripts/pricefeeds/pricefeeds.py index 2daef813..aa66c25d 100755 --- a/scripts/pricefeeds/pricefeeds.py +++ b/scripts/pricefeeds/pricefeeds.py @@ -66,7 +66,7 @@ def publish_rule(): for asset in asset_list_publish : ## Define REAL_PRICE realPrice = statistics.median( price["BTS"][asset] ) - ## Rules + ## Rules if (datetime.utcnow()-lastUpdate[asset]).total_seconds() > config.maxAgeFeedInSeconds : print("Feeds for %s too old! Force updating!" % asset) return True @@ -205,7 +205,7 @@ def fetch_from_bittrex(): mObj = re.match( 'BTC-(.*)', coin[ "MarketName" ] ) altcoin = mObj.group(1) coinmap=altcoin - if altcoin=="BTSX" : + if altcoin=="BTSX" : coinmap=core_symbol if float(coin["Last"]) > config.minValidAssetPriceInBTC: price["BTC"][ coinmap ].append(float(coin["Last"])) @@ -257,6 +257,7 @@ def fetch_bitcoinaverage(): def fetch_from_wallet(rpc): print("Fetching data from wallet...") ## Get my Witness + global myWitness myWitness = rpc.get_witness(config.delegate_name) witnessId = myWitness["witness_account"] @@ -305,7 +306,7 @@ def fetch_from_wallet(rpc): ## ---------------------------------------------------------------------------- ## Send the new feeds! ## ---------------------------------------------------------------------------- -def update_feed(rpc, myassets): +def update_feed(rpc, feeds): wallet_was_unlocked = False if rpc.is_locked() : @@ -314,8 +315,22 @@ def update_feed(rpc, myassets): ret = rpc.unlock(config.unlock) print("publishing feeds for delegate: %s"%config.delegate_name) - for a in myassets : - result = rpc.publish_asset_feed(config.delegate_name, a[0], a[1], True) # True: sign+broadcast + handle = rpc.begin_builder_transaction(); + for feed in feeds : + # id 19 corresponds to price feed update operation + rpc.add_operation_to_builder_transaction(handle, + [19, { + "asset_id" : feed[0], + "feed" : feed[1], + "publisher" : myWitness["witness_account"], + }]) + + # Set fee + rpc.set_fees_on_builder_transaction(handle, "1.3.0") + + # Signing and Broadcast + signedTx = rpc.sign_builder_transaction(handle, True) + print(json.dumps(signedTx,indent=4)); if wallet_was_unlocked : print( "Relocking wallet" ) @@ -396,7 +411,7 @@ def print_stats() : median_exchanges = statistics.median(prices_from_exchanges) if cur_feed == 0 : change_my = -1 else : change_my = ((weighted_external_price - cur_feed)/cur_feed)*100 - if price_from_blockchain == 0 : + if price_from_blockchain == 0 : change_blockchain = -1 price_from_blockchain = -1 else : @@ -469,7 +484,7 @@ def bts_yahoo_map(asset) : for base in _bases + [core_symbol]: price[base] = {} volume[base] = {} - for asset in _all_bts_assets + [core_symbol]: + for asset in _all_bts_assets + [core_symbol]: price[base][asset] = [] volume[base][asset] = [] @@ -492,7 +507,7 @@ def bts_yahoo_map(asset) : if config.enable_poloniex : mythreads["poloniex"] = threading.Thread(target = fetch_from_poloniex) if config.enable_bittrex : mythreads["bittrex"] = threading.Thread(target = fetch_from_bittrex) if config.enable_btcavg : mythreads["btcavg"] = threading.Thread(target = fetch_bitcoinaverage) - + print("[Starting Threads]: ", end="",flush=True) for t in mythreads : print("(%s)"%t, end="",flush=True) @@ -505,49 +520,63 @@ def bts_yahoo_map(asset) : derive_prices() ## Only publish given feeds ################################################## - asset_list_final = [] + price_feeds = [] for asset in asset_list_publish : if len(price[core_symbol][asset]) > 0 : if price_in_bts_weighted[asset] > 0.0: quote_precision = assets[asset]["precision"] base_precision = assets["1.3.0"]["precision"] ## core asset - core_price = price_in_bts_weighted[asset] * 10**(quote_precision-base_precision) - core_price = fractions.Fraction.from_float(core_price).limit_denominator(100000) - denominator = core_price.denominator - numerator = core_price.numerator + core_price = price_in_bts_weighted[asset] * 10**(quote_precision-base_precision) + core_price = fractions.Fraction.from_float(core_price).limit_denominator(100000) + denominator = core_price.denominator + numerator = core_price.numerator assert assets[asset]["symbol"] is not asset + #if denominator == 0 or numerator == 0 or int(denominator * config.core_exchange_factor) : + # continue + price_feed = { "settlement_price": { "quote": { "asset_id": "1.3.0", "amount": denominator - }, + }, "base": { - "asset_id": assets[asset]["id"], + "asset_id": assets[asset]["id"], "amount": numerator } - }, + }, + "maintenance_collateral_ratio": config.maintenance_collateral_ratio, + "maximum_short_squeeze_ratio": config.maximum_short_squeeze_ratio, "core_exchange_rate": { "quote": { "asset_id": "1.3.0", "amount": int(denominator * config.core_exchange_factor) - }, + }, "base": { - "asset_id": assets[asset]["id"], + "asset_id": assets[asset]["id"], "amount": numerator } } } - asset_list_final.append([ asset, price_feed ]) - + price_feeds.append([assets[asset]["id"], price_feed]) + ## Print some stats ########################################################## print_stats() - + ## Check publish rules and publich feeds ##################################### - if publish_rule() and rpc._confirm("Are you SURE you would like to publish this feed?") : - print("Update required! Forcing now!") - update_feed(rpc,asset_list_final) + publish = False + if publish_rule() : + + if config.ask_confirmation : + if rpc._confirm("Are you SURE you would like to publish this feed?") : + publish = True + else : + publish = True + + if publish : + print("Update required! Forcing now!") + update_feed(rpc,price_feeds) else : print("no update required")