diff --git a/grapheneexchange/exchange.py b/grapheneexchange/exchange.py index 3f4ded99..e82ec262 100644 --- a/grapheneexchange/exchange.py +++ b/grapheneexchange/exchange.py @@ -2,6 +2,7 @@ from datetime import datetime import time import math +from grapheneextra.proposal import Proposal class ExampleConfig() : @@ -120,8 +121,20 @@ class Config(): account = "" wallet = None - def __init__(self, config, safe_mode=True) : - self.safe_mode = safe_mode + def __init__(self, config, **kwargs) : + # Defaults: + self.safe_mode = True + + #: Propose transactions (instead of broadcasting every order, we + # here propose every order in a single proposal + self.propose_only = False + self.propose_operations = [] + + if "safe_mode" in kwargs: + self.safe_mode = kwargs["safe_mode"] + if "propose_only" in kwargs: + self.propose_only = kwargs["propose_only"] + self.config = config super().__init__(config) @@ -733,7 +746,7 @@ def returnTradeHistory(self, currencyPair="all", limit=25): r.update({market : trades}) return r - def buy(self, currencyPair, rate, amount): + def buy(self, currencyPair, rate, amount, expiration=7 * 24 * 60 * 60, killfill=False): """ Places a buy order in a given market (buy ``quote``, sell ``base`` in market ``quote_base``). Required POST parameters are "currencyPair", "rate", and "amount". If successful, the @@ -742,6 +755,8 @@ def buy(self, currencyPair, rate, amount): :param str currencyPair: Return results for a particular market only (default: "all") :param float price: price denoted in ``base``/``quote`` :param number amount: Amount of ``quote`` to buy + :param number expiration: (optional) expiration time of the order in seconds (defaults to 7 days) + :param bool killfill: flag that indicates if the order shall be killed if it is not filled (defaults to False) Prices/Rates are denoted in 'base', i.e. the USD_BTS market is priced in BTS per USD. @@ -762,17 +777,21 @@ def buy(self, currencyPair, rate, amount): quote_symbol, base_symbol = currencyPair.split(self.market_separator) base = self.rpc.get_asset(base_symbol) quote = self.rpc.get_asset(quote_symbol) - # Check amount > 0 - return self.rpc.sell_asset(self.config.account, - '{:.{prec}f}'.format(amount * rate, prec=base["precision"]), - base_symbol, - '{:.{prec}f}'.format(amount, prec=quote["precision"]), - quote_symbol, - 7 * 24 * 60 * 60, - False, - not self.safe_mode) - - def sell(self, currencyPair, rate, amount): + transaction = self.rpc.sell_asset(self.config.account, + '{:.{prec}f}'.format(amount * rate, prec=base["precision"]), + base_symbol, + '{:.{prec}f}'.format(amount, prec=quote["precision"]), + quote_symbol, + expiration, + killfill, + not (self.safe_mode or self.propose_only)) + if self.propose_only: + [self.propose_operations.append(o) for o in transaction["operations"]] + return self.propose_operations + else: + return transaction + + def sell(self, currencyPair, rate, amount, expiration=7 * 24 * 60 * 60, killfill=False): """ Places a sell order in a given market (sell ``quote``, buy ``base`` in market ``quote_base``). Required POST parameters are "currencyPair", "rate", and "amount". If successful, the @@ -781,6 +800,8 @@ def sell(self, currencyPair, rate, amount): :param str currencyPair: Return results for a particular market only (default: "all") :param float price: price denoted in ``base``/``quote`` :param number amount: Amount of ``quote`` to sell + :param number expiration: (optional) expiration time of the order in seconds (defaults to 7 days) + :param bool killfill: flag that indicates if the order shall be killed if it is not filled (defaults to False) Prices/Rates are denoted in 'base', i.e. the USD_BTS market is priced in BTS per USD. @@ -801,14 +822,19 @@ def sell(self, currencyPair, rate, amount): quote_symbol, base_symbol = currencyPair.split(self.market_separator) base = self.rpc.get_asset(base_symbol) quote = self.rpc.get_asset(quote_symbol) - return self.rpc.sell_asset(self.config.account, - '{:.{prec}f}'.format(amount, prec=quote["precision"]), - quote_symbol, - '{:.{prec}f}'.format(amount * rate, prec=base["precision"]), - base_symbol, - 7 * 24 * 60 * 60, - False, - not self.safe_mode) + transaction = self.rpc.sell_asset(self.config.account, + '{:.{prec}f}'.format(amount, prec=quote["precision"]), + quote_symbol, + '{:.{prec}f}'.format(amount * rate, prec=base["precision"]), + base_symbol, + expiration, + killfill, + not (self.safe_mode or self.propose_only)) + if self.propose_only: + [self.propose_operations.append(o) for o in transaction["operations"]] + return self.propose_operations + else: + return transaction def close_debt_position(self, symbol): """ Close a debt position and reclaim the collateral @@ -919,11 +945,16 @@ def borrow(self, amount, symbol, collateral_ratio): (fundsNeeded, collateral_asset["symbol"], fundsHave, collateral_asset["symbol"])) # Borrow - return self.rpc.borrow_asset(self.config.account, - '{:.{prec}f}'.format(amount, prec=asset["precision"]), - symbol, - '{:.{prec}f}'.format(amount_of_collateral, prec=collateral_asset["precision"]), - not self.safe_mode) + transaction = self.rpc.borrow_asset(self.config.account, + '{:.{prec}f}'.format(amount, prec=asset["precision"]), + symbol, + '{:.{prec}f}'.format(amount_of_collateral, prec=collateral_asset["precision"]), + not (self.safe_mode or self.propose_only)) + if self.propose_only: + [self.propose_operations.append(o) for o in transaction["operations"]] + return self.propose_operations + else: + return transaction def cancel(self, orderNumber): """ Cancels an order you have placed in a given market. Requires @@ -935,16 +966,13 @@ def cancel(self, orderNumber): if self.safe_mode : print("Safe Mode enabled!") print("Please GrapheneExchange(config, safe_mode=False) to remove this and execute the transaction below") - # return self.rpc.cancel_order(orderNumber, not self.safe_mode) + transaction = self.rpc.cancel_order(orderNumber, not (self.safe_mode or self.propose_only)) - account = self.rpc.get_account(self.config.account) - op = self.rpc.get_prototype_operation("limit_order_cancel_operation") - op[1]["fee_paying_account"] = account["id"] - op[1]["order"] = orderNumber - buildHandle = self.rpc.begin_builder_transaction() - self.rpc.add_operation_to_builder_transaction(buildHandle, op) - self.rpc.set_fees_on_builder_transaction(buildHandle, "1.3.0") - return self.rpc.sign_builder_transaction(buildHandle, True) + if self.propose_only: + [self.propose_operations.append(o) for o in transaction["operations"]] + return self.propose_operations + else: + return transaction def withdraw(self, currency, amount, address): """ This Method makes no sense in a decentralized exchange @@ -1134,3 +1162,27 @@ def cancel_asks_out_of_range(self, market, price, tolerance): self.cancel(order["orderNumber"]) canceledOrders.append(order["orderNumber"]) return canceledOrders + + def propose_all(self, expiration=None, proposer=None): + """ If ``proposal_only`` is set True, this method needs to be + called to **actuctually** propose the operations on the + chain. + + :param time expiration: expiration time formated as ``%Y-%m-%dT%H:%M:%S`` (defaults to 24h) + :param string proposer: name of the account that pays the proposer fee + """ + if not proposer: + proposer = self.config.account + if not expiration: + expiration = datetime.utcfromtimestamp(time.time() + 60 * 60 * 24).strftime('%Y-%m-%dT%H:%M:%S') + account = self.rpc.get_account(proposer) + proposal = Proposal(self) + return proposal.propose_operations(self.propose_operations, + expiration, + account["id"], + broadcast=not self.safe_mode) + + def proposals_clear(self): + """ Clear stored proposals + """ + self.propose_operations = [] diff --git a/grapheneextra/proposal.py b/grapheneextra/proposal.py index 3f245b92..dc4f1454 100644 --- a/grapheneextra/proposal.py +++ b/grapheneextra/proposal.py @@ -1,6 +1,8 @@ from datetime import datetime import time +# from graphenebase.transactions import operations + class Proposal(object) : """ Manage Proposals @@ -70,3 +72,56 @@ def propose_transfer(self, proposer_account, from_account, to_account, self.client.rpc.propose_builder_transaction2(buildHandle, proposer["name"], exp_time, 0, False) self.client.rpc.set_fees_on_builder_transaction(buildHandle, asset["id"]) return self.client.rpc.sign_builder_transaction(buildHandle, broadcast) + + def propose_operations(self, ops, expiration, proposer_account, preview=0, broadcast=False): + """ Propose several operations + + :param Array ops: Array of operations + :param time expiration: Expiration time in format '%Y-%m-%dT%H:%M:%S' + :param proposer_account: Account name or id of the proposer (pays the proposal fee) + :param number preview: Preview period (in seconds) + :param bool broadcast: If true, broadcasts the transaction + :return: Signed transaction + :rtype: json + + Once a proposal has been signed, the corresponding + transaction hash can be obtained via: + + .. code-block:: python + + print(rpc.get_transaction_id(tx)) + """ + + proposer = self.client.rpc.get_account(proposer_account) + buildHandle = self.client.rpc.begin_builder_transaction() + for op in ops: + self.client.rpc.add_operation_to_builder_transaction(buildHandle, op) + self.client.rpc.set_fees_on_builder_transaction(buildHandle, "1.3.0") + self.client.rpc.propose_builder_transaction2(buildHandle, proposer["name"], expiration, preview, False) + self.client.rpc.set_fees_on_builder_transaction(buildHandle, "1.3.0") + return self.client.rpc.sign_builder_transaction(buildHandle, broadcast) + +# ## Alternative implementation building the transactions +# ## manually. Not yet working though +# op = self.client.rpc.get_prototype_operation("proposal_create_operation") +# for o in ops : +# op[1]["proposed_ops"].append(o) +# op[1]["expiration_time"] = expiration +# op[1]["fee_paying_account"] = payee_id +# op[1]["fee"] = self.get_operations_fee(op, "1.3.0") +# buildHandle = self.client.rpc.begin_builder_transaction() +# from pprint import pprint +# pprint(op) +# self.client.rpc.add_operation_to_builder_transaction(buildHandle, op) +# # print(self.client.rpc.preview_builder_transaction(buildHandle)) +# return self.client.rpc.sign_builder_transaction(buildHandle, broadcast) + +# def get_operations_fee(self, op, asset_id): +# global_parameters = self.client.rpc.get_object("2.0.0")[0]["parameters"]["current_fees"] +# parameters = global_parameters["parameters"] +# scale = global_parameters["scale"] / 1e4 +# opID = op[0] +# assert asset_id == "1.3.0", "asset_id has to be '1.3.0'" +# # FIXME limition to "fee"-only! Need to evaluate every other as well +# return {"amount": parameters[opID][1]["fee"], +# "asset_id": asset_id} diff --git a/scripts/exchange-simpleticker-stats/main.py b/scripts/exchange-simpleticker-stats/main.py index ea2c08a1..0cbb3853 100644 --- a/scripts/exchange-simpleticker-stats/main.py +++ b/scripts/exchange-simpleticker-stats/main.py @@ -7,7 +7,8 @@ class Config(): wallet_port = 8092 wallet_user = "" wallet_password = "" - witness_url = "ws://10.0.0.16:8090/" + witness_url = "wss://bitshares.openledger.info/ws/" + # witness_url = "ws://10.0.0.16:8090/" # witness_url = "ws://testnet.bitshares.eu/ws" witness_user = "" witness_password = ""