Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • cebash/sakia
  • santiago/sakia
  • jonas/sakia
3 results
Show changes
Showing
with 2710 additions and 0 deletions
import locale
import asyncio
import logging
import signal
import sys
import traceback
from PyQt5.QtCore import Qt, QObject, QCoreApplication
from PyQt5.QtWidgets import QApplication, QMessageBox, QDialog, QPushButton, QLabel
from duniterpy.api.errors import DuniterError
from sakia.constants import GITLAB_NEW_ISSUE_PAGE_URL
from sakia.helpers import single_instance_lock, cleanup_lock
from quamash import QSelectorEventLoop
from sakia.errors import NoPeerAvailable
from sakia.app import Application
from sakia.gui.dialogs.connection_cfg.controller import ConnectionConfigController
from sakia.gui.main_window.controller import MainWindowController
from sakia.gui.preferences import PreferencesDialog
from sakia.gui.widgets import QAsyncMessageBox
from sakia.gui.dialogs.startup_uic import Ui_StartupDialog
from sakia import __version__
class StartupDialog(QDialog, Ui_StartupDialog):
def __init__(self):
super().__init__()
self.setupUi(self)
self.setWindowTitle("Sakia {version}".format(version=__version__))
def closeEvent(self, event):
"""
Overide close event to exit application if dialog is closed
:param QDialogEvent event:
:return:
"""
cancel_connection()
def exit_exception_handler(loop, context):
"""
An exception handler which prints only on debug (used when exiting)
:param loop: the asyncio loop
:param context: the exception context
"""
logging.debug("Exception handler executing")
message = context.get("message")
if not message:
message = "Unhandled exception in event loop"
try:
exception = context["exception"]
except KeyError:
exc_info = False
else:
exc_info = (type(exception), exception, exception.__traceback__)
logging.debug(
"An unhandled exception occurred: {0}".format(message), exc_info=exc_info
)
def async_exception_handler(loop, context):
"""
An exception handler which exits the program if the exception
was not catch
:param loop: the asyncio loop
:param context: the exception context
"""
logging.debug("Exception handler executing")
message = context.get("message")
if not message:
message = "Unhandled exception in event loop"
try:
exception = context["exception"]
except KeyError:
exc_info = False
else:
exc_info = (type(exception), exception, exception.__traceback__)
log_lines = [message]
for key in [k for k in sorted(context) if k not in {"message", "exception"}]:
log_lines.append("{}: {!r}".format(key, context[key]))
logging.error("\n".join(log_lines), exc_info=exc_info)
for line in log_lines:
for ignored in (
"feed_appdata",
"do_handshake",
"Unclosed",
"socket.gaierror",
"[Errno 110]",
):
if ignored in line:
return
if exc_info:
for line in traceback.format_exception(*exc_info):
for ignored in (
"feed_appdata",
"do_handshake",
"Unclosed",
"socket.gaierror",
"[Errno 110]",
):
if ignored in line:
return
exception_message(log_lines, exc_info)
def exception_handler(*exc_info):
logging.error("An unhandled exception occured", exc_info=exc_info)
exception_message(["An unhandled exception occured"], exc_info)
def exception_message(log_lines, exc_info):
stacktrace = traceback.format_exception(*exc_info) if exc_info else ""
message = """
{log_lines}
----
{stacktrace}
""".format(
log_lines="\n".join(log_lines), stacktrace="\n".join(stacktrace)
)
mb = QMessageBox(
QMessageBox.Critical,
"Critical error",
"""A critical error occured. Select the details to display it.
Please report it to <a href='{}'>the developers Gitlab</a>""".format(
GITLAB_NEW_ISSUE_PAGE_URL
),
QMessageBox.Ok,
QApplication.activeWindow(),
)
mb.setDetailedText(message)
mb.setTextFormat(Qt.RichText)
mb.exec()
def cancel_connection(button=None):
"""
Exit application
:param QMessageBox button: Clicked button or None if close event
:return:
"""
print("Cancel connection! Exited.")
sys.exit(0)
def main():
# activate ctrl-c interrupt
signal.signal(signal.SIGINT, signal.SIG_DFL)
sakia = QApplication(sys.argv)
sys.excepthook = exception_handler
# sakia.setStyle('Fusion')
loop = QSelectorEventLoop(sakia)
loop.set_exception_handler(async_exception_handler)
# loop.set_debug(True)
asyncio.set_event_loop(loop)
# Fix quamash https://github.com/harvimt/quamash/issues/123
asyncio.events._set_running_loop(loop)
with loop:
app = Application.startup(sys.argv, sakia, loop)
lock = single_instance_lock(app.currency)
if not lock:
lock = single_instance_lock(app.currency)
if not lock:
QMessageBox.critical(None, "Sakia", "Sakia is already running.")
sys.exit(1)
app.start_coroutines()
app.get_last_version()
keep_trying = True
while not app.blockchain_service.initialized():
try:
box = StartupDialog()
box.show()
box.cancelButton.clicked.connect(cancel_connection)
loop.run_until_complete(app.initialize_blockchain())
box.hide()
except (DuniterError, NoPeerAvailable) as e:
reply = QMessageBox.critical(
None,
"Error",
"Error connecting to the network: {:}. Keep Trying?".format(str(e)),
QMessageBox.Ok | QMessageBox.Abort,
)
if reply == QMessageBox.Ok:
loop.run_until_complete(PreferencesDialog(app).async_exec())
else:
break
else:
if not app.connection_exists():
conn_controller = ConnectionConfigController.create_connection(
None, app
)
loop.run_until_complete(conn_controller.async_exec())
window = MainWindowController.startup(app)
loop.run_forever()
try:
loop.set_exception_handler(exit_exception_handler)
loop.run_until_complete(app.stop_current_profile())
logging.debug("Application stopped")
except asyncio.CancelledError:
logging.info("CancelledError")
logging.debug("Exiting")
cleanup_lock(lock)
sys.exit()
if __name__ == "__main__":
main()
from PyQt5.QtCore import QAbstractItemModel, QModelIndex, Qt, QSize
from PyQt5.QtGui import QIcon, QTextDocument
import logging
def parse_node(node_data, parent_item):
node = NodeItem(node_data, parent_item)
if parent_item:
parent_item.appendChild(node)
if "children" in node_data:
for child in node_data["children"]:
parse_node(child, node)
return node
class NodeItem(QAbstractItemModel):
def __init__(self, node, parent_item):
super().__init__(parent_item)
self.children = []
self.node = node
self.parent_item = parent_item
def appendChild(self, node_item):
self.children.append(node_item)
def child(self, row):
return self.children[row]
def childCount(self):
return len(self.children)
def columnCount(self):
return 1
def data(self, index, role):
if role == Qt.DisplayRole and "title" in self.node:
return self.node["title"]
if role == Qt.ToolTipRole and "tooltip" in self.node:
return self.node["tooltip"]
if role == Qt.DecorationRole and "icon" in self.node:
return QIcon(self.node["icon"])
if role == Qt.SizeHintRole:
return QSize(1, 22)
if role == GenericTreeModel.ROLE_RAW_DATA:
return self.node
def row(self):
if self.parent_item:
return self.parent_item.row() + self.parent_item.children.index(self)
return 0
def column(self):
return 0
class GenericTreeModel(QAbstractItemModel):
"""
A Qt abstract item model to display nodes from a dict
dict_format = {
'root_node': {
'node': ["title", "icon", "tooltip", "action"],
'children': {}
}
}
"""
ROLE_RAW_DATA = 101
def __init__(self, title, root_item):
"""
Constructor
"""
super().__init__(None)
self.title = title
self.root_item = root_item
@classmethod
def create(cls, title, data):
root_item = NodeItem({}, None)
for node in data:
parse_node(node, root_item)
return cls(title, root_item)
def columnCount(self, parent):
return 1
def data(self, index, role):
if not index.isValid():
return None
item = index.internalPointer()
if (
role
in (
Qt.DisplayRole,
Qt.DecorationRole,
Qt.ToolTipRole,
Qt.SizeHintRole,
GenericTreeModel.ROLE_RAW_DATA,
)
and index.column() == 0
):
return item.data(0, role)
return None
def flags(self, index):
if not index.isValid():
return Qt.NoItemFlags
if index.column() == 0:
return Qt.ItemIsEnabled | Qt.ItemIsSelectable
def headerData(self, section, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole and section == 0:
return self.title
return None
def index(self, row, column, parent):
if not self.hasIndex(row, column, parent):
return QModelIndex()
if not parent.isValid():
parent_item = self.root_item
else:
parent_item = parent.internalPointer()
child_item = parent_item.child(row)
if child_item:
return self.createIndex(row, column, child_item)
else:
return QModelIndex()
def parent(self, index):
if not index.isValid():
return QModelIndex()
child_item = index.internalPointer()
parent_item = child_item.parent()
if parent_item == self.root_item:
return QModelIndex()
return self.createIndex(parent_item.row(), 0, parent_item)
def rowCount(self, parent_index):
if not parent_index.isValid():
parent_item = self.root_item
else:
parent_item = parent_index.internalPointer()
if parent_index.column() > 0:
return 0
return parent_item.childCount()
def setData(self, index, value, role=Qt.EditRole):
if index.column() == 0:
return True
def insert_node(self, raw_data):
self.beginInsertRows(QModelIndex(), self.rowCount(QModelIndex()), 0)
parse_node(raw_data, self.root_item)
self.endInsertRows()
from .quantitative import Quantitative
from .relative import Relative
from .quant_zerosum import QuantitativeZSum
from .relative_zerosum import RelativeZSum
from .percent_of_average import PercentOfAverage
Referentials = (
Quantitative,
Relative,
PercentOfAverage,
QuantitativeZSum,
RelativeZSum,
)
from typing import Optional
class BaseReferential:
"""
Interface to all referentials
"""
def __init__(
self, amount: float, currency: str, app, block_number: Optional[int] = None
):
"""
Init base referential instance
:param amount: Amount to transform
:param currency: Name of currency
:param app: Application instance
:param block_number: Block number
"""
self.amount = amount
self.app = app
self.currency = currency
self._block_number = block_number
# todo: remove this useless class method and replace all occurence with a classic Object() creation.
@classmethod
def instance(cls, amount, currency, app, block_number=None):
return cls(amount, currency, app, block_number)
@classmethod
def translated_name(self):
raise NotImplementedError()
@property
def units(self):
raise NotImplementedError()
@property
def diff_units(self):
raise NotImplementedError()
def value(self):
raise NotImplementedError()
def differential(self):
raise NotImplementedError()
def set_referential(self, value):
raise NotImplementedError()
def set_diff_referential(self, value):
raise NotImplementedError()
@staticmethod
def to_si(value, base):
raise NotImplementedError()
@staticmethod
def base_str(base):
raise NotImplementedError()
def localized(self, units=False, show_base=False):
raise NotImplementedError()
def diff_localized(self, units=False, show_base=False):
raise NotImplementedError()
import re
def shortened(currency):
"""
Format the currency name to a short one
:return: The currency name in a shot format.
"""
words = re.split("[_\W]+", currency)
if len(words) > 1:
short = "".join([w[0] for w in words])
else:
vowels = ("a", "e", "i", "o", "u", "y")
short = currency
short = "".join([c for c in short if c not in vowels])
return short.upper()
def symbol(currency):
"""
Format the currency name to a symbol one.
:return: The currency name as a utf-8 circled symbol.
"""
letter = currency[0]
u = ord("\u24B6") + ord(letter) - ord("A")
return chr(u)
from typing import Optional
from .base_referential import BaseReferential
from ..data.processors import BlockchainProcessor
from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP, QLocale
class PercentOfAverage(BaseReferential):
_NAME_STR_ = QT_TRANSLATE_NOOP("PercentOfAverage", "PoA")
_REF_STR_ = QT_TRANSLATE_NOOP("PercentOfAverage", "{0} {1}{2}")
_UNITS_STR_ = QT_TRANSLATE_NOOP("PercentOfAverage", "PoA")
_FORMULA_STR_ = QT_TRANSLATE_NOOP(
"PercentOfAverage",
"""PoA = (Q / ( M(t-1) / N)) / 100
<br >
<table>
<tr><td>PoA</td><td>Percent of Average value</td></tr>
<tr><td>Q</td><td>Quantitative value</td></tr>
<tr><td>M</td><td>Monetary mass</td></tr>
<tr><td>N</td><td>Members count</td></tr>
</table>""",
)
_DESCRIPTION_STR_ = QT_TRANSLATE_NOOP(
"PercentOfAverage",
"""Another relative referential of the money.<br />
Percent of Average value PoA is calculated by dividing the quantitative value Q by the average<br />
then multiply by one hundred.<br />
This referential is relative and can be used to display prices and accounts, when UD growth is too slow.<br />
No money creation or destruction is apparent here and every account tend to<br />
the 100%.
""",
)
def __init__(self, amount, currency, app, block_number=None):
super().__init__(amount, currency, app, block_number)
self._blockchain_processor = BlockchainProcessor.instanciate(self.app)
@classmethod
def instance(
cls, amount: float, currency: str, app, block_number: Optional[int] = None
):
"""
Init PercentOfAverage referential instance
:param amount: Amount to transform
:param currency: Name of currency
:param app: Application instance
:param block_number: Block number
:return:
"""
return cls(amount, currency, app, block_number)
@classmethod
def translated_name(cls):
return QCoreApplication.translate(
"PercentOfAverage", PercentOfAverage._NAME_STR_
)
@property
def units(self):
return QCoreApplication.translate(
"PercentOfAverage", PercentOfAverage._UNITS_STR_
)
@property
def formula(self):
return QCoreApplication.translate(
"PercentOfAverage", PercentOfAverage._FORMULA_STR_
)
@property
def description(self):
return QCoreApplication.translate(
"PercentOfAverage", PercentOfAverage._DESCRIPTION_STR_
)
@property
def diff_units(self):
return self.units
@staticmethod
def base_str(base):
return ""
def value(self):
"""
Return relative value of amount
value = amount / UD(t)
:param int amount: Value
:param sakia.core.community.Community community: Community instance
:return: float
"""
mass = self._blockchain_processor.last_mass(self.currency)
members = self._blockchain_processor.last_members_count(self.currency)
average = mass / members
if average > 0:
return self.amount / average * 100
else:
return self.amount
def differential(self):
return self.value()
def set_referential(self, value):
"""
Set quantitative amount from referential value
:param value: Value in referential units
:return:
"""
mass = self._blockchain_processor.last_mass(self.currency)
members = self._blockchain_processor.last_members_count(self.currency)
average = mass / members
self.amount = value / 100 * average
return self
def set_diff_referential(self, value):
"""
Set quantitative amount from differential referential value
:param value:
:return:
"""
return self.set_referential(value)
def localized(self, units=False, show_base=False):
value = self.value()
localized_value = QLocale().toString(
float(value), "f", self.app.parameters.digits_after_comma
)
if units:
return QCoreApplication.translate(
"PercentOfAverage", PercentOfAverage._REF_STR_
).format(localized_value, "", (self.units if units else ""))
else:
return localized_value
def diff_localized(self, units=False, show_base=False):
value = self.differential()
localized_value = QLocale().toString(
float(value), "f", self.app.parameters.digits_after_comma
)
if units:
return QCoreApplication.translate(
"PercentOfAverage", PercentOfAverage._REF_STR_
).format(localized_value, "", (self.diff_units if units else ""))
else:
return localized_value
from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP, QLocale
from . import Quantitative
from .base_referential import BaseReferential
from .currency import shortened
from ..data.processors import BlockchainProcessor
class QuantitativeZSum(BaseReferential):
_NAME_STR_ = QT_TRANSLATE_NOOP("QuantitativeZSum", "Quant Z-sum")
_REF_STR_ = QT_TRANSLATE_NOOP("QuantitativeZSum", "{0}{1}{2}")
_UNITS_STR_ = QT_TRANSLATE_NOOP("QuantitativeZSum", "Q0")
_FORMULA_STR_ = QT_TRANSLATE_NOOP(
"QuantitativeZSum",
"""Q0 = Q - ( M(t) / N(t) )
<br >
<table>
<tr><td>Q0</td><td>Quantitative value at zero sum</td></tr>
<tr><td>Q</td><td>Quantitative value</td></tr>
<tr><td>M</td><td>Monetary mass</td></tr>
<tr><td>N</td><td>Members count</td></tr>
<tr><td>t</td><td>Last UD time</td></tr>
</table>""",
)
_DESCRIPTION_STR_ = QT_TRANSLATE_NOOP(
"QuantitativeZSum",
"""Quantitative at zero sum is used to display the difference between<br />
the quantitative value and the average quantitative value.<br />
If it is positive, the value is above the average value, and if it is negative,<br />
the value is under the average value.<br />
""",
)
def __init__(self, amount, currency, app, block_number=None):
super().__init__(amount, currency, app, block_number)
self._blockchain_processor = BlockchainProcessor.instanciate(self.app)
@classmethod
def translated_name(cls):
return QCoreApplication.translate(
"QuantitativeZSum", QuantitativeZSum._NAME_STR_
)
@property
def units(self):
return QCoreApplication.translate(
"QuantitativeZSum", QuantitativeZSum._UNITS_STR_
)
@property
def formula(self):
return QCoreApplication.translate(
"QuantitativeZSum", QuantitativeZSum._FORMULA_STR_
)
@property
def description(self):
return QCoreApplication.translate(
"QuantitativeZSum", QuantitativeZSum._DESCRIPTION_STR_
)
@property
def diff_units(self):
return QCoreApplication.translate(
"Quantitative", Quantitative._UNITS_STR_
).format("units")
def value(self):
"""
Return quantitative value of amount minus the average value
Z0 = Q - ( M(t) / N(t) )
Z0 = Quantitative value at zero sum
Q = Quantitative value
t = last UD block time
M = Monetary mass
N = Members count
:param int amount: Value
:param sakia.core.community.Community community: Community instance
:return: int
"""
last_members_count = self._blockchain_processor.last_members_count(
self.currency
)
monetary_mass = self._blockchain_processor.last_mass(self.currency)
if last_members_count != 0:
average = int(monetary_mass / last_members_count)
else:
average = 0
return (self.amount - average) / 100
@staticmethod
def base_str(base):
return Quantitative.base_str(base)
@staticmethod
def to_si(value, base):
return Quantitative.to_si(value, base)
def differential(self):
return Quantitative(self.amount, self.currency, self.app).value()
def set_referential(self, value):
"""
Set quantitative amount from referential value
:param value: Value in referential units
:return:
"""
last_members_count = self._blockchain_processor.last_members_count(
self.currency
)
monetary_mass = self._blockchain_processor.last_mass(self.currency)
if last_members_count != 0:
average = int(monetary_mass / last_members_count)
else:
average = 0
self.amount = (value + average) * 100
return self
def set_diff_referential(self, value):
"""
Set quantitative amount from differential referential value
:param value:
:return:
"""
self.amount = value * 100
return self
def localized(self, units=False, show_base=False):
value = self.value()
dividend, base = self._blockchain_processor.last_ud(self.currency)
prefix = ""
if show_base:
localized_value = QuantitativeZSum.to_si(value, base)
prefix = QuantitativeZSum.base_str(base)
else:
localized_value = QLocale().toString(float(value), "f", 2)
if units or show_base:
return QCoreApplication.translate(
"QuantitativeZSum", QuantitativeZSum._REF_STR_
).format(localized_value, "", (" " + self.units if units else ""))
else:
return localized_value
def diff_localized(self, units=False, show_base=False):
localized = Quantitative(self.amount, self.currency, self.app).localized(
units, show_base
)
return localized
from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP, QLocale
from .base_referential import BaseReferential
from .currency import shortened
from ..data.processors import BlockchainProcessor
class Quantitative(BaseReferential):
_NAME_STR_ = QT_TRANSLATE_NOOP("Quantitative", "Units")
_REF_STR_ = QT_TRANSLATE_NOOP("Quantitative", "{0} {1}{2}")
_UNITS_STR_ = QT_TRANSLATE_NOOP("Quantitative", "units")
_FORMULA_STR_ = QT_TRANSLATE_NOOP(
"Quantitative",
"""Q = Q
<br >
<table>
<tr><td>Q</td><td>Quantitative value</td></tr>
</table>
""",
)
_DESCRIPTION_STR_ = QT_TRANSLATE_NOOP(
"Quantitative", "Base referential of the money. Units values are used here."
)
def __init__(self, amount, currency, app, block_number=None):
super().__init__(amount, currency, app, block_number)
self._blockchain_processor = BlockchainProcessor.instanciate(self.app)
@classmethod
def translated_name(cls):
return QCoreApplication.translate("Quantitative", Quantitative._NAME_STR_)
@property
def units(self):
res = QCoreApplication.translate("Quantitative", Quantitative._UNITS_STR_)
return res
@property
def formula(self):
return QCoreApplication.translate("Quantitative", Quantitative._FORMULA_STR_)
@property
def description(self):
return QCoreApplication.translate(
"Quantitative", Quantitative._DESCRIPTION_STR_
)
@property
def diff_units(self):
return self.units
def value(self):
"""
Return quantitative value of amount
:param int amount: Value
:param sakia.core.community.Community community: Community instance
:return: int
"""
return int(self.amount) / 100
def differential(self):
return self.value()
def set_referential(self, value):
"""
Set quantitative amount from referential value
:param value: Value in referential units
:return:
"""
self.amount = value * 100
return self
def set_diff_referential(self, value):
"""
Set quantitative amount from differential referential value
:param value:
:return:
"""
return self.set_referential(value)
@staticmethod
def base_str(base):
unicodes = {
"0": ord("\u2070"),
"1": ord("\u00B9"),
"2": ord("\u00B2"),
"3": ord("\u00B3"),
}
for n in range(4, 10):
unicodes[str(n)] = ord("\u2070") + n
if base > 0:
return ".10" + "".join([chr(unicodes[e]) for e in str(base)])
else:
return ""
@staticmethod
def to_si(value, base):
if value < 0:
value = -value
multiplier = -1
else:
multiplier = 1
scientific_value = value
scientific_value /= 10 ** base
if base > 0:
localized_value = QLocale().toString(
float(scientific_value * multiplier), "f", 2
)
else:
localized_value = QLocale().toString(float(value * multiplier), "f", 2)
return localized_value
def localized(self, units=False, show_base=False):
value = self.value()
dividend, base = self._blockchain_processor.last_ud(self.currency)
localized_value = Quantitative.to_si(value, base)
prefix = Quantitative.base_str(base)
if units or show_base:
return QCoreApplication.translate(
"Quantitative", Quantitative._REF_STR_
).format(
localized_value,
prefix,
(" " if prefix and units else "") + (self.units if units else ""),
)
else:
return localized_value
def diff_localized(self, units=False, show_base=False):
value = self.differential()
dividend, base = self._blockchain_processor.last_ud(self.currency)
localized_value = Quantitative.to_si(value, base)
prefix = Quantitative.base_str(base)
if units or show_base:
return QCoreApplication.translate(
"Quantitative", Quantitative._REF_STR_
).format(
localized_value,
prefix,
(" " if prefix and units else "") + (self.diff_units if units else ""),
)
else:
return localized_value
from .base_referential import BaseReferential
from .currency import shortened
from ..data.processors import BlockchainProcessor
from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP, QLocale
class Relative(BaseReferential):
_NAME_STR_ = QT_TRANSLATE_NOOP("Relative", "UD")
_REF_STR_ = QT_TRANSLATE_NOOP("Relative", "{0} {1}{2}")
_UNITS_STR_ = QT_TRANSLATE_NOOP("Relative", "UD")
_FORMULA_STR_ = QT_TRANSLATE_NOOP(
"Relative",
"""R = Q / UD(t)
<br >
<table>
<tr><td>R</td><td>Relative value</td></tr>
<tr><td>Q</td><td>Quantitative value</td></tr>
<tr><td>UD</td><td>Universal Dividend</td></tr>
<tr><td>t</td><td>Last UD time</td></tr>
</table>""",
)
_DESCRIPTION_STR_ = QT_TRANSLATE_NOOP(
"Relative",
"""Relative referential of the money.<br />
Relative value R is calculated by dividing the quantitative value Q by the last<br />
Universal Dividend UD.<br />
This referential is the most practical one to display prices and accounts.<br />
No money creation or destruction is apparent here and every account tend to<br />
the average.
""",
)
def __init__(self, amount, currency, app, block_number=None):
super().__init__(amount, currency, app, block_number)
self._blockchain_processor = BlockchainProcessor.instanciate(self.app)
@classmethod
def instance(cls, amount, currency, app, block_number=None):
"""
:param int amount:
:param str currency:
:param sakia.app.Application app:
:param int block_number:
:return:
"""
return cls(amount, currency, app, block_number)
@classmethod
def translated_name(cls):
return QCoreApplication.translate("Relative", Relative._NAME_STR_)
@property
def units(self):
return QCoreApplication.translate("Relative", Relative._UNITS_STR_)
@property
def formula(self):
return QCoreApplication.translate("Relative", Relative._FORMULA_STR_)
@property
def description(self):
return QCoreApplication.translate("Relative", Relative._DESCRIPTION_STR_)
@property
def diff_units(self):
return self.units
@staticmethod
def base_str(base):
return ""
def value(self):
"""
Return relative value of amount
value = amount / UD(t)
:param int amount: Value
:param sakia.core.community.Community community: Community instance
:return: float
"""
dividend, base = self._blockchain_processor.last_ud(self.currency)
if dividend > 0:
return self.amount / (float(dividend * (10 ** base)))
else:
return self.amount / 100
def differential(self):
return self.value()
def set_referential(self, value):
"""
Set quantitative amount from referential value
:param value: Value in referential units
:return:
"""
dividend, base = self._blockchain_processor.last_ud(self.currency)
self.amount = value * (float(dividend * (10 ** base)))
return self
def set_diff_referential(self, value):
"""
Set quantitative amount from differential referential value
:param value:
:return:
"""
return self.set_referential(value)
def localized(self, units=False, show_base=False):
value = self.value()
localized_value = QLocale().toString(
float(value), "f", self.app.parameters.digits_after_comma
)
if units:
return QCoreApplication.translate("Relative", Relative._REF_STR_).format(
localized_value, "", (self.units if units else "")
)
else:
return localized_value
def diff_localized(self, units=False, show_base=False):
value = self.differential()
localized_value = QLocale().toString(
float(value), "f", self.app.parameters.digits_after_comma
)
if units:
return QCoreApplication.translate("Relative", Relative._REF_STR_).format(
localized_value, "", (self.diff_units if units else "")
)
else:
return localized_value
from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP, QLocale
from .relative import Relative
from .base_referential import BaseReferential
from .currency import shortened
from ..data.processors import BlockchainProcessor
class RelativeZSum(BaseReferential):
_NAME_STR_ = QT_TRANSLATE_NOOP("RelativeZSum", "Relat Z-sum")
_REF_STR_ = QT_TRANSLATE_NOOP("RelativeZSum", "{0} {1}{2}")
_UNITS_STR_ = QT_TRANSLATE_NOOP("RelativeZSum", "R0 UD")
_FORMULA_STR_ = QT_TRANSLATE_NOOP(
"RelativeZSum",
"""R0 = (Q / UD(t)) - (( M(t) / N(t) ) / UD(t))
<br >
<table>
<tr><td>R0</td><td>Relative value at zero sum</td></tr>
<tr><td>R</td><td>Relative value</td></tr>
<tr><td>M</td><td>Monetary mass</td></tr>
<tr><td>N</td><td>Members count</td></tr>
<tr><td>t</td><td>Last UD time</td></tr>
</table>""",
)
_DESCRIPTION_STR_ = QT_TRANSLATE_NOOP(
"RelativeZSum",
"""Relative at zero sum is used to display the difference between<br />
the relative value and the average relative value.<br />
If it is positive, the value is above the average value, and if it is negative,<br />
the value is under the average value.<br />
""",
)
def __init__(self, amount, currency, app, block_number=None):
super().__init__(amount, currency, app, block_number)
self._blockchain_processor = BlockchainProcessor.instanciate(self.app)
@classmethod
def translated_name(cls):
return QCoreApplication.translate("RelativeZSum", RelativeZSum._NAME_STR_)
@property
def units(self):
return QCoreApplication.translate("RelativeZSum", RelativeZSum._UNITS_STR_)
@property
def formula(self):
return QCoreApplication.translate("RelativeZSum", RelativeZSum._FORMULA_STR_)
@property
def description(self):
return QCoreApplication.translate(
"RelativeZSum", RelativeZSum._DESCRIPTION_STR_
)
@property
def diff_units(self):
return QCoreApplication.translate("Relative", Relative._UNITS_STR_)
@staticmethod
def base_str(base):
return Relative.base_str(base)
def value(self):
"""
Return relative value of amount minus the average value
zsum value = (value / UD(t)) - (( M(t) / N(t) ) / UD(t))
t = last UD block
M = Monetary mass
N = Members count
:param int amount: Value
:param sakia.core.community.Community community: Community instance
:return: float
"""
dividend, base = self._blockchain_processor.last_ud(self.currency)
monetary_mass = self._blockchain_processor.last_mass(self.currency)
members_count = self._blockchain_processor.last_members_count(self.currency)
if monetary_mass and members_count > 0:
median = monetary_mass / members_count
relative_value = self.amount / float(dividend * 10 ** base)
relative_median = median / float(dividend * 10 ** base)
else:
relative_value = self.amount
relative_median = 0
return relative_value - relative_median
def differential(self):
return Relative(self.amount, self.currency, self.app).value()
def set_referential(self, value):
"""
Set quantitative amount from referential value
:param value: Value in referential units
:return:
"""
dividend, base = self._blockchain_processor.last_ud(self.currency)
monetary_mass = self._blockchain_processor.last_mass(self.currency)
members_count = self._blockchain_processor.last_members_count(self.currency)
if monetary_mass and members_count > 0:
median = monetary_mass / members_count
relative_median = median / float(dividend * 10 ** base)
else:
relative_median = 0
self.amount = (value + relative_median) * float(dividend * 10 ** base)
return self
def set_diff_referential(self, value):
"""
Set quantitative amount from differential referential value
:param value:
:return:
"""
dividend, base = self._blockchain_processor.previous_ud(self.currency)
self.amount = value * float(dividend * 10 ** base)
return self
def localized(self, units=False, show_base=False):
value = self.value()
localized_value = QLocale().toString(
float(value), "f", self.app.parameters.digits_after_comma
)
if units:
return QCoreApplication.translate(
"RelativeZSum", RelativeZSum._REF_STR_
).format(localized_value, "", self.units if units else "")
else:
return localized_value
def diff_localized(self, units=False, show_base=False):
value = self.differential()
localized_value = QLocale().toString(
float(value), "f", self.app.parameters.digits_after_comma
)
if units:
return QCoreApplication.translate("Relative", Relative._REF_STR_).format(
localized_value, "", (self.diff_units if units else "")
)
else:
return localized_value
import attr
import logging
from logging import StreamHandler
from logging.handlers import RotatingFileHandler
from optparse import OptionParser
from os import environ, path, makedirs
import sys
def config_path_factory():
if sys.platform.startswith("darwin") and "XDG_CONFIG_HOME" in environ:
env_path = environ["XDG_CONFIG_HOME"]
elif sys.platform.startswith("linux") and "HOME" in environ:
env_path = path.join(environ["HOME"], ".config")
elif sys.platform.startswith("win32") and "APPDATA" in environ:
env_path = environ["APPDATA"]
else:
env_path = path.dirname(__file__)
return path.join(env_path, "sakia")
@attr.s()
class SakiaOptions:
config_path = attr.ib(default=attr.Factory(config_path_factory))
currency = attr.ib(default="gtest")
profile = attr.ib(default="Default Profile")
with_plugin = attr.ib(default="")
_logger = attr.ib(default=attr.Factory(lambda: logging.getLogger("sakia")))
@classmethod
def from_arguments(cls, argv):
options = cls()
if not path.exists(options.config_path):
makedirs(options.config_path)
options._parse_arguments(argv)
return options
def _parse_arguments(self, argv):
parser = OptionParser()
parser.add_option(
"-v",
"--verbose",
action="store_true",
dest="verbose",
default=False,
help="Print INFO messages to stdout",
)
parser.add_option(
"-d",
"--debug",
action="store_true",
dest="debug",
default=False,
help="Print DEBUG messages to stdout",
)
parser.add_option(
"--currency",
dest="currency",
default="g1",
help="Select a currency between g1, g1-test",
)
parser.add_option(
"--profile",
dest="profile",
default="Default Profile",
help="Select profile to use",
)
parser.add_option(
"--withplugin",
dest="with_plugin",
default="",
help="Load a plugin (for development purpose)",
)
(options, args) = parser.parse_args(argv)
if options.currency not in ("g1", "g1-test"):
raise RuntimeError("{0} is not a valid currency".format(options.currency))
else:
self.currency = options.currency
if options.profile:
self.profile = options.profile
if options.with_plugin:
if path.isfile(options.with_plugin) and options.with_plugin.endswith(
".zip"
):
self.with_plugin = options.with_plugin
else:
raise RuntimeError(
"{:} is not a valid path to a zip file".format(options.with_plugin)
)
if options.debug:
self._logger.setLevel(logging.DEBUG)
formatter = logging.Formatter(
"%(levelname)s:%(module)s:%(funcName)s:%(message)s"
)
elif options.verbose:
self._logger.setLevel(logging.INFO)
formatter = logging.Formatter("%(levelname)s:%(message)s")
if options.debug or options.verbose:
logging.getLogger("quamash").setLevel(logging.INFO)
file_handler = RotatingFileHandler(
path.join(self.config_path, "sakia.log"), "a", 1000000, 10
)
file_handler.setFormatter(formatter)
stream_handler = StreamHandler()
stream_handler.setFormatter(formatter)
self._logger.handlers = [file_handler, stream_handler]
self._logger.propagate = False
g1:
display: ğ1
nodes:
8iVdpXqFLCxGyPqgVx5YbFSkmWKkceXveRd2yvBKeARL:
- "BMAS g1.duniter.org 443"
38MEAZN68Pz1DTvT3tqgxx4yQP6snJCQhPqEFxbDk4aE:
- "BMAS g1.e-is.pro 443"
D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx:
- "BMAS g1.elo.tf 443"
g1-test:
display: ğ1-test
nodes:
238pNfpkNs4TdRgt6NnJ5Q72CDZbgNqm4cJo4nCP3BxC:
- "BASIC_MERKLED_API g1-test.duniter.org 91.121.157.13 10900"
from .network import NetworkService
from .identities import IdentitiesService
from .blockchain import BlockchainService
from .documents import DocumentsService
from .sources import SourcesServices
from .transactions import TransactionsService
import asyncio
from PyQt5.QtCore import QObject
import math
import logging
from duniterpy.api import bma
from duniterpy.api.errors import DuniterError
from sakia.errors import NoPeerAvailable
class BlockchainService(QObject):
"""
Blockchain service is managing new blocks received
to update data locally
"""
def __init__(
self,
app,
currency,
blockchain_processor,
connections_processor,
bma_connector,
identities_service,
transactions_service,
sources_service,
):
"""
Constructor the identities service
:param sakia.app.Application app: Sakia application
:param str currency: The currency name of the community
:param sakia.data.processors.BlockchainProcessor blockchain_processor: the blockchain processor for given currency
:param sakia.data.processors.ConnectionsProcessor connections_processor: the connections processor
:param sakia.data.connectors.BmaConnector bma_connector: The connector to BMA API
:param sakia.services.IdentitiesService identities_service: The identities service
:param sakia.services.TransactionsService transactions_service: The transactions service
:param sakia.services.SourcesService sources_service: The sources service
"""
super().__init__()
self.app = app
self._blockchain_processor = blockchain_processor
self._connections_processor = connections_processor
self._bma_connector = bma_connector
self.currency = currency
self._identities_service = identities_service
self._transactions_service = transactions_service
self._sources_service = sources_service
self._logger = logging.getLogger("sakia")
self._update_lock = False
def initialized(self):
return self._blockchain_processor.initialized(self.app.currency)
async def handle_blockchain_progress(self, network_blockstamp):
"""
Handle a new current block uid
:param duniterpy.documents.BlockUID network_blockstamp:
"""
if (
self._blockchain_processor.initialized(self.currency)
and not self._update_lock
):
try:
self._update_lock = True
self.app.refresh_started.emit()
start_number = self._blockchain_processor.block_number_30days_ago(
self.currency, network_blockstamp
)
if self.current_buid().number > start_number:
start_number = self.current_buid().number + 1
else:
connections = self._connections_processor.connections_to(
self.currency
)
end_time = self._blockchain_processor.rounded_timestamp(
self.currency, start_number
)
self._transactions_service.insert_stopline(
connections, start_number, end_time
)
self._logger.debug("Parsing from {0}".format(start_number))
connections = self._connections_processor.connections_to(self.currency)
await self._identities_service.refresh()
(
changed_tx,
new_tx,
new_dividends,
) = await self._transactions_service.handle_new_blocks(
connections, start_number, network_blockstamp.number
)
await self._sources_service.refresh_sources(connections)
await self._blockchain_processor.handle_new_blocks(
self.currency, network_blockstamp
)
self.app.db.commit()
for tx in changed_tx:
self.app.transaction_state_changed.emit(tx)
for conn in new_tx:
for tx in new_tx[conn]:
self.app.new_transfer.emit(conn, tx)
for conn in new_dividends:
for ud in new_dividends[conn]:
self.app.new_dividend.emit(conn, ud)
self.app.new_blocks_handled.emit()
self.app.sources_refreshed.emit()
except (NoPeerAvailable, DuniterError) as e:
self._logger.debug(str(e))
finally:
self.app.refresh_finished.emit()
self._update_lock = False
def current_buid(self):
return self._blockchain_processor.current_buid(self.currency)
def parameters(self):
return self._blockchain_processor.parameters(self.currency)
def time(self):
return self._blockchain_processor.time(self.currency)
def current_members_count(self):
return self._blockchain_processor.current_members_count(self.currency)
def current_mass(self):
return self._blockchain_processor.current_mass(self.currency)
def last_monetary_mass(self):
return self._blockchain_processor.last_mass(self.currency)
def last_ud(self):
return self._blockchain_processor.last_ud(self.currency)
def last_members_count(self):
return self._blockchain_processor.last_members_count(self.currency)
def last_ud_time(self):
return self._blockchain_processor.last_ud_time(self.currency)
def previous_members_count(self):
return self._blockchain_processor.previous_members_count(self.currency)
def previous_monetary_mass(self):
return self._blockchain_processor.previous_monetary_mass(self.currency)
def previous_ud_time(self):
return self._blockchain_processor.previous_ud_time(self.currency)
def previous_ud(self):
return self._blockchain_processor.previous_ud(self.currency)
def adjusted_ts(self, time):
return self._blockchain_processor.adjusted_ts(self.currency, time)
def next_ud_reeval(self):
parameters = self._blockchain_processor.parameters(self.currency)
mediantime = self._blockchain_processor.time(self.currency)
if parameters.ud_reeval_time_0 == 0 or parameters.dt_reeval == 0:
return 0
else:
ud_reeval = parameters.ud_reeval_time_0
while ud_reeval <= mediantime:
ud_reeval += parameters.dt_reeval
return ud_reeval
def computed_dividend(self):
"""
Computes next dividend value
Duniter formula is:
HEAD.dividend = Math.ceil(HEAD_1.dividend + Math.pow(conf.c, 2) *
Math.ceil(HEAD_1.massReeval / Math.pow(10, previousUB)) / HEAD.membersCount / (conf.dtReeval / conf.dt));
:rtype: int
"""
parameters = self.parameters()
if self.last_members_count():
last_ud = self.last_ud()[0] * 10 ** self.last_ud()[1]
next_ud = (
last_ud
+ pow(parameters.c / (parameters.dt_reeval / parameters.dt), 2)
* self.previous_monetary_mass()
/ self.last_members_count()
)
else:
next_ud = parameters.ud0
return math.ceil(next_ud)
import jsonschema
import attr
import logging
import pypeg2
from duniterpy.key import SigningKey
from duniterpy.documents import (
Certification,
Membership,
Revocation,
InputSource,
OutputSource,
SIGParameter,
Unlock,
block_uid,
)
from duniterpy.documents import Transaction as TransactionDoc
from duniterpy.documents.transaction import reduce_base
from duniterpy.grammars.output import Condition, Operator, SIG, CSV
from duniterpy.api import bma
from sakia.data.entities import Identity, Transaction, Source
from sakia.data.processors import (
BlockchainProcessor,
IdentitiesProcessor,
NodesProcessor,
TransactionsProcessor,
SourcesProcessor,
CertificationsProcessor,
ConnectionsProcessor,
)
from sakia.data.connectors import BmaConnector, parse_bma_responses
from sakia.errors import NotEnoughChangeError
from sakia.services.sources import SourcesServices
@attr.s()
class DocumentsService:
"""
A service to forge and broadcast documents
to the network
:param sakia.data.connectors.BmaConnector _bma_connector: the connector
:param sakia.data.processors.BlockchainProcessor _blockchain_processor: the blockchain processor
:param sakia.data.processors.IdentitiesProcessor _identities_processor: the identities processor
:param sakia.data.processors.TransactionsProcessor _transactions_processor: the transactions processor
:param sakia.data.processors.SourcesProcessor _sources_processor: the sources processor
"""
_bma_connector = attr.ib()
_blockchain_processor = attr.ib()
_identities_processor = attr.ib()
_certifications_processor = attr.ib()
_transactions_processor = attr.ib()
_sources_processor = attr.ib()
_sources_services = attr.ib() # type: SourcesServices
_logger = attr.ib(default=attr.Factory(lambda: logging.getLogger("sakia")))
@classmethod
def instanciate(cls, app):
"""
Instanciate a blockchain processor
:param sakia.app.Application app: the app
"""
return cls(
BmaConnector(NodesProcessor(app.db.nodes_repo), app.parameters),
BlockchainProcessor.instanciate(app),
IdentitiesProcessor.instanciate(app),
CertificationsProcessor.instanciate(app),
TransactionsProcessor.instanciate(app),
SourcesProcessor.instanciate(app),
SourcesServices(
app.currency,
SourcesProcessor.instanciate(app),
ConnectionsProcessor.instanciate(app),
TransactionsProcessor.instanciate(app),
BlockchainProcessor.instanciate(app),
BmaConnector(NodesProcessor(app.db.nodes_repo), app.parameters),
),
)
def generate_identity(self, connection):
identity = self._identities_processor.get_identity(
connection.currency, connection.pubkey, connection.uid
)
if not identity:
identity = Identity(connection.currency, connection.pubkey, connection.uid)
sig_window = self._blockchain_processor.parameters(
connection.currency
).sig_window
current_time = self._blockchain_processor.time(connection.currency)
if identity.is_obsolete(sig_window, current_time):
block_uid = self._blockchain_processor.current_buid(connection.currency)
identity.blockstamp = block_uid
timestamp = self._blockchain_processor.time(connection.currency)
identity.timestamp = timestamp
identity.signature = None
return identity
async def broadcast_identity(self, connection, identity_doc):
"""
Send our self certification to a target community
:param sakia.data.entities.Connection connection: the connection published
"""
self._logger.debug("Key publish: {0}".format(identity_doc.signed_raw()))
responses = await self._bma_connector.broadcast(
connection.currency,
bma.wot.add,
req_args={"identity_signed_raw": identity_doc.signed_raw()},
)
result = await parse_bma_responses(responses)
return result
async def broadcast_revocation(
self, currency, identity_document, revocation_document
):
signed_raw = revocation_document.signed_raw()
self._logger.debug("Broadcasting: \n" + signed_raw)
responses = await self._bma_connector.broadcast(
currency, bma.wot.revoke, req_args={"revocation_signed_raw": signed_raw}
)
result = False, ""
for r in responses:
if r.status == 200:
result = True, (await r.json())
elif not result[0]:
try:
result = False, bma.api.parse_error(await r.text())["message"]
except jsonschema.ValidationError as e:
result = False, str(e)
else:
await r.release()
return result
async def send_membership(self, connection, secret_key, password, mstype):
"""
Send a membership document to a target community.
Signal "document_broadcasted" is emitted at the end.
:param sakia.data.entities.Connection connection: the connection publishing ms doc
:param str secret_key: The account SigningKey salt
:param str password: The account SigningKey password
:param str mstype: The type of membership demand. "IN" to join, "OUT" to leave
"""
self._logger.debug("Send membership")
blockUID = self._blockchain_processor.current_buid(connection.currency)
membership = Membership(
10,
connection.currency,
connection.pubkey,
blockUID,
mstype,
connection.uid,
connection.blockstamp,
None,
)
key = SigningKey.from_credentials(
secret_key, password, connection.scrypt_params
)
membership.sign([key])
self._logger.debug("Membership: {0}".format(membership.signed_raw()))
responses = await self._bma_connector.broadcast(
connection.currency,
bma.blockchain.membership,
req_args={"membership_signed_raw": membership.signed_raw()},
)
result = await parse_bma_responses(responses)
return result
async def certify(self, connection, secret_key, password, identity):
"""
Certify another identity
:param sakia.data.entities.Connection connection: the connection published
:param str secret_key: the private key salt
:param str password: the private key password
:param sakia.data.entities.Identity identity: the identity certified
"""
self._logger.debug("Certdata")
blockUID = self._blockchain_processor.current_buid(connection.currency)
if not identity.signature:
lookup_data = await self._bma_connector.get(
connection.currency,
bma.wot.lookup,
req_args={"search": identity.pubkey},
)
for uid_data in next(
data["uids"]
for data in lookup_data["results"]
if data["pubkey"] == identity.pubkey
):
if (
uid_data["uid"] == identity.uid
and block_uid(uid_data["meta"]["timestamp"]) == identity.blockstamp
):
identity.signature = uid_data["self"]
break
else:
return False, "Could not find certified identity signature"
certification = Certification(
10,
connection.currency,
connection.pubkey,
identity.document(),
blockUID,
"",
)
key = SigningKey.from_credentials(
secret_key, password, connection.scrypt_params
)
certification.sign([key])
signed_cert = certification.signed_raw()
self._logger.debug("Certification: {0}".format(signed_cert))
timestamp = self._blockchain_processor.time(connection.currency)
responses = await self._bma_connector.broadcast(
connection.currency,
bma.wot.certify,
req_args={"certification_signed_raw": signed_cert},
)
result = await parse_bma_responses(responses)
if result[0]:
self._identities_processor.insert_or_update_identity(identity)
self._certifications_processor.create_or_update_certification(
connection.currency, certification, timestamp, None
)
return result
async def revoke(self, currency, identity, salt, password):
"""
Revoke self-identity on server, not in blockchain
:param str currency: The currency of the identity
:param sakia.data.entities.IdentityDoc identity: The certified identity
:param str salt: The account SigningKey salt
:param str password: The account SigningKey password
"""
revocation = Revocation(10, currency, identity, "")
self_cert = identity.document()
key = SigningKey.from_credentials(salt, password)
revocation.sign([key])
self._logger.debug("Self-Revocation Document: \n{0}".format(revocation.raw()))
self._logger.debug("Signature: \n{0}".format(revocation.signatures[0]))
data = {
"pubkey": identity.pubkey,
"self_": self_cert.signed_raw(),
"sig": revocation.signatures[0],
}
self._logger.debug("Posted data: {0}".format(data))
responses = await self._bma_connector.broadcast(currency, bma.wot.revoke, data)
result = await parse_bma_responses(responses)
return result
def generate_revocation(self, connection, secret_key, password):
"""
Generate account revocation document for given community
:param sakia.data.entities.Connection connection: The connection of the identity
:param str secret_key: The account SigningKey secret key
:param str password: The account SigningKey password
"""
identity = self._identities_processor.get_identity(
connection.currency, connection.pubkey, connection.uid
)
if not identity:
identity = self.generate_identity(connection)
identity_doc = identity.document()
key = SigningKey.from_credentials(
connection.salt, connection.password, connection.scrypt_params
)
identity_doc.sign([key])
identity.signature = identity_doc.signatures[0]
self._identities_processor.insert_or_update_identity(identity)
document = Revocation(10, connection.currency, identity.document(), "")
key = SigningKey.from_credentials(
secret_key, password, connection.scrypt_params
)
document.sign([key])
return document.signed_raw(), identity
def tx_sources(self, amount, amount_base, currency, key: SigningKey):
"""
Get inputs to generate a transaction with a given amount of money
:param int amount: The amount target value
:param int amount_base: The amount base target value
:param str currency: The community target of the transaction
:param SigningKey key: The key owning the sources
:return: The list of inputs to use in the transaction document
"""
# such a dirty algorithmm
# everything should be done again from scratch
# in future versions
def current_value(inputs, overhs):
i = 0
for s in inputs:
i += s.amount * (10 ** s.base)
for o in overhs:
i -= o[0] * (10 ** o[1])
return i
amount, amount_base = reduce_base(amount, amount_base)
available_sources = self._sources_processor.available(currency, key.pubkey)
if available_sources:
current_base = max([src.base for src in available_sources])
value = 0
sources = []
outputs = []
overheads = []
buf_sources = list(available_sources)
while current_base >= 0:
for s in [src for src in available_sources if src.base == current_base]:
condition = pypeg2.parse(s.conditions, Condition)
# evaluate the condition
result, _ = self._sources_services.evaluate_condition(
currency, condition, [key.pubkey], [], s.identifier
)
if not result:
continue
test_sources = sources + [s]
val = current_value(test_sources, overheads)
# if we have to compute an overhead
if current_value(test_sources, overheads) > amount * (
10 ** amount_base
):
overhead = current_value(test_sources, overheads) - int(
amount
) * (10 ** amount_base)
# we round the overhead in the current base
# example: 12 in base 1 -> 1*10^1
overhead = int(round(float(overhead) / (10 ** current_base)))
source_value = s.amount * (10 ** s.base)
out = int(
(source_value - (overhead * (10 ** current_base)))
/ (10 ** current_base)
)
if out * (10 ** current_base) <= amount * (10 ** amount_base):
sources.append(s)
buf_sources.remove(s)
overheads.append((overhead, current_base))
outputs.append((out, current_base))
# else just add the output
else:
sources.append(s)
buf_sources.remove(s)
outputs.append((s.amount, s.base))
if current_value(sources, overheads) == amount * (
10 ** amount_base
):
return sources, outputs, overheads
current_base -= 1
raise NotEnoughChangeError(
current_value(available_sources, []),
currency,
len(available_sources),
amount * pow(10, amount_base),
)
def tx_inputs(self, sources):
"""
Get inputs to generate a transaction with a given amount of money
:param list[sakia.data.entities.Source] sources: The sources used to send the given amount of money
:return: The list of inputs to use in the transaction document
"""
inputs = []
for s in sources:
inputs.append(
InputSource(s.amount, s.base, s.type, s.identifier, s.noffset)
)
return inputs
def tx_unlocks(self, sources):
"""
Get unlocks to generate a transaction with a given amount of money
:param list sources: The sources used to send the given amount of money
:return: The list of unlocks to use in the transaction document
"""
unlocks = []
for i, s in enumerate(sources):
unlocks.append(Unlock(i, [SIGParameter(0)]))
return unlocks
def tx_outputs(self, issuer, receiver, outputs, overheads, lock_mode):
"""
Get outputs to generate a transaction with a given amount of money
:param int lock_mode: Index of the selected spend condition
:param str issuer: The issuer of the transaction
:param str receiver: The target of the transaction
:param list outputs: The amount to send
:param list inputs: The inputs used to send the given amount of money
:param list overheads: The overheads used to send the given amount of money
:return: The list of outputs to use in the transaction document
"""
lock_modes = {
# Receiver
0: pypeg2.compose(Condition.token(SIG.token(receiver)), Condition),
# Receiver or (issuer and delay of one week)
1: pypeg2.compose(
Condition.token(
SIG.token(receiver),
Operator.token("||"),
Condition.token(
SIG.token(issuer),
Operator.token("&&"),
CSV.token(604800),
),
),
Condition,
),
}
total = []
outputs_bases = set(o[1] for o in outputs)
for base in outputs_bases:
output_sum = 0
for o in outputs:
if o[1] == base:
output_sum += o[0]
# fixme: OutputSource condition argument should be an instance of Condition, not a string
# it is not to the user to construct the condition script, but to the dedicated classes
total.append(OutputSource(output_sum, base, lock_modes[lock_mode]))
overheads_bases = set(o[1] for o in overheads)
for base in overheads_bases:
overheads_sum = 0
for o in overheads:
if o[1] == base:
overheads_sum += o[0]
# fixme: OutputSource condition argument should be an instance of Condition, not a string
# it is not to the user to construct the condition script, but to the dedicated classes
total.append(
OutputSource(
overheads_sum,
base,
pypeg2.compose(Condition.token(SIG.token(issuer)), Condition),
)
)
return total
def commit_outputs_to_self(self, currency, pubkey, txdoc):
"""
Save outputs to self
:param str currency:
:param str pubkey:
:param TransactionDoc txdoc:
:return:
"""
for offset, output in enumerate(txdoc.outputs):
if self._sources_services.find_signature_in_condition(
output.condition, pubkey
):
source = Source(
currency=currency,
pubkey=pubkey,
identifier=txdoc.sha_hash,
type=Source.TYPE_TRANSACTION,
noffset=offset,
amount=output.amount,
base=output.base,
conditions=pypeg2.compose(output.condition, Condition),
)
self._sources_processor.insert(source)
def prepare_tx(
self,
key,
receiver,
blockstamp,
amount,
amount_base,
message,
currency,
lock_mode=0,
source=None,
):
"""
Prepare a simple Transaction document
:param SigningKey key: the issuer of the transaction
:param str receiver: the target of the transaction
:param duniterpy.documents.BlockUID blockstamp: the blockstamp
:param int amount: the amount sent to the receiver
:param int amount_base: the amount base of the currency
:param str message: the comment of the tx
:param str currency: the target community
:param int lock_mode: Lock condition mode selected in combo box
:param Source source: Source instance or None
:return: the transaction document
:rtype: List[sakia.data.entities.Transaction]
"""
forged_tx = []
if source is None:
# automatic selection of sources
sources = [None] * 41
while len(sources) > 40:
result = self.tx_sources(int(amount), amount_base, currency, key)
sources = result[0]
computed_outputs = result[1]
overheads = result[2]
# Fix issue #594
if len(sources) > 40:
sources_value = 0
for s in sources[:39]:
sources_value += s.amount * (10 ** s.base)
sources_value, sources_base = reduce_base(sources_value, 0)
chained_tx = self.prepare_tx(
key,
key.pubkey,
blockstamp,
sources_value,
sources_base,
"[CHAINED]",
currency,
)
forged_tx += chained_tx
else:
sources = [source]
computed_outputs = [(source.amount, source.base)]
overheads = []
logging.debug("Inputs: {0}".format(sources))
inputs = self.tx_inputs(sources)
unlocks = self.tx_unlocks(sources)
outputs = self.tx_outputs(
key.pubkey, receiver, computed_outputs, overheads, lock_mode
)
logging.debug("Outputs: {0}".format(outputs))
txdoc = TransactionDoc(
10,
currency,
blockstamp,
0,
[key.pubkey],
inputs,
unlocks,
outputs,
message,
None,
)
txdoc.sign([key])
self.commit_outputs_to_self(currency, key.pubkey, txdoc)
time = self._blockchain_processor.time(currency)
tx = Transaction(
currency=currency,
pubkey=key.pubkey,
sha_hash=txdoc.sha_hash,
written_block=0,
blockstamp=blockstamp,
timestamp=time,
signatures=txdoc.signatures,
issuers=[key.pubkey],
receivers=[receiver],
amount=amount,
amount_base=amount_base,
comment=txdoc.comment,
txid=0,
state=Transaction.TO_SEND,
local=True,
raw=txdoc.signed_raw(),
)
forged_tx.append(tx)
self._sources_processor.consume(sources, tx.sha_hash)
return forged_tx
async def send_money(
self,
connection,
secret_key,
password,
recipient,
amount,
amount_base,
message,
lock_mode,
source,
):
"""
Send money to a given recipient in a specified community
:param int lock_mode: Index in the combo_locks combobox
:param sakia.data.entities.Connection connection: The account salt
:param str secret_key: The account secret_key
:param str password: The account password
:param str recipient: The pubkey of the recipient
:param int amount: The amount of money to transfer
:param int amount_base: The amount base of the transfer
:param str message: The message to send with the transfer
:param Source source: Source instance or None
"""
blockstamp = self._blockchain_processor.current_buid(connection.currency)
key = SigningKey.from_credentials(
secret_key, password, connection.scrypt_params
)
logging.debug("Sender pubkey:{0}".format(key.pubkey))
tx_entities = []
result = (True, ""), tx_entities
try:
tx_entities = self.prepare_tx(
key,
recipient,
blockstamp,
amount,
amount_base,
message,
connection.currency,
lock_mode,
source,
)
for i, tx in enumerate(tx_entities):
logging.debug("Transaction: [{0}]".format(tx.raw))
tx.txid = i
tx_res, tx_entities[i] = await self._transactions_processor.send(
tx, connection.currency
)
# Result can be negative if a tx is not accepted by the network
if result[0]:
if not tx_res[0]:
result = (False, tx_res[1]), tx_entities
result = result[0], tx_entities
return result
except NotEnoughChangeError as e:
return (False, str(e)), tx_entities
from PyQt5.QtCore import QObject
import asyncio
from duniterpy.api import bma, errors
from duniterpy.documents import BlockUID, block_uid
from sakia.errors import NoPeerAvailable
from sakia.data.entities import Certification, Identity
import logging
class IdentitiesService(QObject):
"""
Identities service is managing identities data received
to update data locally
"""
def __init__(
self,
currency,
connections_processor,
identities_processor,
certs_processor,
blockchain_processor,
bma_connector,
):
"""
Constructor the identities service
:param str currency: The currency name of the community
:param sakia.data.processors.IdentitiesProcessor identities_processor: the identities processor for given currency
:param sakia.data.processors.CertificationsProcessor certs_processor: the certifications processor for given currency
:param sakia.data.processors.BlockchainProcessor blockchain_processor: the blockchain processor for given currency
:param sakia.data.processors.ConnectionsProcessor connections_processor: the connections processor
:param sakia.data.connectors.BmaConnector bma_connector: The connector to BMA API
"""
super().__init__()
self._connections_processor = connections_processor
self._identities_processor = identities_processor
self._certs_processor = certs_processor
self._blockchain_processor = blockchain_processor
self._bma_connector = bma_connector
self.currency = currency
self._logger = logging.getLogger("sakia")
def certification_expired(self, cert_time):
"""
Return True if the certificaton time is too old
:param int cert_time: the timestamp of the certification
"""
parameters = self._blockchain_processor.parameters(self.currency)
blockchain_time = self._blockchain_processor.time(self.currency)
return blockchain_time - cert_time > parameters.sig_validity
def certification_writable(self, cert_time):
"""
Return True if the certificaton time is too old
:param int cert_time: the timestamp of the certification
"""
parameters = self._blockchain_processor.parameters(self.currency)
blockchain_time = self._blockchain_processor.time(self.currency)
return (
blockchain_time - cert_time
< parameters.sig_window * parameters.avg_gen_time
)
def expiration_date(self, identity):
"""
Get the expiration date of the identity
:param sakia.data.entities.Identity identity:
:return: the expiration timestamp
:rtype: int
"""
validity = self._blockchain_processor.parameters(self.currency).ms_validity
if identity.membership_timestamp:
return identity.membership_timestamp + validity
else:
return 0
def _get_connections_identities(self):
"""
:rtype: List of sakia.data.entities.Identity
"""
connections = self._connections_processor.connections_with_uids(self.currency)
identities = []
for c in connections:
identities.append(
self._identities_processor.get_identity(self.currency, c.pubkey)
)
return identities
def is_identity_of_connection(self, identity):
return identity.pubkey in self._connections_processor.pubkeys()
async def load_memberships(self, identity):
"""
Request the identity data and save it to written identities
It does nothing if the identity is already written and updated with blockchain lookups
:param sakia.data.entities.Identity identity: the identity
"""
try:
search = await self._bma_connector.get(
self.currency,
bma.blockchain.memberships,
req_args={"search": identity.pubkey},
)
blockstamp = BlockUID.empty()
membership_data = None
for ms in search["memberships"]:
if ms["blockNumber"] > blockstamp.number:
blockstamp = BlockUID(ms["blockNumber"], ms["blockHash"])
membership_data = ms
if membership_data:
identity.membership_timestamp = (
await self._blockchain_processor.timestamp(
self.currency, blockstamp.number
)
)
identity.membership_buid = blockstamp
identity.membership_type = membership_data["membership"]
identity.membership_written_on = membership_data["written"]
identity = await self.load_requirements(identity)
# We save connections pubkeys
identity.written = True
if self.is_identity_of_connection(identity):
self._identities_processor.insert_or_update_identity(identity)
except errors.DuniterError as e:
logging.debug(str(e))
if e.ucode in (
errors.NO_MATCHING_IDENTITY,
errors.NO_MEMBER_MATCHING_PUB_OR_UID,
):
identity.written = False
if self.is_identity_of_connection(identity):
self._identities_processor.insert_or_update_identity(identity)
except NoPeerAvailable as e:
logging.debug(str(e))
return identity
async def load_certs_in_lookup(self, identity, certifiers, certified):
"""
:param sakia.data.entities.Identity identity: the identity
:param dict[sakia.data.entities.Certification] certifiers: the list of certifiers got in /wot/certifiers-of
:param dict[sakia.data.entities.Certification] certified: the list of certified got in /wot/certified-by
"""
try:
lookup_data = await self._bma_connector.get(
self.currency, bma.wot.lookup, {"search": identity.pubkey}
)
for result in lookup_data["results"]:
if result["pubkey"] == identity.pubkey:
for uid_data in result["uids"]:
if not identity.uid or uid_data["uid"] == identity.uid:
if (
not identity.blockstamp
or identity.blockstamp
== block_uid(uid_data["meta"]["timestamp"])
):
for other_data in uid_data["others"]:
cert = Certification(
currency=self.currency,
certified=identity.pubkey,
certifier=other_data["pubkey"],
block=other_data["meta"]["block_number"],
timestamp=0,
signature=other_data["signature"],
)
certifier = Identity(
currency=self.currency,
pubkey=other_data["pubkey"],
uid=other_data["uids"][0],
member=other_data["isMember"],
)
if cert not in certifiers:
cert.timestamp = self._blockchain_processor.rounded_timestamp(
self.currency, cert.block
)
certifiers[cert] = certifier
# We save connections pubkeys
if self.is_identity_of_connection(identity):
self._certs_processor.insert_or_update_certification(
cert
)
for signed_data in result["signed"]:
cert = Certification(
currency=self.currency,
certified=signed_data["pubkey"],
certifier=identity.pubkey,
block=signed_data["cert_time"]["block"],
timestamp=0,
signature=signed_data["signature"],
)
certified_idty = Identity(
currency=self.currency,
pubkey=signed_data["pubkey"],
uid=signed_data["uid"],
member=signed_data["isMember"],
)
if cert not in certified:
certified[cert] = certified_idty
# We save connections pubkeys
if self.is_identity_of_connection(identity):
cert.timestamp = (
self._blockchain_processor.rounded_timestamp(
self.currency, cert.block
)
)
self._certs_processor.insert_or_update_certification(cert)
except errors.DuniterError as e:
logging.debug("Certified by error: {0}".format(str(e)))
except NoPeerAvailable as e:
logging.debug(str(e))
return certifiers, certified
async def load_certifiers_of(self, identity):
"""
Request the identity data and save it to written certifications
It does nothing if the identity is already written and updated with blockchain lookups
:param sakia.data.entities.Identity identity: the identity
"""
certifications = {}
try:
data = await self._bma_connector.get(
self.currency, bma.wot.certifiers_of, {"search": identity.pubkey}
)
for certifier_data in data["certifications"]:
cert = Certification(
currency=self.currency,
certified=data["pubkey"],
certifier=certifier_data["pubkey"],
block=certifier_data["cert_time"]["block"],
timestamp=certifier_data["cert_time"]["medianTime"],
signature=certifier_data["signature"],
)
certifier = Identity(
currency=self.currency,
pubkey=certifier_data["pubkey"],
uid=certifier_data["uid"],
member=certifier_data["isMember"],
)
if certifier_data["written"]:
cert.written_on = certifier_data["written"]["number"]
certifications[cert] = certifier
# We save connections pubkeys
if identity.pubkey in self._connections_processor.pubkeys():
self._certs_processor.insert_or_update_certification(cert)
identity.written = True
if self.is_identity_of_connection(identity):
self._identities_processor.insert_or_update_identity(identity)
except errors.DuniterError as e:
if e.ucode in (
errors.NO_MATCHING_IDENTITY,
errors.NO_MEMBER_MATCHING_PUB_OR_UID,
):
identity.written = False
if identity.pubkey in self._connections_processor.pubkeys():
self._identities_processor.insert_or_update_identity(identity)
logging.debug("Certified by error: {0}".format(str(e)))
except NoPeerAvailable as e:
logging.debug(str(e))
return certifications
async def load_certified_by(self, identity):
"""
Request the identity data and save it to written certifications
It does nothing if the identity is already written and updated with blockchain lookups
:param sakia.data.entities.Identity identity: the identity
"""
certifications = {}
try:
data = await self._bma_connector.get(
self.currency, bma.wot.certified_by, {"search": identity.pubkey}
)
for certified_data in data["certifications"]:
cert = Certification(
currency=self.currency,
certifier=data["pubkey"],
certified=certified_data["pubkey"],
block=certified_data["cert_time"]["block"],
timestamp=certified_data["cert_time"]["medianTime"],
signature=certified_data["signature"],
)
certified = Identity(
currency=self.currency,
pubkey=certified_data["pubkey"],
uid=certified_data["uid"],
member=certified_data["isMember"],
)
if certified_data["written"]:
cert.written_on = certified_data["written"]["number"]
certifications[cert] = certified
# We save connections pubkeys
if identity.pubkey in self._connections_processor.pubkeys():
self._certs_processor.insert_or_update_certification(cert)
identity.written = True
if self.is_identity_of_connection(identity):
self._identities_processor.insert_or_update_identity(identity)
except errors.DuniterError as e:
if e.ucode in (
errors.NO_MATCHING_IDENTITY,
errors.NO_MEMBER_MATCHING_PUB_OR_UID,
):
logging.debug("Certified by error: {0}".format(str(e)))
identity.written = False
if identity.pubkey in self._connections_processor.pubkeys():
self._identities_processor.insert_or_update_identity(identity)
except NoPeerAvailable as e:
logging.debug(str(e))
return certifications
async def initialize_certifications(self, identity, log_stream=None, progress=None):
"""
Initialize certifications to and from a given identity
:param sakia.data.entities.Identity identity:
:param callable log_stream: Logger function
:param callable progress: Progress function for progress bar
"""
if log_stream:
log_stream("Requesting certifiers of data")
certifiers = await self.load_certifiers_of(identity)
if log_stream:
log_stream("Requesting certified by data")
certified = await self.load_certified_by(identity)
if log_stream:
log_stream("Requesting lookup data")
certifiers, certified = await self.load_certs_in_lookup(
identity, certifiers, certified
)
if log_stream:
log_stream("Requesting identities of certifications")
identities = []
i = 0
nb_certs = len(certified) + len(certifiers)
for cert in certifiers:
if log_stream:
log_stream("Requesting identity... {0}/{1}".format(i, nb_certs))
i += 1
if progress:
progress(1 / nb_certs)
if not self.get_identity(cert.certifier):
identities.append(certifiers[cert])
for cert in certified:
if log_stream:
log_stream("Requesting identity... {0}/{1}".format(i, nb_certs))
i += 1
if progress:
progress(1 / nb_certs)
if not self.get_identity(cert.certified):
identities.append(certified[cert])
if log_stream:
log_stream("Commiting identities...")
for idty in identities:
self._identities_processor.insert_or_update_identity(idty)
def _parse_median_time(self, block):
"""
Parse revoked pubkeys found in a block and refresh local data
:param duniterpy.documents.Block block: the block received
:return: list of identities updated
"""
identities = []
connections_identities = self._get_connections_identities()
parameters = self._blockchain_processor.parameters(block.currency)
for idty in connections_identities:
if (
idty.member
and idty.membership_timestamp + parameters.ms_validity
< block.mediantime
):
identities.append(idty)
return identities
async def load_requirements(self, identity):
"""
Refresh a given identity information
:param sakia.data.entities.Identity identity:
:return:
"""
try:
requirements = await self._bma_connector.get(
self.currency,
bma.wot.requirements,
req_args={"search": identity.pubkey},
)
for identity_data in requirements["identities"]:
if not identity.uid or identity.uid == identity_data["uid"]:
if not identity.blockstamp or identity.blockstamp == block_uid(
identity_data["meta"]["timestamp"]
):
identity.uid = identity_data["uid"]
identity.blockstamp = block_uid(
identity_data["meta"]["timestamp"]
)
identity.timestamp = (
self._blockchain_processor.rounded_timestamp(
self.currency, identity.blockstamp.number
)
)
identity.outdistanced = identity_data["outdistanced"]
identity.written = identity_data["wasMember"]
identity.sentry = identity_data["isSentry"]
identity.member = identity_data["membershipExpiresIn"] > 0
median_time = self._blockchain_processor.time(self.currency)
expiration_time = self._blockchain_processor.parameters(
self.currency
).ms_validity
identity.membership_timestamp = median_time - (
expiration_time - identity_data["membershipExpiresIn"]
)
# We save connections pubkeys
if self._identities_processor.get_identity(
self.currency, identity.pubkey, identity.uid
):
self._identities_processor.insert_or_update_identity(
identity
)
except errors.DuniterError as e:
if e.ucode == errors.NO_MEMBER_MATCHING_PUB_OR_UID:
pass
else:
self._logger.debug(str(e))
except NoPeerAvailable as e:
self._logger.debug(str(e))
return identity
async def refresh(self):
"""
Handle new block received and refresh local data
:param duniterpy.documents.Block block: the received block
"""
need_refresh = self._get_connections_identities()
refresh_futures = []
# for every identity for which we need a refresh, we gather
# requirements requests
for identity in set(need_refresh):
refresh_futures.append(self.load_requirements(identity))
await asyncio.gather(*refresh_futures)
return need_refresh
async def lookup(self, text):
"""
Lookup for a given text in the network and in local db
:param str text: text contained in identity data
:rtype: list[sakia.data.entities.Identity]
:return: the list of identities found
"""
return await self._identities_processor.lookup(self.currency, text)
def insert_or_update_identity(self, identity):
return self._identities_processor.insert_or_update_identity(identity)
def get_identity(self, pubkey, uid=""):
return self._identities_processor.get_identity(self.currency, pubkey, uid)
async def find_from_pubkey(self, pubkey):
return await self._identities_processor.find_from_pubkey(self.currency, pubkey)
def ms_time_remaining(self, identity):
return self.expiration_date(identity) - self._blockchain_processor.time(
identity.currency
)
def certifications_received(self, pubkey):
"""
Get the list of certifications received by a given identity
:param str pubkey: the pubkey
:rtype: list[Certification]
"""
return self._certs_processor.certifications_received(self.currency, pubkey)
def certifications_sent(self, pubkey):
"""
Get the list of certifications received by a given identity
:param str pubkey: the pubkey
:rtype: list[Certification]
"""
return self._certs_processor.certifications_sent(self.currency, pubkey)