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 3096 additions and 2 deletions
import attr
from duniterpy.documents import BlockUID, block_uid, MalformedDocumentError
from ..entities import Node
@attr.s(frozen=True)
class NodesRepo:
"""The repository for Communities entities."""
_conn = attr.ib() # :type sqlite3.Connection
_primary_keys = (attr.fields(Node).currency, attr.fields(Node).pubkey)
def insert(self, node):
"""
Commit a node to the database
:param sakia.data.entities.Node node: the node to commit
"""
node_tuple = attr.astuple(node, tuple_factory=list)
node_tuple[2] = "\n".join([str(n) for n in node_tuple[2]])
node_tuple[12] = "\n".join([str(n) for n in node_tuple[12]])
values = ",".join(["?"] * len(node_tuple))
self._conn.execute("INSERT INTO nodes VALUES ({0})".format(values), node_tuple)
def update(self, node):
"""
Update an existing node in the database
:param sakia.data.entities.Node node: the node to update
"""
updated_fields = attr.astuple(
node,
tuple_factory=list,
filter=attr.filters.exclude(*NodesRepo._primary_keys),
)
updated_fields[0] = "\n".join([str(n) for n in updated_fields[0]])
updated_fields[10] = "\n".join([str(n) for n in updated_fields[9]])
where_fields = attr.astuple(
node,
tuple_factory=list,
filter=attr.filters.include(*NodesRepo._primary_keys),
)
self._conn.execute(
"""UPDATE nodes SET
endpoints=?,
peer_buid=?,
uid=?,
current_buid=?,
current_ts=?,
previous_buid=?,
state=?,
software=?,
version=?,
merkle_peers_root=?,
merkle_peers_leaves=?,
root=?,
member=?,
last_state_change=?
WHERE
currency=? AND
pubkey=?""",
updated_fields + where_fields,
)
def get_one(self, **search):
"""
Get an existing node in the database
:param dict search: the criterions of the lookup
:rtype: sakia.data.entities.Node
"""
filters = []
values = []
for k, v in search.items():
if isinstance(v, bool):
v = int(v)
filters.append("{k}=?".format(k=k))
values.append(v)
request = "SELECT * FROM nodes WHERE {filters}".format(
filters=" AND ".join(filters)
)
c = self._conn.execute(request, tuple(values))
data = c.fetchone()
if data:
return Node(*data)
def get_all(self, **search):
"""
Get all existing node in the database corresponding to the search
:param dict search: the criterions of the lookup
:rtype: sakia.data.entities.Node
"""
filters = []
values = []
for k, v in search.items():
if isinstance(v, bool):
value = int(v)
else:
value = v
filters.append("{key} = ?".format(key=k))
values.append(value)
if filters:
request = "SELECT * FROM nodes WHERE {filters}".format(
filters=" AND ".join(filters)
)
else:
request = "SELECT * FROM nodes"
c = self._conn.execute(request, tuple(values))
datas = c.fetchall()
if datas:
return [Node(*data) for data in datas]
return []
def drop(self, node):
"""
Drop an existing node from the database
:param sakia.data.entities.Node node: the node to update
"""
where_fields = attr.astuple(
node, filter=attr.filters.include(*NodesRepo._primary_keys)
)
self._conn.execute(
"""DELETE FROM nodes
WHERE
currency=? AND pubkey=?""",
where_fields,
)
def get_offline_nodes(self, currency):
c = self._conn.execute(
"SELECT * FROM nodes WHERE currency == ? AND state > ?;",
(currency, Node.FAILURE_THRESHOLD),
)
datas = c.fetchall()
if datas:
return [Node(*data) for data in datas]
return []
def get_synced_nodes(self, currency, current_buid):
c = self._conn.execute(
"SELECT * FROM nodes "
"WHERE currency == ? "
"AND state <= ?"
"AND current_buid == ?",
(currency, Node.FAILURE_THRESHOLD, current_buid),
)
datas = c.fetchall()
if datas:
return [Node(*data) for data in datas]
return []
def get_synced_members_nodes(self, currency, current_buid):
c = self._conn.execute(
"SELECT * FROM nodes "
"WHERE currency == ? "
"AND state <= ?"
"AND current_buid == ?"
"AND member == 1",
(currency, Node.FAILURE_THRESHOLD, current_buid),
)
datas = c.fetchall()
if datas:
return [Node(*data) for data in datas]
return []
def get_online_nodes(self, currency):
c = self._conn.execute(
"SELECT * FROM nodes WHERE currency == ? AND state <= ?;",
(currency, Node.FAILURE_THRESHOLD),
)
datas = c.fetchall()
if datas:
return [Node(*data) for data in datas]
return []
def get_offline_synced_nodes(self, currency, current_buid):
c = self._conn.execute(
"SELECT * FROM nodes "
"WHERE currency == ? "
"AND state > ?"
"AND current_buid == ?",
(currency, Node.FAILURE_THRESHOLD, current_buid),
)
datas = c.fetchall()
if datas:
return [Node(*data) for data in datas]
return []
def current_buid(self, currency):
c = self._conn.execute(
"""SELECT COUNT(`uid`)
FROM `nodes`
WHERE member == 1 AND currency == ?
LIMIT 1;""",
(currency,),
)
data = c.fetchone()
if data and data[0] > 3:
c = self._conn.execute(
"""SELECT `current_buid`,
COUNT(`current_buid`) AS `value_occurrence`
FROM `nodes`
WHERE member == 1 AND currency == ?
GROUP BY `current_buid`
ORDER BY `value_occurrence` DESC
LIMIT 1;""",
(currency,),
)
data = c.fetchone()
if data:
return block_uid(data[0])
else:
c = self._conn.execute(
"""SELECT `current_buid`,
COUNT(`current_buid`) AS `value_occurrence`
FROM `nodes`
WHERE currency == ?
GROUP BY `current_buid`
ORDER BY `value_occurrence` DESC
LIMIT 1;""",
(currency,),
)
data = c.fetchone()
if data:
return block_uid(data[0])
return BlockUID.empty()
import attr
from ..entities import Source
@attr.s(frozen=True)
class SourcesRepo:
"""The repository for Communities entities."""
_conn = attr.ib() # :type sqlite3.Connection
_primary_keys = (
attr.fields(Source).currency,
attr.fields(Source).pubkey,
attr.fields(Source).identifier,
attr.fields(Source).noffset,
)
def insert(self, source):
"""
Commit a source to the database
:param sakia.data.entities.Source source: the source to commit
"""
source_tuple = attr.astuple(source)
values = ",".join(["?"] * len(source_tuple))
self._conn.execute(
"INSERT INTO sources VALUES ({0})".format(values), source_tuple
)
def get_one(self, **search):
"""
Get an existing not consumed source in the database
:param ** search: the criterions of the lookup
:rtype: sakia.data.entities.Source
"""
filters = []
values = []
for k, v in search.items():
filters.append("{k}=?".format(k=k))
values.append(v)
request = "SELECT * FROM sources WHERE used_by is NULL AND {filters}".format(
filters=" AND ".join(filters)
)
c = self._conn.execute(request, tuple(values))
data = c.fetchone()
if data:
return Source(*data)
def get_all(self, **search):
"""
Get all existing not consumed source in the database corresponding to the search
:param ** search: the criterions of the lookup
:rtype: [sakia.data.entities.Source]
"""
filters = []
values = []
for k, v in search.items():
value = v
filters.append("{key} = ?".format(key=k))
values.append(value)
request = "SELECT * FROM sources WHERE used_by is NULL AND {filters}".format(
filters=" AND ".join(filters)
)
c = self._conn.execute(request, tuple(values))
datas = c.fetchall()
if datas:
return [Source(*data) for data in datas]
return []
def drop(self, source):
"""
Drop an existing source from the database
:param sakia.data.entities.Source source: the source to update
"""
where_fields = attr.astuple(
source, filter=attr.filters.include(*SourcesRepo._primary_keys)
)
self._conn.execute(
"""DELETE FROM sources
WHERE
currency=? AND
pubkey=? AND
identifier=? AND
noffset=?""",
where_fields,
)
def drop_all(self, **filter):
filters = []
values = []
for k, v in filter.items():
value = v
filters.append("{key} = ?".format(key=k))
values.append(value)
request = "DELETE FROM sources WHERE {filters}".format(
filters=" AND ".join(filters)
)
self._conn.execute(request, tuple(values))
def consume(self, source, tx_hash):
"""
Consume a source by setting the used_by column with the tx hash
:param Source source: Source instance to consume
:param str tx_hash: Hash of tx
:return:
"""
where_fields = attr.astuple(
source, filter=attr.filters.include(*SourcesRepo._primary_keys)
)
fields = (tx_hash,) + where_fields
self._conn.execute(
"""UPDATE sources SET used_by=?
WHERE
currency=? AND
pubkey=? AND
identifier=? AND
noffset=?""",
fields,
)
def restore_all(self, tx_hash):
"""
Restore all sources released by tx_hash setting the used_by column with null
:param Source source: Source instance to consume
:param str tx_hash: Hash of tx
:return:
"""
self._conn.execute(
"""UPDATE sources SET used_by=NULL
WHERE
used_by=?""",
(tx_hash,),
)
import attr
from ..entities import Transaction, Dividend
@attr.s(frozen=True)
class TransactionsRepo:
"""The repository for Communities entities."""
_conn = attr.ib() # :type sqlite3.Connection
_primary_keys = (
attr.fields(Transaction).currency,
attr.fields(Transaction).pubkey,
attr.fields(Transaction).sha_hash,
)
def insert(self, transaction):
"""
Commit a transaction to the database
:param sakia.data.entities.Transaction transaction: the transaction to commit
"""
transaction_tuple = attr.astuple(transaction, tuple_factory=list)
transaction_tuple[6] = "\n".join([str(n) for n in transaction_tuple[6]])
transaction_tuple[7] = "\n".join([str(n) for n in transaction_tuple[7]])
transaction_tuple[8] = "\n".join([str(n) for n in transaction_tuple[8]])
values = ",".join(["?"] * len(transaction_tuple))
self._conn.execute(
"INSERT INTO transactions VALUES ({0})".format(values), transaction_tuple
)
def update(self, transaction):
"""
Update an existing transaction in the database
:param sakia.data.entities.Transaction transaction: the transaction to update
"""
updated_fields = attr.astuple(
transaction,
filter=attr.filters.exclude(*TransactionsRepo._primary_keys),
tuple_factory=list,
)
updated_fields[3] = "\n".join([str(n) for n in updated_fields[3]])
updated_fields[4] = "\n".join([str(n) for n in updated_fields[4]])
updated_fields[5] = "\n".join([str(n) for n in updated_fields[5]])
where_fields = attr.astuple(
transaction,
filter=attr.filters.include(*TransactionsRepo._primary_keys),
tuple_factory=list,
)
self._conn.execute(
"""UPDATE transactions SET
written_on=?,
blockstamp=?,
ts=?,
signatures=?,
issuers = ?,
receivers = ?,
amount = ?,
amountbase = ?,
comment = ?,
txid = ?,
state = ?,
local = ?,
raw = ?
WHERE
currency=? AND
pubkey=? AND
sha_hash=?""",
updated_fields + where_fields,
)
def get_one(self, **search):
"""
Get an existing transaction in the database
:param ** search: the criterions of the lookup
:rtype: sakia.data.entities.Transaction
"""
filters = []
values = []
for k, v in search.items():
filters.append("{k}=?".format(k=k))
values.append(v)
request = "SELECT * FROM transactions WHERE {filters}".format(
filters=" AND ".join(filters)
)
c = self._conn.execute(request, tuple(values))
data = c.fetchone()
if data:
return Transaction(*data)
def get_all(self, **search):
"""
Get all existing transaction in the database corresponding to the search
:param dict search: the criterions of the lookup
:rtype: sakia.data.entities.Transaction
"""
filters = []
values = []
for k, v in search.items():
value = v
filters.append("{key} = ?".format(key=k))
values.append(value)
request = "SELECT * FROM transactions WHERE {filters}".format(
filters=" AND ".join(filters)
)
c = self._conn.execute(request, tuple(values))
datas = c.fetchall()
if datas:
return [Transaction(*data) for data in datas]
return []
def get_transfers(
self,
currency,
pubkey,
offset=0,
limit=1000,
sort_by="currency",
sort_order="ASC",
):
"""
Get all transfers in the database on a given currency from or to a pubkey
:param str pubkey: the criterions of the lookup
:rtype: List[sakia.data.entities.Transaction]
"""
request = """SELECT * FROM transactions
WHERE currency=? AND pubkey=?
ORDER BY {sort_by} {sort_order}
LIMIT {limit} OFFSET {offset}""".format(
offset=offset, limit=limit, sort_by=sort_by, sort_order=sort_order
)
c = self._conn.execute(request, (currency, pubkey))
datas = c.fetchall()
if datas:
return [Transaction(*data) for data in datas]
return []
def drop(self, transaction):
"""
Drop an existing transaction from the database
:param sakia.data.entities.Transaction transaction: the transaction to update
"""
where_fields = attr.astuple(
transaction, filter=attr.filters.include(*TransactionsRepo._primary_keys)
)
self._conn.execute(
"""DELETE FROM transactions
WHERE
currency=? AND
pubkey=? AND
sha_hash=?""",
where_fields,
)
import asyncio
import functools
import logging
def cancel_once_task(object, fn):
if getattr(object, "__tasks", None):
tasks = getattr(object, "__tasks")
if fn.__name__ in tasks and not tasks[fn.__name__].done():
logging.debug("Cancelling {0} ".format(fn.__name__))
getattr(object, "__tasks")[fn.__name__].cancel()
def once_at_a_time(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
def task_done(task):
try:
func_call = args[0].__tasks[fn.__name__]
args[0].__tasks.pop(fn.__name__)
if getattr(func_call, "_next_task", None):
start_task(
func_call._next_task[0],
*func_call._next_task[1],
**func_call._next_task[2]
)
except KeyError:
logging.debug("Task {0} already removed".format(fn.__name__))
def start_task(f, *a, **k):
args[0].__tasks[f.__name__] = f(*a, **k)
args[0].__tasks[f.__name__].add_done_callback(task_done)
if getattr(args[0], "__tasks", None) is None:
setattr(args[0], "__tasks", {})
if fn.__name__ in args[0].__tasks:
args[0].__tasks[fn.__name__]._next_task = (fn, args, kwargs)
args[0].__tasks[fn.__name__].cancel()
else:
start_task(fn, *args, **kwargs)
return args[0].__tasks[fn.__name__]
return wrapper
def asyncify(fn):
"""
Instanciates a coroutine in a task
:param fn: the coroutine to run
:return: the task
:rtype: asyncio.Task
"""
@functools.wraps(fn)
def wrapper(*args, **kwargs):
return asyncio.ensure_future(asyncio.coroutine(fn)(*args, **kwargs))
return wrapper
"""
Created on 9 févr. 2014
@author: inso
"""
class Error(Exception):
def __init__(self, message):
"""
Constructor
"""
self.message = "Error: " + message
def __str__(self):
return self.message
class NotEnoughChangeError(Error):
"""
Exception raised when trying to send money but user
is missing change
"""
def __init__(self, available, currency, nb_inputs, requested):
"""
Constructor
"""
super().__init__(
"Only {0} {1} available in {2} sources, needs {3}".format(
available, currency, nb_inputs, requested
)
)
class NoPeerAvailable(Error):
"""
Exception raised when a community doesn't have any
peer available.
"""
def __init__(self, currency, nbpeers):
"""
Constructor
"""
super().__init__(
"No peer answered in {0} community ({1} peers available)".format(
currency, nbpeers
)
)
class InvalidNodeCurrency(Error):
"""
Exception raised when a node doesn't use the intended currency
"""
def __init__(self, currency, node_currency):
"""
Constructor
"""
super().__init__(
"Node is working for {0} currency, but should be {1}".format(
node_currency, currency
)
)
<!DOCTYPE html>
<!--Converted via md-to-html-->
<html>
<head>
</head>
<body>
<p>
License Ğ1 - v0.2.5
</p>
<p>
===================
</p>
<p>
<strong>
Money licensing and liability commitment.
</strong>
</p>
<p>
Any certification operation of a new member of Ğ1 must first be accompanied by the transmission of this license of the currency Ğ1 whose certifier must ensure that it has been studied, understood and accepted by the person who will be certified.
</p>
<p>
Money Ğ1
</p>
<hr/>
<p>
Ğ1 occurs via a Universal Dividend (DU) for any human member, which is of the form:
</p>
<ul>
<li>
1 DU per person per day
</li>
</ul>
<p>
The amount of DU is identical each day until the next equinox, where the DU will then be reevaluated according to the formula:
</p>
<ul>
<li>
DU &lt;sub&gt;day&lt;/sub&gt; (the following equinox) = DU &lt;day&gt;(equinox) + c² (M / N) (equinox) / (15778800 seconds)&lt;/day&gt;
</li>
</ul>
<p>
With as parameters:
</p>
<ul>
<li>
c = 4.88% / equinox
</li>
</ul>
<ul>
<li>
UD (0) = 10.00 Ğ1
</li>
</ul>
<p>
And as variables:
</p>
<ul>
<li>
<em>
M
</em>
the total monetary mass at the equinox
</li>
</ul>
<ul>
<li>
<em>
N
</em>
the number of members at the equinox
</li>
</ul>
<p>
Web of Trust Ğ1 (WoT Ğ1)
</p>
<hr/>
<p>
<strong>
Warning:
</strong>
Certifying is not just about making sure you've met the person, it's ensuring that the community Ğ1 knows the certified person well enough and Duplicate account made by a person certified by you, or other types of problems (disappearance ...), by cross-checking that will reveal the problem if necessary.
</p>
<p>
When you are a member of Ğ1 and you are about to certify a new account:
</p>
<p>
<strong>
You are assured:
</strong>
</p>
<p>
1°) The person who declares to manage this public key (new account) and to have personally checked with him that this is the public key is sufficiently well known (not only to know this person visually) that you are about to certify.
</p>
<p>
2a°) To meet her physically to make sure that it is this person you know who manages this public key.
</p>
<p>
2b°) Remotely verify the public person / key link by contacting the person via several different means of communication, such as social network + forum + mail + video conference + phone (acknowledge voice).
</p>
<p>
Because if you can hack an email account or a forum account, it will be much harder to imagine hacking four distinct means of communication, and mimic the appearance (video) as well as the voice of the person .
</p>
<p>
However, the 2 °) is preferable to 3 °, whereas the 1 °) is always indispensable in all cases.
</p>
<p>
3 °) To have verified with the person concerned that he has indeed generated his Duniter account revocation document, which will enable him, if necessary, to cancel his account (in case of account theft, ID, an incorrectly created account, etc.).
</p>
<p>
<strong>
Abbreviated WoT rules:
</strong>
</p>
<p>
Each member has a stock of 100 possible certifications, which can only be issued at the rate of 1 certification / 5 days.
</p>
<p>
Valid for 2 months, certification for a new member is definitively adopted only if the certified has at least 4 other certifications after these 2 months, otherwise the entry process will have to be relaunched.
</p>
<p>
To become a new member of WoT Ğ1 therefore 5 certifications must be obtained at a distance &lt; 5 of 80% of the WoT sentinels.
</p>
<p>
A member of the TdC Ğ1 is sentinel when he has received and issued at least Y [N] certifications where N is the number of members of the TdC and Y [N] = ceiling N ^ (1/5). Examples:
</p>
<ul>
<li>
For 1024 &lt; N ≤ 3125 we have Y [N] = 5
</li>
</ul>
<ul>
<li>
For 7776 &lt; N ≤ 16807 we have Y [N] = 7
</li>
</ul>
<ul>
<li>
For 59049 &lt; N ≤ 100 000 we have Y [N] = 10
</li>
</ul>
<p>
Once the new member is part of the WoT Ğ1 his certifications remain valid for 2 years.
</p>
<p>
To remain a member, you must renew your agreement regularly with your private key (every 12 months) and make sure you have at least 5 certifications valid after 2 years.
</p>
<p>
Software Ğ1 and license Ğ1
</p>
<hr/>
<p>
The software Ğ1 allowing users to manage their use of Ğ1 must transmit this license with the software and all the technical parameters of the currency Ğ1 and TdC Ğ1 which are entered in block 0 of Ğ1.
</p>
<p>
For more details in the technical details it is possible to consult directly the code of Duniter which is a free software and also the data of the blockchain Ğ1 by retrieving it via a Duniter instance or node Ğ1.
</p>
<p>
More information on the Duniter Team website
<a href="https://www.duniter.org">
https://www.duniter.org
</a>
</p>
</body>
</html>
\ No newline at end of file
''' """
Created on 11 mars 2014 Created on 11 mars 2014
@author: inso @author: inso
''' """
from PyQt5 import QtSvg
from .controller import ConnectionConfigController
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CongratulationPopup</class>
<widget class="QDialog" name="CongratulationPopup">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>350</width>
<height>198</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>350</width>
<height>16777215</height>
</size>
</property>
<property name="windowTitle">
<string>Congratulation</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>label</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="alignment">
<set>Qt::AlignJustify|Qt::AlignTop</set>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::TextBrowserInteraction</set>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Ok</set>
</property>
<property name="centerButtons">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>CongratulationPopup</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ConnectionConfigurationDialog</class>
<widget class="QDialog" name="ConnectionConfigurationDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>623</width>
<height>577</height>
</rect>
</property>
<property name="windowTitle">
<string>Add an account</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QStackedWidget" name="stacked_pages">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="page_node">
<layout class="QVBoxLayout" name="verticalLayout_12">
<item>
<layout class="QVBoxLayout" name="verticalLayout_13">
<property name="topMargin">
<number>6</number>
</property>
<item>
<widget class="QPushButton" name="button_pubkey">
<property name="text">
<string>Add using a public key (quick)</string>
</property>
<property name="icon">
<iconset resource="../../../../../res/icons/sakia.icons.qrc">
<normaloff>:/icons/duniter_info_icon</normaloff>:/icons/duniter_info_icon</iconset>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_wallet">
<property name="text">
<string>Add a wallet</string>
</property>
<property name="icon">
<iconset resource="../../../../../res/icons/sakia.icons.qrc">
<normaloff>:/icons/wallet_icon</normaloff>:/icons/wallet_icon</iconset>
</property>
<property name="iconSize">
<size>
<width>50</width>
<height>32</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_connect">
<property name="text">
<string>Add an existing member account</string>
</property>
<property name="icon">
<iconset resource="../../../../../res/icons/sakia.icons.qrc">
<normaloff>:/icons/connect_icon</normaloff>:/icons/connect_icon</iconset>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_register">
<property name="text">
<string>Create a new member account</string>
</property>
<property name="icon">
<iconset resource="../../../../../res/icons/sakia.icons.qrc">
<normaloff>:/icons/new_membership</normaloff>:/icons/new_membership</iconset>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_8">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_licence">
<layout class="QVBoxLayout" name="verticalLayout_8">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:large; font-weight:600;&quot;&gt;Licence&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item>
<widget class="QTextEdit" name="text_license">
<property name="html">
<string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot; margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'Hack'; font-size:10pt;&quot;&gt; This program is free software: you can redistribute it and/or modify&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'Hack'; font-size:10pt;&quot;&gt; it under the terms of the GNU General Public License as published by&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'Hack'; font-size:10pt;&quot;&gt; the Free Software Foundation, either version 3 of the License, or&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'Hack'; font-size:10pt;&quot;&gt; (at your option) any later version.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Hack'; font-size:10pt;&quot;&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'Hack'; font-size:10pt;&quot;&gt; This program is distributed in the hope that it will be useful,&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'Hack'; font-size:10pt;&quot;&gt; but WITHOUT ANY WARRANTY; without even the implied warranty of&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'Hack'; font-size:10pt;&quot;&gt; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'Hack'; font-size:10pt;&quot;&gt; GNU General Public License for more details.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Hack'; font-size:10pt;&quot;&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'Hack'; font-size:10pt;&quot;&gt; You should have received a copy of the GNU General Public License&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'Hack'; font-size:10pt;&quot;&gt; along with this program. If not, see &amp;lt;http://www.gnu.org/licenses/&amp;gt;. &lt;/span&gt;&lt;a name=&quot;TransNote1-rev&quot;&gt;&lt;/a&gt;&lt;a href=&quot;https://www.gnu.org/licenses/gpl-howto.fr.html#TransNote1&quot;&gt;&lt;span style=&quot; font-family:'Hack'; font-size:10pt; text-decoration: underline; color:#2980b9; vertical-align:super;&quot;&gt;1&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>By going throught the process of creating a wallet, you accept the licence above.</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<property name="topMargin">
<number>6</number>
</property>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="button_accept">
<property name="text">
<string>I accept the above licence</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_connection">
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Account parameters</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_action">
<property name="text">
<string>Identity name (UID)</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="edit_uid"/>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="layout_key">
<item>
<layout class="QVBoxLayout" name="verticalLayout_5">
<property name="topMargin">
<number>6</number>
</property>
<item>
<widget class="QGroupBox" name="groupbox_pubkey">
<property name="title">
<string>Public key</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_7">
<item>
<widget class="QLineEdit" name="edit_pubkey">
<property name="placeholderText">
<string>Public key</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupbox_key">
<property name="title">
<string>Credentials</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<widget class="QLineEdit" name="edit_salt">
<property name="text">
<string/>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
<property name="placeholderText">
<string>Secret key</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="edit_salt_bis">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
<property name="placeholderText">
<string>Please repeat your secret key</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="edit_password">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
<property name="placeholderText">
<string>Your password</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="edit_password_repeat">
<property name="text">
<string/>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
<property name="placeholderText">
<string>Please repeat your password</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_generate">
<property name="text">
<string>Show public key</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="topMargin">
<number>6</number>
</property>
<item>
<widget class="QLabel" name="label_6">
<property name="text">
<string>Scrypt parameters</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="combo_scrypt_params">
<item>
<property name="text">
<string>Simple</string>
</property>
</item>
<item>
<property name="text">
<string>Secure</string>
</property>
</item>
<item>
<property name="text">
<string>Hardest</string>
</property>
</item>
<item>
<property name="text">
<string>Extreme</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>N</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spin_n"/>
</item>
<item>
<widget class="QLabel" name="label_4">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>r</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spin_r"/>
</item>
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>p</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spin_p">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<property name="topMargin">
<number>5</number>
</property>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="button_next">
<property name="text">
<string>Export revocation document to continue</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_services">
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QProgressBar" name="progress_bar">
<property name="value">
<number>24</number>
</property>
</widget>
</item>
<item>
<widget class="QPlainTextEdit" name="plain_text_edit">
<property name="readOnly">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QLabel" name="label_currency">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_info">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../../../../../res/icons/sakia.icons.qrc"/>
</resources>
<connections/>
<slots>
<slot>open_process_add_community()</slot>
<slot>key_changed(int)</slot>
<slot>action_remove_community()</slot>
<slot>open_process_edit_community(QModelIndex)</slot>
<slot>next()</slot>
<slot>previous()</slot>
<slot>open_import_key()</slot>
<slot>open_generate_account_key()</slot>
<slot>action_edit_account_key()</slot>
<slot>action_edit_account_parameters()</slot>
<slot>action_show_pubkey()</slot>
<slot>action_delete_account()</slot>
</slots>
</ui>
import asyncio
import logging
from PyQt5.QtCore import QObject, Qt, QCoreApplication
from aiohttp import ClientError
from asyncio import TimeoutError
from sakia.gui.widgets.dialogs import dialog_async_exec, QAsyncFileDialog, QMessageBox
from duniterpy.api.errors import DuniterError
from duniterpy.documents import MalformedDocumentError
from sakia.data.connectors import BmaConnector
from sakia.data.processors import IdentitiesProcessor, NodesProcessor
from sakia.decorators import asyncify
from sakia.errors import NoPeerAvailable
from sakia.helpers import detect_non_printable
from .model import ConnectionConfigModel
from .view import ConnectionConfigView
class ConnectionConfigController(QObject):
"""
The AccountConfigController view
"""
CONNECT = 0
REGISTER = 1
WALLET = 2
PUBKEY = 3
def __init__(self, parent, view, model):
"""
Constructor of the AccountConfigController component
:param sakia.gui.dialogs.connection_cfg.view.ConnectionConfigView: the view
:param sakia.gui.dialogs.connection_cfg.model.ConnectionConfigView model: the model
"""
super().__init__(parent)
self.view = view
self.model = model
self.mode = -1
self.step_node = asyncio.Future()
self.step_licence = asyncio.Future()
self.step_key = asyncio.Future()
self.view.button_connect.clicked.connect(
lambda: self.step_node.set_result(ConnectionConfigController.CONNECT)
)
self.view.button_register.clicked.connect(
lambda: self.step_node.set_result(ConnectionConfigController.REGISTER)
)
self.view.button_wallet.clicked.connect(
lambda: self.step_node.set_result(ConnectionConfigController.WALLET)
)
self.view.button_pubkey.clicked.connect(
lambda: self.step_node.set_result(ConnectionConfigController.PUBKEY)
)
self.view.values_changed.connect(
lambda: self.view.button_next.setEnabled(self.check_key())
)
self.view.values_changed.connect(
lambda: self.view.button_generate.setEnabled(self.check_key())
)
self._logger = logging.getLogger("sakia")
@classmethod
def create(cls, parent, app):
"""
Instanciate a AccountConfigController component
:param sakia.gui.component.controller.ComponentController parent:
:param sakia.app.Application app:
:return: a new AccountConfigController controller
:rtype: AccountConfigController
"""
view = ConnectionConfigView(parent.view if parent else None, app)
model = ConnectionConfigModel(
None, app, None, IdentitiesProcessor.instanciate(app)
)
account_cfg = cls(parent, view, model)
model.setParent(account_cfg)
view.set_license(app.currency)
return account_cfg
@classmethod
def create_connection(cls, parent, app):
"""
Open a dialog to create a new account
:param parent:
:param app:
:return:
"""
connection_cfg = cls.create(parent, app)
connection_cfg.view.set_creation_layout(app.currency)
asyncio.ensure_future(connection_cfg.process())
return connection_cfg
def init_nodes_page(self):
self.view.set_steps_buttons_visible(True)
model = self.model.init_nodes_model()
self.view.tree_peers.customContextMenuRequested(self.show_context_menu)
self.view.set_nodes_model(model)
self.view.button_previous.setEnabled(False)
self.view.button_next.setText(
QCoreApplication.translate("ConnectionConfigController", "Ok")
)
def init_name_page(self):
"""
Initialize an account name page
"""
if self.model.connection:
self.view.set_account_name(self.model.connection.uid)
self.view.button_previous.setEnabled(False)
self.view.button_next.setEnabled(False)
def check_name(self):
return len(self.view.edit_account_name.text()) > 2
async def process(self):
self._logger.debug("Begin process")
if self.model.connection:
self.mode = await self.step_node
else:
while not self.model.connection:
self.mode = await self.step_node
self._logger.debug("Create account")
try:
self.view.button_connect.setEnabled(False)
self.view.button_register.setEnabled(False)
await self.model.create_connection()
except (
ClientError,
MalformedDocumentError,
ValueError,
TimeoutError,
) as e:
self._logger.debug(str(e))
self.view.display_info(
QCoreApplication.translate(
"ConnectionConfigController",
"Could not connect. Check hostname, IP address or port: <br/>"
+ str(e),
)
)
self.step_node = asyncio.Future()
self.view.button_connect.setEnabled(True)
self.view.button_register.setEnabled(True)
if self.mode == ConnectionConfigController.REGISTER:
self._logger.debug("Licence step")
self.view.stacked_pages.setCurrentWidget(self.view.page_licence)
self.view.button_accept.clicked.connect(
lambda: self.step_licence.set_result(True)
)
await self.step_licence
self.view.button_accept.disconnect()
self._logger.debug("Key step")
self.view.set_currency(self.model.connection.currency)
connection_identity = None
self.view.button_next.setEnabled(self.check_key())
if self.mode == ConnectionConfigController.REGISTER:
self._logger.debug("Registering mode")
self.view.groupbox_pubkey.hide()
self.view.button_next.clicked.connect(self.check_register)
self.view.stacked_pages.setCurrentWidget(self.view.page_connection)
connection_identity = await self.step_key
elif self.mode == ConnectionConfigController.CONNECT:
self._logger.debug("Connect mode")
self.view.button_next.setText(
QCoreApplication.translate("ConnectionConfigController", "Next")
)
self.view.groupbox_pubkey.hide()
self.view.button_next.clicked.connect(self.check_connect)
self.view.stacked_pages.setCurrentWidget(self.view.page_connection)
connection_identity = await self.step_key
elif self.mode == ConnectionConfigController.WALLET:
self._logger.debug("Wallet mode")
self.view.button_next.setText(
QCoreApplication.translate("ConnectionConfigController", "Next")
)
self.view.button_next.clicked.connect(self.check_wallet)
self.view.edit_uid.hide()
self.view.label_action.hide()
self.view.groupbox_pubkey.hide()
self.view.stacked_pages.setCurrentWidget(self.view.page_connection)
connection_identity = await self.step_key
elif self.mode == ConnectionConfigController.PUBKEY:
self._logger.debug("Pubkey mode")
self.view.button_next.setText(
QCoreApplication.translate("ConnectionConfigController", "Next")
)
self.view.button_next.clicked.connect(self.check_pubkey)
if not self.view.label_action.text().endswith(
QCoreApplication.translate("ConnectionConfigController", " (Optional)")
):
self.view.label_action.setText(
self.view.label_action.text()
+ QCoreApplication.translate(
"ConnectionConfigController", " (Optional)"
)
)
self.view.groupbox_key.hide()
self.view.stacked_pages.setCurrentWidget(self.view.page_connection)
connection_identity = await self.step_key
self.view.stacked_pages.setCurrentWidget(self.view.page_services)
self.view.set_progress_steps(6)
try:
if self.mode == ConnectionConfigController.REGISTER:
self.view.display_info(
QCoreApplication.translate(
"ConnectionConfigController", "Broadcasting identity..."
)
)
self.view.stream_log("Broadcasting identity...")
result = await self.model.publish_selfcert(connection_identity)
if result[0]:
await self.view.show_success(self.model.notification())
else:
self.view.show_error(self.model.notification(), result[1])
raise StopIteration()
self.view.set_step(1)
if (
self.mode
in (
ConnectionConfigController.REGISTER,
ConnectionConfigController.CONNECT,
ConnectionConfigController.PUBKEY,
)
and connection_identity
):
self.view.stream_log("Saving identity...")
self.model.connection.blockstamp = connection_identity.blockstamp
self.model.insert_or_update_connection()
self.model.insert_or_update_identity(connection_identity)
self.view.stream_log("Initializing identity informations...")
await self.model.initialize_identity(
connection_identity,
log_stream=self.view.stream_log,
progress=self.view.progress,
)
self.view.stream_log("Initializing certifications informations...")
self.view.set_step(2)
await self.model.initialize_certifications(
connection_identity,
log_stream=self.view.stream_log,
progress=self.view.progress,
)
self.view.set_step(3)
self.view.stream_log("Initializing transactions history...")
transactions = await self.model.initialize_transactions(
self.model.connection,
log_stream=self.view.stream_log,
progress=self.view.progress,
)
self.view.set_step(4)
self.view.stream_log("Initializing dividends history...")
dividends = await self.model.initialize_dividends(
self.model.connection,
transactions,
log_stream=self.view.stream_log,
progress=self.view.progress,
)
self.view.set_step(5)
await self.model.initialize_sources(
log_stream=self.view.stream_log, progress=self.view.progress
)
self.view.set_step(6)
self._logger.debug("Validate changes")
self.model.insert_or_update_connection()
self.model.app.db.commit()
if self.mode == ConnectionConfigController.REGISTER:
await self.view.show_register_message(
self.model.blockchain_parameters()
)
except (NoPeerAvailable, DuniterError, StopIteration) as e:
if not isinstance(e, StopIteration):
self.view.show_error(self.model.notification(), str(e))
self._logger.debug(str(e))
self.view.stacked_pages.setCurrentWidget(self.view.page_connection)
self.step_node = asyncio.Future()
self.step_node.set_result(self.mode)
self.step_key = asyncio.Future()
self.view.button_next.disconnect()
self.view.edit_uid.show()
asyncio.ensure_future(self.process())
return
await self.accept()
def check_key(self):
if self.mode == ConnectionConfigController.PUBKEY:
if len(self.view.edit_pubkey.text()) < 42:
self.view.label_info.setText(
QCoreApplication.translate(
"ConnectionConfigController", "Forbidden: pubkey is too short"
)
)
return False
if len(self.view.edit_pubkey.text()) > 45:
self.view.label_info.setText(
QCoreApplication.translate(
"ConnectionConfigController", "Forbidden: pubkey is too long"
)
)
return False
else:
if self.view.edit_password.text() != self.view.edit_password_repeat.text():
self.view.label_info.setText(
QCoreApplication.translate(
"ConnectionConfigController", "Error: passwords are different"
)
)
return False
if self.view.edit_salt.text() != self.view.edit_salt_bis.text():
self.view.label_info.setText(
QCoreApplication.translate(
"ConnectionConfigController", "Error: salts are different"
)
)
return False
if detect_non_printable(self.view.edit_salt.text()):
self.view.label_info.setText(
QCoreApplication.translate(
"ConnectionConfigController",
"Forbidden: invalid characters in salt",
)
)
return False
if detect_non_printable(self.view.edit_password.text()):
self.view.label_info.setText(
QCoreApplication.translate(
"ConnectionConfigController",
"Forbidden: invalid characters in password",
)
)
return False
if self.model.app.parameters.expert_mode:
self.view.label_info.setText(
QCoreApplication.translate("ConnectionConfigController", "")
)
return True
if len(self.view.edit_salt.text()) < 6:
self.view.label_info.setText(
QCoreApplication.translate(
"ConnectionConfigController", "Forbidden: salt is too short"
)
)
return False
if len(self.view.edit_password.text()) < 6:
self.view.label_info.setText(
QCoreApplication.translate(
"ConnectionConfigController", "Forbidden: password is too short"
)
)
return False
self.view.label_info.setText("")
return True
async def action_save_revocation(self):
raw_document, identity = self.model.generate_revocation()
# Testable way of using a QFileDialog
selected_files = await QAsyncFileDialog.get_save_filename(
self.view,
QCoreApplication.translate(
"ConnectionConfigController", "Save a revocation document"
),
"revocation-{uid}-{pubkey}-{currency}.txt".format(
uid=identity.uid, pubkey=identity.pubkey[:8], currency=identity.currency
),
QCoreApplication.translate(
"ConnectionConfigController", "All text files (*.txt)"
),
)
if selected_files:
path = selected_files[0]
if not path.endswith(".txt"):
path = "{0}.txt".format(path)
with open(path, "w") as save_file:
save_file.write(raw_document)
dialog = QMessageBox(
QMessageBox.Information,
QCoreApplication.translate(
"ConnectionConfigController", "Revocation file"
),
QCoreApplication.translate(
"ConnectionConfigController",
"""<div>Your revocation document has been saved.</div>
<div><b>Please keep it in a safe place.</b></div>
The publication of this document will revoke your identity on the network.</p>""",
),
QMessageBox.Ok,
)
dialog.setTextFormat(Qt.RichText)
return True, identity
return False, identity
@asyncify
async def check_pubkey(self, checked=False):
self._logger.debug("Is valid ? ")
self.view.display_info(
QCoreApplication.translate("ConnectionConfigController", "connecting...")
)
self.view.button_next.setDisabled(True)
try:
self.model.set_pubkey(self.view.edit_pubkey.text(), self.view.scrypt_params)
self.model.set_uid(self.view.edit_uid.text())
if not self.model.key_exists():
try:
registered, found_identity = await self.model.check_registered()
self.view.button_connect.setEnabled(True)
self.view.button_register.setEnabled(True)
if self.view.edit_uid.text():
if registered[0] is False and registered[2] is None:
self.view.display_info(
QCoreApplication.translate(
"ConnectionConfigController",
"Could not find your identity on the network.",
)
)
elif registered[0] is False and registered[2]:
self.view.display_info(
QCoreApplication.translate(
"ConnectionConfigController",
"""Your pubkey or UID is different on the network.
Yours: {0}, the network: {1}""".format(
registered[1], registered[2]
),
)
)
else:
self.step_key.set_result(found_identity)
else:
self.step_key.set_result(None)
except DuniterError as e:
self.view.display_info(e.message)
self.step_key.set_result(None)
except NoPeerAvailable as e:
self.view.display_info(str(e))
self.step_key.set_result(None)
else:
self.view.display_info(
QCoreApplication.translate(
"ConnectionConfigController",
"An account already exists using this key.",
)
)
except NoPeerAvailable:
self.config_dialog.label_error.setText(
QCoreApplication.translate(
"ConnectionConfigController",
"Could not connect. Check node peering entry",
)
)
@asyncify
async def check_wallet(self, checked=False):
self._logger.debug("Is valid ? ")
self.view.display_info(
QCoreApplication.translate("ConnectionConfigController", "connecting...")
)
try:
salt = self.view.edit_salt.text()
password = self.view.edit_password.text()
self.model.set_scrypt_infos(salt, password, self.view.scrypt_params)
self.model.set_uid("")
if not self.model.key_exists():
try:
registered, found_identity = await self.model.check_registered()
self.view.button_connect.setEnabled(True)
self.view.button_register.setEnabled(True)
if registered[0] is False and registered[2] is None:
self.step_key.set_result(None)
elif registered[2]:
self.view.display_info(
QCoreApplication.translate(
"ConnectionConfigController",
"""Your pubkey is associated to an identity.
Yours: {0}, the network: {1}""".format(
registered[1], registered[2]
),
)
)
except DuniterError as e:
self.view.display_info(e.message)
except NoPeerAvailable as e:
self.view.display_info(str(e))
else:
self.view.display_info(
QCoreApplication.translate(
"ConnectionConfigController",
"An account already exists using this key.",
)
)
except NoPeerAvailable:
self.config_dialog.label_error.setText(
QCoreApplication.translate(
"ConnectionConfigController",
"Could not connect. Check node peering entry",
)
)
@asyncify
async def check_connect(self, checked=False):
self._logger.debug("Is valid ? ")
self.view.display_info(
QCoreApplication.translate("ConnectionConfigController", "connecting...")
)
try:
salt = self.view.edit_salt.text()
password = self.view.edit_password.text()
self.model.set_scrypt_infos(salt, password, self.view.scrypt_params)
self.model.set_uid(self.view.edit_uid.text())
if not self.model.key_exists():
try:
registered, found_identity = await self.model.check_registered()
self.view.button_connect.setEnabled(True)
self.view.button_register.setEnabled(True)
if registered[0] is False and registered[2] is None:
self.view.display_info(
QCoreApplication.translate(
"ConnectionConfigController",
"Could not find your identity on the network.",
)
)
elif registered[0] is False and registered[2]:
self.view.display_info(
QCoreApplication.translate(
"ConnectionConfigController",
"""Your pubkey or UID is different on the network.
Yours: {0}, the network: {1}""".format(
registered[1], registered[2]
),
)
)
else:
self.step_key.set_result(found_identity)
except DuniterError as e:
self.view.display_info(e.message)
except NoPeerAvailable as e:
self.view.display_info(str(e))
else:
self.view.display_info(
QCoreApplication.translate(
"ConnectionConfigController",
"An account already exists using this key.",
)
)
except NoPeerAvailable:
self.config_dialog.label_error.setText(
QCoreApplication.translate(
"ConnectionConfigController",
"Could not connect. Check node peering entry",
)
)
@asyncify
async def check_register(self, checked=False):
self._logger.debug("Is valid ? ")
self.view.display_info(
QCoreApplication.translate("ConnectionConfigController", "connecting...")
)
try:
salt = self.view.edit_salt.text()
password = self.view.edit_password.text()
self.model.set_scrypt_infos(salt, password, self.view.scrypt_params)
self.model.set_uid(self.view.edit_uid.text())
if not self.model.key_exists():
try:
registered, found_identity = await self.model.check_registered()
if registered[0] is False and registered[2] is None:
result, identity = await self.action_save_revocation()
if result:
self.step_key.set_result(identity)
else:
self.view.display_info(
"Saving your revocation document on your disk is mandatory."
)
elif registered[0] is False and registered[2]:
self.view.display_info(
QCoreApplication.translate(
"ConnectionConfigController",
"""Your pubkey or UID was already found on the network.
Yours: {0}, the network: {1}""".format(
registered[1], registered[2]
),
)
)
else:
self.view.display_info(
"Your account already exists on the network"
)
except DuniterError as e:
self.view.display_info(e.message)
except NoPeerAvailable as e:
self.view.display_info(str(e))
else:
self.view.display_info(
QCoreApplication.translate(
"ConnectionConfigController",
"An account already exists using this key.",
)
)
except NoPeerAvailable:
self.view.display_info(
QCoreApplication.translate(
"ConnectionConfigController",
"Could not connect. Check node peering entry",
)
)
@asyncify
async def accept(self):
self.view.accept()
def async_exec(self):
future = asyncio.Future()
self.view.finished.connect(lambda r: future.set_result(r))
self.view.open()
return future
def exec(self):
return self.view.exec()
import aiohttp
from PyQt5.QtCore import QObject
from duniterpy.key import SigningKey
from sakia.data.entities import Connection
from sakia.data.processors import (
ConnectionsProcessor,
BlockchainProcessor,
SourcesProcessor,
TransactionsProcessor,
DividendsProcessor,
IdentitiesProcessor,
)
class ConnectionConfigModel(QObject):
"""
The model of AccountConfig component
"""
def __init__(
self, parent, app, connection, identities_processor, node_connector=None
):
"""
:param sakia.gui.dialogs.account_cfg.controller.AccountConfigController parent:
:param sakia.app.Application app: the main application
:param sakia.data.entities.Connection connection: the connection
:param sakia.data.processors.IdentitiesProcessor identities_processor: the identities processor
:param sakia.data.connectors.NodeConnector node_connector: the node connector
"""
super().__init__(parent)
self.app = app
self.connection = connection
self.identities_processor = identities_processor
async def create_connection(self):
self.connection = Connection(self.app.currency, "", "")
def notification(self):
return self.app.parameters.notifications
def set_uid(self, uid):
self.connection.uid = uid
def set_scrypt_infos(self, salt, password, scrypt_params):
self.connection.salt = salt
self.connection.scrypt_N = scrypt_params.N
self.connection.scrypt_r = scrypt_params.r
self.connection.scrypt_p = scrypt_params.p
self.connection.password = password
self.connection.pubkey = SigningKey.from_credentials(
self.connection.salt, password, scrypt_params
).pubkey
def set_pubkey(self, pubkey, scrypt_params):
self.connection.salt = ""
self.connection.scrypt_N = scrypt_params.N
self.connection.scrypt_r = scrypt_params.r
self.connection.scrypt_p = scrypt_params.p
self.connection.password = ""
self.connection.pubkey = pubkey
def insert_or_update_connection(self):
ConnectionsProcessor(self.app.db.connections_repo).commit_connection(
self.connection
)
def insert_or_update_identity(self, identity):
self.identities_processor.insert_or_update_identity(identity)
def generate_revocation(self):
return self.app.documents_service.generate_revocation(
self.connection, self.connection.salt, self.connection.password
)
def generate_identity(self):
return self.app.documents_service.generate_identity(self.connection)
async def initialize_blockchain(self, log_stream):
"""
Download blockchain information locally
:param function log_stream: a method to log data in the screen
:return:
"""
blockchain_processor = BlockchainProcessor.instanciate(self.app)
await blockchain_processor.initialize_blockchain(self.app.currency)
async def initialize_sources(self, log_stream, progress):
"""
Download sources information locally
:param function log_stream: a method to log data in the screen
:param function progress: a callback to display progress
:return:
"""
log_stream("Parsing sources...")
await self.app.sources_service.initialize_sources(
self.connection.pubkey, log_stream, progress
)
log_stream("Sources parsed succefully !")
async def initialize_identity(self, identity, log_stream, progress):
"""
Download identity information locally
:param sakia.data.entities.Identity identity: the identity to initialize
:param function log_stream: a method to log data in the screen
:return:
"""
await self.identities_processor.initialize_identity(
identity, log_stream, progress
)
async def initialize_certifications(self, identity, log_stream, progress):
"""
Download certifications information locally
:param sakia.data.entities.Identity identity: the identity to initialize
:param function log_stream: a method to log data in the screen
:return:
"""
await self.app.identities_service.initialize_certifications(
identity, log_stream, progress
)
async def initialize_transactions(self, identity, log_stream, progress):
"""
Download certifications information locally
:param sakia.data.entities.Identity identity: the identity to initialize
:param function log_stream: a method to log data in the screen
:return:
"""
transactions_processor = TransactionsProcessor.instanciate(self.app)
return await transactions_processor.initialize_transactions(
identity, log_stream, progress
)
async def initialize_dividends(self, identity, transactions, log_stream, progress):
"""
Download certifications information locally
:param sakia.data.entities.Identity identity: the identity to initialize
:param List[sakia.data.entities.Transaction] transactions: the list of transactions found by tx processor
:param function log_stream: a method to log data in the screen
:return:
"""
dividends_processor = DividendsProcessor.instanciate(self.app)
return await dividends_processor.initialize_dividends(
identity, transactions, log_stream, progress
)
async def publish_selfcert(self, identity):
""" "
Publish the self certification of the connection identity
"""
result = await self.app.documents_service.broadcast_identity(
self.connection, identity.document()
)
return result
async def check_registered(self):
identities_processor = IdentitiesProcessor.instanciate(self.app)
return await identities_processor.check_registered(self.connection)
def key_exists(self):
return (
self.connection.pubkey
in ConnectionsProcessor.instanciate(self.app).pubkeys()
)
def blockchain_parameters(self):
blockchain_processor = BlockchainProcessor.instanciate(self.app)
return blockchain_processor.parameters(self.app.currency)
from PyQt5.QtWidgets import QDialog
from PyQt5.QtCore import pyqtSignal, Qt, QElapsedTimer, QDateTime, QCoreApplication
from .connection_cfg_uic import Ui_ConnectionConfigurationDialog
from .congratulation_uic import Ui_CongratulationPopup
from duniterpy.key import SigningKey
from duniterpy.key.scrypt_params import ScryptParams
from math import ceil, log
from sakia.gui.widgets import toast
from sakia.helpers import timestamp_to_dhms
from sakia.gui.widgets.dialogs import dialog_async_exec, QAsyncMessageBox
from sakia.constants import G1_LICENSE
from sakia.app import Application
class ConnectionConfigView(QDialog, Ui_ConnectionConfigurationDialog):
"""
Connection config view
"""
values_changed = pyqtSignal()
def __init__(self, parent, app: Application):
"""
Init ConnectionConfigView
:param parent: Parent QObject
:param app: Application instance
"""
super().__init__(parent)
self.app = app
self.setupUi(self)
self.last_speed = 0.1
self.average_speed = 1
self.timer = QElapsedTimer()
self.edit_uid.textChanged.connect(self.values_changed)
self.edit_password.textChanged.connect(self.values_changed)
self.edit_password_repeat.textChanged.connect(self.values_changed)
self.edit_salt.textChanged.connect(self.values_changed)
self.edit_pubkey.textChanged.connect(self.values_changed)
self.button_generate.clicked.connect(self.action_show_pubkey)
self.text_license.setReadOnly(True)
self.combo_scrypt_params.currentIndexChanged.connect(self.handle_combo_change)
self.scrypt_params = ScryptParams(4096, 16, 1)
self.spin_n.setMaximum(2 ** 20)
self.spin_n.setValue(self.scrypt_params.N)
self.spin_n.valueChanged.connect(self.handle_n_change)
self.spin_r.setMaximum(128)
self.spin_r.setValue(self.scrypt_params.r)
self.spin_r.valueChanged.connect(self.handle_r_change)
self.spin_p.setMaximum(128)
self.spin_p.setValue(self.scrypt_params.p)
self.spin_p.valueChanged.connect(self.handle_p_change)
self.label_info.setTextFormat(Qt.RichText)
def handle_combo_change(self, index):
strengths = [
(2 ** 12, 16, 1),
(2 ** 14, 32, 2),
(2 ** 16, 32, 4),
(2 ** 18, 64, 8),
]
self.spin_n.blockSignals(True)
self.spin_r.blockSignals(True)
self.spin_p.blockSignals(True)
self.spin_n.setValue(strengths[index][0])
self.spin_r.setValue(strengths[index][1])
self.spin_p.setValue(strengths[index][2])
self.spin_n.blockSignals(False)
self.spin_r.blockSignals(False)
self.spin_p.blockSignals(False)
def set_license(self, currency):
license_text = QCoreApplication.translate("ConnectionConfigView", G1_LICENSE)
self.text_license.setText(license_text)
def handle_n_change(self, value):
spinbox = self.sender()
self.scrypt_params.N = ConnectionConfigView.compute_power_of_2(
spinbox, value, self.scrypt_params.N
)
def handle_r_change(self, value):
spinbox = self.sender()
self.scrypt_params.r = ConnectionConfigView.compute_power_of_2(
spinbox, value, self.scrypt_params.r
)
def handle_p_change(self, value):
spinbox = self.sender()
self.scrypt_params.p = ConnectionConfigView.compute_power_of_2(
spinbox, value, self.scrypt_params.p
)
@staticmethod
def compute_power_of_2(spinbox, value, param):
if value > 1:
if value > param:
value = pow(2, ceil(log(value) / log(2)))
else:
value -= 1
value = 2 ** int(log(value, 2))
else:
value = 1
spinbox.blockSignals(True)
spinbox.setValue(value)
spinbox.blockSignals(False)
return value
def display_info(self, info):
self.label_info.setText(info)
def set_currency(self, currency):
self.label_currency.setText(currency)
def add_node_parameters(self):
server = self.lineedit_add_address.text()
port = self.spinbox_add_port.value()
return server, port
async def show_success(self, notification):
if notification:
toast.display(
QCoreApplication.translate("ConnectionConfigView", "UID broadcast"),
QCoreApplication.translate(
"ConnectionConfigView", "Identity broadcasted to the network"
),
)
else:
await QAsyncMessageBox.information(
self,
QCoreApplication.translate("ConnectionConfigView", "UID broadcast"),
QCoreApplication.translate(
"ConnectionConfigView", "Identity broadcasted to the network"
),
)
def show_error(self, notification, error_txt):
if notification:
toast.display(
QCoreApplication.translate("ConnectionConfigView", "UID broadcast"),
error_txt,
)
self.label_info.setText(
QCoreApplication.translate("ConnectionConfigView", "Error")
+ " "
+ error_txt
)
def set_nodes_model(self, model):
self.tree_peers.setModel(model)
def set_creation_layout(self, currency):
"""
Hide unecessary buttons and display correct title
"""
self.setWindowTitle(
QCoreApplication.translate(
"ConnectionConfigView", "New sakia account on {0} network"
).format(self.app.root_servers[currency]["display"])
)
def action_show_pubkey(self):
salt = self.edit_salt.text()
password = self.edit_password.text()
pubkey = SigningKey.from_credentials(salt, password, self.scrypt_params).pubkey
self.label_info.setText(pubkey)
def account_name(self):
return self.edit_account_name.text()
def set_communities_list_model(self, model):
"""
Set communities list model
:param sakia.models.communities.CommunitiesListModel model:
"""
self.list_communities.setModel(model)
def stream_log(self, log):
"""
Add log to
:param str log:
"""
self.plain_text_edit.appendPlainText(log)
def progress(self, step_ratio):
"""
:param float ratio: the ratio of progress of current step (between 0 and 1)
:return:
"""
SMOOTHING_FACTOR = 0.005
if self.timer.elapsed() > 0:
value = self.progress_bar.value()
next_value = value + 1000000 * step_ratio
speed_percent_by_ms = (next_value - value) / self.timer.elapsed()
self.average_speed = (
SMOOTHING_FACTOR * self.last_speed
+ (1 - SMOOTHING_FACTOR) * self.average_speed
)
remaining = (self.progress_bar.maximum() - next_value) / self.average_speed
self.last_speed = speed_percent_by_ms
displayed_remaining_time = (
QDateTime.fromTime_t(remaining).toUTC().toString("hh:mm:ss")
)
self.progress_bar.setFormat(
QCoreApplication.translate(
"ConnectionConfigView",
"{0} remaining...".format(displayed_remaining_time),
)
)
self.progress_bar.setValue(next_value)
self.timer.restart()
def set_progress_steps(self, steps):
self.progress_bar.setValue(0)
self.timer.start()
self.progress_bar.setMaximum(steps * 1000000)
def set_step(self, step):
self.progress_bar.setValue(step * 1000000)
async def show_register_message(self, blockchain_parameters):
"""
:param sakia.data.entities.BlockchainParameters blockchain_parameters:
:return:
"""
days, hours, minutes, seconds = timestamp_to_dhms(
blockchain_parameters.idty_window
)
expiration_time_str = QCoreApplication.translate(
"ConnectionConfigView", "{days} days, {hours}h and {min}min"
).format(days=days, hours=hours, min=minutes)
dialog = QDialog(self)
about_dialog = Ui_CongratulationPopup()
about_dialog.setupUi(dialog)
dialog.setWindowTitle("Identity registration")
about_dialog.label.setText(
QCoreApplication.translate(
"ConnectionConfigView",
"""
<p><b>Congratulations!</b><br>
<br>
You just published your identity to the network.<br>
For your identity to be registered, you will need<br>
<b>{certs} certifications</b> from members.<br>
Once you got the required certifications, <br>
you will be able to validate your registration<br>
by <b>publishing your membership request!</b><br>
Please notice that your identity document <br>
<b>will expire in {expiration_time_str}.</b><br>
If you failed to get {certs} certifications before this time, <br>
the process will have to be restarted from scratch.</p>
""".format(
certs=blockchain_parameters.sig_qty,
expiration_time_str=expiration_time_str,
),
)
)
await dialog_async_exec(dialog)
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ContactDialog</class>
<widget class="QDialog" name="ContactDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>629</width>
<height>536</height>
</rect>
</property>
<property name="windowTitle">
<string>Contacts</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Contacts list</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QTableView" name="table_contacts">
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderStretchLastSection">
<bool>false</bool>
</attribute>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<property name="topMargin">
<number>6</number>
</property>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="button_delete">
<property name="text">
<string>Delete selected contact</string>
</property>
<property name="icon">
<iconset resource="../../../../../res/icons/sakia.icons.qrc">
<normaloff>:/icons/not_member</normaloff>:/icons/not_member</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_clear">
<property name="text">
<string>Clear selection</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Contact informations</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QScrollArea" name="scrollArea">
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>561</width>
<height>128</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Name</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="edit_name"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Public key</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="edit_pubkey"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="add_info_button">
<property name="text">
<string>Add other informations</string>
</property>
<property name="popupMode">
<enum>QToolButton::MenuButtonPopup</enum>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<property name="topMargin">
<number>6</number>
</property>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="button_save">
<property name="text">
<string>Save</string>
</property>
<property name="icon">
<iconset resource="../../../../../res/icons/sakia.icons.qrc">
<normaloff>:/icons/add_account_icon</normaloff>:/icons/add_account_icon</iconset>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="button_box">
<property name="enabled">
<bool>true</bool>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../../../../../res/icons/sakia.icons.qrc"/>
</resources>
<connections/>
<slots>
<slot>open_manage_wallet_coins()</slot>
<slot>change_displayed_wallet(int)</slot>
<slot>transfer_mode_changed(bool)</slot>
<slot>recipient_mode_changed(bool)</slot>
<slot>change_current_community(int)</slot>
<slot>amount_changed()</slot>
<slot>relative_amount_changed()</slot>
</slots>
</ui>
import asyncio
from PyQt5.QtCore import QObject
from .model import ContactModel
from .view import ContactView
import attr
@attr.s()
class ContactController(QObject):
"""
The Contact view
"""
view = attr.ib()
model = attr.ib()
def __attrs_post_init__(self):
super().__init__()
self.view.button_box.rejected.connect(self.view.close)
@classmethod
def create(cls, parent, app):
"""
Instanciate a Contact component
:param sakia.gui.component.controller.ComponentController parent:
:param sakia.app.Application app: sakia application
:return: a new Contact controller
:rtype: ContactController
"""
view = ContactView(parent.view if parent else None)
model = ContactModel(app)
contact = cls(view, model)
view.set_table_contacts_model(model.init_contacts_table())
view.button_save.clicked.connect(contact.save_contact)
view.table_contacts.clicked.connect(contact.edit_contact)
view.button_clear.clicked.connect(contact.clear_selection)
view.button_delete.clicked.connect(contact.delete_contact)
return contact
@classmethod
def open_dialog(cls, parent, app):
"""
Certify and identity
:param sakia.gui.component.controller.ComponentController parent: the parent
:param sakia.core.Application app: the application
:return:
"""
dialog = cls.create(parent, app)
return dialog.exec()
def save_contact(self):
name = self.view.edit_name.text()
pubkey = self.view.edit_pubkey.text()
contact_id = self.model.selected_id
self.model.save_contact(name, pubkey, contact_id)
self.view.edit_name.clear()
self.view.edit_pubkey.clear()
self.model.selected_id = -1
def edit_contact(self):
contact_index = self.view.selected_contact_index()
contact = self.model.contact(contact_index)
if contact:
self.view.edit_pubkey.setText(contact.pubkey)
self.view.edit_name.setText(contact.name)
self.model.selected_id = contact.contact_id
def delete_contact(self):
contact_index = self.view.selected_contact_index()
contact = self.model.contact(contact_index)
if contact:
self.model.delete_contact(contact)
self.view.edit_pubkey.clear()
self.view.edit_name.clear()
self.model.selected_id = -1
def clear_selection(self):
self.view.edit_pubkey.clear()
self.view.edit_name.clear()
self.view.table_contacts.clearSelection()
self.model.selected_id = -1
def async_exec(self):
future = asyncio.Future()
self.view.finished.connect(lambda r: future.set_result(r))
self.view.open()
return future
def exec(self):
self.view.exec()
from PyQt5.QtCore import QObject, Qt
from sakia.data.entities import Contact
from sakia.data.processors import (
IdentitiesProcessor,
ContactsProcessor,
BlockchainProcessor,
ConnectionsProcessor,
)
from sakia.helpers import timestamp_to_dhms
from .table_model import ContactsTableModel, ContactsFilterProxyModel
import attr
@attr.s()
class ContactModel(QObject):
"""
The model of Contact component
"""
app = attr.ib()
selected_id = attr.ib(default=-1)
_contacts_processor = attr.ib(default=None)
def __attrs_post_init__(self):
super().__init__()
self._contacts_processor = ContactsProcessor.instanciate(self.app)
def init_contacts_table(self):
"""
Generates a contacts table model
:return:
"""
self._model = ContactsTableModel(self, self.app)
self._proxy = ContactsFilterProxyModel(self)
self._proxy.setSourceModel(self._model)
self._proxy.setDynamicSortFilter(True)
self._proxy.setSortRole(Qt.DisplayRole)
self._model.init_contacts()
return self._proxy
def save_contact(self, name, pubkey, contact_id):
contact = Contact(self.app.currency, name, pubkey, contact_id=contact_id)
self._contacts_processor.commit(contact)
self._model.add_or_edit_contact(contact)
def delete_contact(self, contact):
self._contacts_processor.delete(contact)
self._model.remove_contact(contact)
def contact(self, index):
contact_id = self._proxy.contact_id(index)
if contact_id >= 0:
return self._contacts_processor.contact(contact_id)
from PyQt5.QtCore import (
QAbstractTableModel,
Qt,
QVariant,
QSortFilterProxyModel,
QModelIndex,
QCoreApplication,
QT_TRANSLATE_NOOP,
)
from sakia.data.processors import ContactsProcessor
class ContactsFilterProxyModel(QSortFilterProxyModel):
def __init__(self, parent):
"""
History of all transactions
:param PyQt5.QtWidgets.QWidget parent: parent widget
"""
super().__init__(parent)
self.app = None
def columnCount(self, parent):
return self.sourceModel().columnCount(None) - 1
def setSourceModel(self, source_model):
self.app = source_model.app
super().setSourceModel(source_model)
def lessThan(self, left, right):
"""
Sort table by given column number.
"""
source_model = self.sourceModel()
left_data = source_model.data(left, Qt.DisplayRole)
right_data = source_model.data(right, Qt.DisplayRole)
if left_data == right_data:
txid_col = source_model.columns_types.index("contact_id")
txid_left = source_model.index(left.row(), txid_col)
txid_right = source_model.index(right.row(), txid_col)
return txid_left < txid_right
return left_data < right_data
def contact_id(self, index):
"""
Gets available table data at given index
:param index:
:return: tuple containing (Identity, Transfer)
"""
if index.isValid() and index.row() < self.rowCount(QModelIndex()):
source_index = self.mapToSource(index)
contact_id_col = ContactsTableModel.columns_types.index("contact_id")
contact_id = self.sourceModel().contacts_data[source_index.row()][
contact_id_col
]
return contact_id
return -1
def data(self, index, role):
source_index = self.mapToSource(index)
model = self.sourceModel()
source_data = model.data(source_index, role)
return source_data
class ContactsTableModel(QAbstractTableModel):
"""
A Qt abstract item model to display contacts in a table view
"""
columns_types = ("name", "pubkey", "contact_id")
# mark strings as translatable string
columns_headers = (
QT_TRANSLATE_NOOP("ContactsTableModel", "Name"),
QT_TRANSLATE_NOOP("ContactsTableModel", "Public key"),
)
def __init__(self, parent, app):
"""
History of all transactions
:param PyQt5.QtWidgets.QWidget parent: parent widget
:param sakia.app.Application app: the main application
"""
super().__init__(parent)
self.app = app
self.contacts_processor = ContactsProcessor.instanciate(app)
self.contacts_data = []
def add_or_edit_contact(self, contact):
for i, data in enumerate(self.contacts_data):
if (
data[ContactsTableModel.columns_types.index("contact_id")]
== contact.contact_id
):
self.contacts_data[i] = self.data_contact(contact)
self.dataChanged.emit(
self.index(i, 0),
self.index(i, len(ContactsTableModel.columns_types)),
)
return
else:
self.beginInsertRows(
QModelIndex(), len(self.contacts_data), len(self.contacts_data)
)
self.contacts_data.append(self.data_contact(contact))
self.endInsertRows()
def remove_contact(self, contact):
for i, data in enumerate(self.contacts_data):
if (
data[ContactsTableModel.columns_types.index("contact_id")]
== contact.contact_id
):
self.beginRemoveRows(QModelIndex(), i, i)
self.contacts_data.pop(i)
self.endRemoveRows()
return
def data_contact(self, contact):
"""
Converts a contact to table data
:param sakia.data.entities.Contact contact: the contact
:return: data as tuple
"""
return contact.name, contact.pubkey, contact.contact_id
def init_contacts(self):
self.beginResetModel()
self.contacts_data = []
contacts = self.contacts_processor.contacts()
for contact in contacts:
self.contacts_data.append(self.data_contact(contact))
self.endResetModel()
def rowCount(self, parent):
return len(self.contacts_data)
def columnCount(self, parent):
return len(ContactsTableModel.columns_types)
def headerData(self, section, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return QCoreApplication.translate(
"ContactsTableModel", ContactsTableModel.columns_headers[section]
)
def data(self, index, role):
row = index.row()
col = index.column()
if not index.isValid():
return QVariant()
if role in (Qt.DisplayRole, Qt.ForegroundRole, Qt.ToolTipRole):
return self.contacts_data[row][col]
def flags(self, index):
return Qt.ItemIsSelectable | Qt.ItemIsEnabled
from PyQt5.QtWidgets import (
QDialog,
QDialogButtonBox,
QMessageBox,
QAbstractItemView,
QHeaderView,
)
from PyQt5.QtCore import QT_TRANSLATE_NOOP, Qt, QModelIndex
from .contact_uic import Ui_ContactDialog
from duniterpy.constants import PUBKEY_REGEX
import re
class ContactView(QDialog, Ui_ContactDialog):
"""
The view of the certification component
"""
def __init__(self, parent):
"""
:param parent:
"""
super().__init__(parent)
self.setupUi(self)
self.edit_name.textChanged.connect(self.check_name)
self.edit_pubkey.textChanged.connect(self.check_pubkey)
self.add_info_button.hide()
self.check_pubkey()
self.check_name()
def set_table_contacts_model(self, model):
"""
Define the table history model
:param QAbstractItemModel model:
:return:
"""
self.table_contacts.setModel(model)
self.table_contacts.setSelectionBehavior(QAbstractItemView.SelectRows)
self.table_contacts.setSortingEnabled(True)
self.table_contacts.horizontalHeader().setSectionResizeMode(
QHeaderView.Interactive
)
self.table_contacts.resizeRowsToContents()
self.table_contacts.verticalHeader().setSectionResizeMode(
QHeaderView.ResizeToContents
)
def selected_contact_index(self):
indexes = self.table_contacts.selectedIndexes()
if indexes:
return indexes[0]
return QModelIndex()
def check_pubkey(self):
text = self.edit_pubkey.text()
re_pubkey = re.compile(PUBKEY_REGEX)
result = re_pubkey.match(text)
if result:
self.edit_pubkey.setStyleSheet("")
self.button_save.setEnabled(True)
else:
self.edit_pubkey.setStyleSheet("border: 1px solid red")
self.button_save.setDisabled(True)
def check_name(self):
text = self.edit_name.text()
re_name = re.compile("[\w\s\d]+")
result = re_name.match(text)
if result:
self.edit_name.setStyleSheet("")
self.button_save.setEnabled(True)
else:
self.edit_name.setStyleSheet("border: 1px solid red")
self.button_save.setDisabled(True)