diff --git a/ci/appveyor/sakia.iss b/ci/appveyor/sakia.iss index 8b4d79e2bc04bcfafeb2089dec492092cc2d5045..1270b2962f3053f601f1412b0e13f7a281ad8a67 100644 --- a/ci/appveyor/sakia.iss +++ b/ci/appveyor/sakia.iss @@ -15,7 +15,7 @@ #error "Unable to find MyAppExe" #endif -#define MyAppVerStr "0.20.6" +#define MyAppVerStr "0.20.7" [Setup] AppName={#MyAppName} diff --git a/ci/travis/debian/DEBIAN/control b/ci/travis/debian/DEBIAN/control index 981bec206856a5fe5ae5ec49455d57a00f2b662d..dff134e38087265628d3345e475caddbf213d184 100644 --- a/ci/travis/debian/DEBIAN/control +++ b/ci/travis/debian/DEBIAN/control @@ -1,5 +1,5 @@ Package: sakia -Version: 0.20.6 +Version: 0.20.7 Section: misc Priority: optional Architecture: all diff --git a/ci/travis/debian/usr/share/applications/sakia.desktop b/ci/travis/debian/usr/share/applications/sakia.desktop index fb144af4158e6f1e32098fee6ddfd67bd69600fb..251e4add65e1a19013968074347a6718a1bb8c5d 100644 --- a/ci/travis/debian/usr/share/applications/sakia.desktop +++ b/ci/travis/debian/usr/share/applications/sakia.desktop @@ -1,5 +1,5 @@ [Desktop Entry] -Version=0.20.6 +Version=0.20.7 Name=Sakia Comment=Duniter Qt Client Exec=sakia diff --git a/ci/travis/test.sh b/ci/travis/test.sh index 25e05bd48cd516aa5028015cb12d1db915e956b9..2ef4409d195aeceeaf55fceaf6a4846d1ec71ab5 100755 --- a/ci/travis/test.sh +++ b/ci/travis/test.sh @@ -6,7 +6,7 @@ if [ $TRAVIS_OS_NAME == "linux" ] then export XVFBARGS="-screen 0 1280x1024x24" export DISPLAY=:99.0 - sh -e /etc/init.d/xvfb restart + sudo sh -e /etc/init.d/xvfb restart sleep 3 fi diff --git a/src/sakia/__init__.py b/src/sakia/__init__.py index 16dcf24d77d51b9aaaad2c02ce1198fd021d9f46..b7d5639d4303f5fca579df0ebbe5876988f2ca20 100644 --- a/src/sakia/__init__.py +++ b/src/sakia/__init__.py @@ -1,2 +1,2 @@ -__version_info__ = ('0', '20', '6') +__version_info__ = ('0', '20', '7') __version__ = '.'.join(__version_info__) diff --git a/src/sakia/core/account.py b/src/sakia/core/account.py index 96ba5ada8562e513338fe199c3acb95b7163ee9c..a0db953f6d9f3027461d31c4db9e6297245dbedc 100644 --- a/src/sakia/core/account.py +++ b/src/sakia/core/account.py @@ -20,7 +20,7 @@ from . import money from .wallet import Wallet from .community import Community from .registry import LocalState -from ..tools.exceptions import ContactAlreadyExists +from ..tools.exceptions import ContactAlreadyExists, LookupFailureError from .. import __version__ @@ -509,8 +509,12 @@ class Account(QObject): """ logging.debug("Certdata") blockUID = community.network.current_blockUID - identity = await self._identities_registry.future_find(pubkey, community) - selfcert = await identity.selfcert(community) + try: + identity = await self._identities_registry.future_find(pubkey, community) + selfcert = await identity.selfcert(community) + except LookupFailureError as e: + return False, str(e) + if selfcert: certification = Certification(PROTOCOL_VERSION, community.currency, self.pubkey, pubkey, blockUID, None) diff --git a/src/sakia/core/app.py b/src/sakia/core/app.py index 3f4397b12a14c63c62159c5df16223ee71579b51..0895cb60daf878a1143b08401946de4ea6d42f0d 100644 --- a/src/sakia/core/app.py +++ b/src/sakia/core/app.py @@ -268,7 +268,9 @@ class Application(QObject): try: with open(account_notifications_path, 'r') as json_data: data = json.load(json_data) - account.notifications = data + for notification in data: + if notification in account.notifications: + account.notifications[notification] = data[notification] except FileNotFoundError: logging.debug("Could not find notifications file") pass diff --git a/src/sakia/core/txhistory.py b/src/sakia/core/txhistory.py index e2497841ca8c56bd937b80c57a14a1444e1b53fd..4fd7b9a6c008f7f446026f670fa802602f6ab4bb 100644 --- a/src/sakia/core/txhistory.py +++ b/src/sakia/core/txhistory.py @@ -2,8 +2,7 @@ import asyncio import logging import hashlib import math -from duniterpy.documents.transaction import SimpleTransaction -from duniterpy.documents.block import Block +from duniterpy.documents import SimpleTransaction, Block, MalformedDocumentError from duniterpy.api import bma, errors from .transfer import Transfer, TransferState from .net.network import MAX_CONFIRMATIONS @@ -300,7 +299,7 @@ class TxHistory: for transfer in [t for t in self._transfers if t.state == TransferState.AWAITING]: transfer.run_state_transitions((False, block_to, parameters['avgGenTime'], parameters['medianTimeBlocks'])) - except NoPeerAvailable as e: + except (MalformedDocumentError, NoPeerAvailable) as e: logging.debug(str(e)) self.wallet.refresh_finished.emit([]) return diff --git a/src/sakia/core/wallet.py b/src/sakia/core/wallet.py index fae1633b2f6d620bb2812df82c1e2f8cff7b5e67..7cd58741298fda0f7c007d3f33b192e0ad5e0b3f 100644 --- a/src/sakia/core/wallet.py +++ b/src/sakia/core/wallet.py @@ -175,22 +175,52 @@ class Wallet(QObject): :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, 0) cache = self.caches[community.currency] - current_base = amount_base + current_base = max([src['base'] for src in cache.available_sources]) + value = 0 + sources = [] + outputs = [] + overheads = [] + buf_sources = list(cache.available_sources) while current_base >= 0: - value = 0 - sources = [] - buf_sources = list(cache.available_sources) for s in [src for src in cache.available_sources if src['base'] == current_base]: - value += s['amount'] * pow(10, s['base']) - sources.append(s) - buf_sources.remove(s) - if value >= amount * pow(10, amount_base): - overhead = value - int(amount) * pow(10, amount_base) - overhead, overhead_max_base = reduce_base(overhead, 0) - if overhead_max_base >= current_base: - return (sources, buf_sources) + 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 + # exemple : 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, buf_sources + current_base -= 1 raise NotEnoughMoneyError(value, community.currency, @@ -206,7 +236,7 @@ class Wallet(QObject): """ inputs = [] for s in sources: - inputs.append(InputSource(None, None, s['type'], s['identifier'], s['noffset'])) + inputs.append(InputSource(s['amount'], s['base'], s['type'], s['identifier'], s['noffset'])) return inputs def tx_unlocks(self, sources): @@ -222,31 +252,35 @@ class Wallet(QObject): unlocks.append(Unlock(i, [SIGParameter(0)])) return unlocks - def tx_outputs(self, pubkey, amount, inputs): + def tx_outputs(self, pubkey, outputs, overheads): """ Get outputs to generate a transaction with a given amount of money :param str pubkey: The target pubkey of the transaction - :param int amount: The amount to send + :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 """ - outputs = [] - inputs_value = 0 - for i in inputs: - logging.debug(i) - inputs_value += i['amount'] * pow(10, i['base']) - inputs_max_base = max([i['base'] for i in inputs]) - overhead = inputs_value - int(amount) - - amount, amount_base = int(amount / pow(10, inputs_max_base)), inputs_max_base - overhead, overhead_base = int(overhead / pow(10, inputs_max_base)), inputs_max_base - - outputs.append(OutputSource(amount, amount_base, output.Condition.token(output.SIG.token(pubkey)))) - if overhead != 0: - outputs.append(OutputSource(overhead, overhead_base, output.Condition.token(output.SIG.token(self.pubkey)))) - return outputs + 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] + total.append(OutputSource(output_sum, base, output.Condition.token(output.SIG.token(pubkey)))) + + 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] + total.append(OutputSource(overheads_sum, base, output.Condition.token(output.SIG.token(self.pubkey)))) + + return total def prepare_tx(self, pubkey, blockstamp, amount, message, community): """ @@ -260,12 +294,14 @@ class Wallet(QObject): """ result = self.tx_sources(int(amount), community) sources = result[0] - self.caches[community.currency].available_sources = result[1][1:] + computed_outputs = result[1] + overheads = result[2] + self.caches[community.currency].available_sources = result[3][1:] logging.debug("Inputs : {0}".format(sources)) inputs = self.tx_inputs(sources) unlocks = self.tx_unlocks(sources) - outputs = self.tx_outputs(pubkey, amount, sources) + outputs = self.tx_outputs(pubkey, computed_outputs, overheads) logging.debug("Outputs : {0}".format(outputs)) tx = Transaction(PROTOCOL_VERSION, community.currency, blockstamp, 0, [self.pubkey], inputs, unlocks, diff --git a/src/sakia/tests/unit/core/test_wallet.py b/src/sakia/tests/unit/core/test_wallet.py index 44e880a54f5db91bfe5ec6a1c6257a42b692ec8d..eee64a6e121b83bbc353c1a6d9de35dfacb27016 100644 --- a/src/sakia/tests/unit/core/test_wallet.py +++ b/src/sakia/tests/unit/core/test_wallet.py @@ -3,6 +3,7 @@ import pypeg2 from unittest.mock import MagicMock, PropertyMock from asynctest import CoroutineMock from duniterpy.grammars import output +from duniterpy.documents import BlockUID from PyQt5.QtCore import QLocale from sakia.core.registry.identities import IdentitiesRegistry from sakia.core import Wallet @@ -61,7 +62,10 @@ class TestWallet(unittest.TestCase, QuamashTest): "Wallet 1", self.identities_registry) wallet.caches["test_currency"] = cache tx = wallet.prepare_tx("FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", + BlockUID(32, "000005E0F228038E4DDD4F6CA4ACB01EC88FBAF8"), 99, "", community) + self.assertEqual(tx.blockstamp.number, 32) + self.assertEqual(tx.blockstamp.sha_hash, "000005E0F228038E4DDD4F6CA4ACB01EC88FBAF8") self.assertEqual(len(tx.issuers), 1) self.assertEqual(tx.issuers[0], "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ") self.assertEqual(len(tx.inputs), 2) @@ -134,7 +138,10 @@ Comment:""" + " \n") "Wallet 1", self.identities_registry) wallet.caches["test_currency"] = cache tx = wallet.prepare_tx("FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", + BlockUID(32, "000005E0F228038E4DDD4F6CA4ACB01EC88FBAF8"), 100, "", community) + self.assertEqual(tx.blockstamp.number, 32) + self.assertEqual(tx.blockstamp.sha_hash, "000005E0F228038E4DDD4F6CA4ACB01EC88FBAF8") self.assertEqual(len(tx.issuers), 1) self.assertEqual(tx.issuers[0], "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ") self.assertEqual(len(tx.inputs), 1) @@ -166,4 +173,90 @@ Unlocks: Outputs: 10:1:SIG(FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn) 1:1:SIG(7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ) +Comment:""" + " \n") + + def test_prepare_tx_base_1_overheads(self): + community = MagicMock("sakia.core.Community") + community.currency = "test_currency" + cache = MagicMock("sakia.core.txhistory.TxHistory") + cache.available_sources = [{ + "pubkey": "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "type": "D", + "noffset": 2, + "identifier": "FCAD5A388AC8A811B45A9334A375585E77071AA9F6E5B6896582961A6C66F365", + "amount": 15, + "base": 0 + }, + { + "pubkey": "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "type": "D", + "noffset": 4, + "identifier": "A0AC57E2E4B24D66F2D25E66D8501D8E881D9E6453D1789ED753D7D426537ED5", + "amount": 85, + "base": 0 + }, + { + "pubkey": "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", + "type": "T", + "noffset": 4, + "identifier": "7518C700E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67", + "amount": 11, + "base": 1 + }] + wallet = Wallet(0, "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "Wallet 1", self.identities_registry) + wallet.caches["test_currency"] = cache + tx = wallet.prepare_tx("FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", + BlockUID(32, "000005E0F228038E4DDD4F6CA4ACB01EC88FBAF8"), + 101, "", community) + self.assertEqual(tx.blockstamp.number, 32) + self.assertEqual(tx.blockstamp.sha_hash, "000005E0F228038E4DDD4F6CA4ACB01EC88FBAF8") + self.assertEqual(len(tx.issuers), 1) + self.assertEqual(tx.issuers[0], "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ") + self.assertEqual(len(tx.inputs), 2) + self.assertEqual(tx.inputs[0].origin_id, "7518C700E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67") + self.assertEqual(tx.inputs[0].source, "T") + self.assertEqual(tx.inputs[0].index, 4) + self.assertEqual(tx.inputs[1].origin_id, "FCAD5A388AC8A811B45A9334A375585E77071AA9F6E5B6896582961A6C66F365") + self.assertEqual(tx.inputs[1].source, "D") + self.assertEqual(tx.inputs[1].index, 2) + self.assertEqual(len(tx.outputs), 4) + self.assertEqual(tx.outputs[0].amount, 1) + self.assertEqual(tx.outputs[0].base, 0) + self.assertEqual(pypeg2.compose(tx.outputs[0].conditions, output.Condition), + "SIG(FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn)") + self.assertEqual(tx.outputs[1].amount, 10) + self.assertEqual(tx.outputs[1].base, 1) + self.assertEqual(pypeg2.compose(tx.outputs[1].conditions, output.Condition), + "SIG(FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn)") + self.assertEqual(tx.outputs[2].amount, 14) + self.assertEqual(tx.outputs[2].base, 0) + self.assertEqual(pypeg2.compose(tx.outputs[2].conditions, output.Condition), + "SIG(7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ)") + self.assertEqual(tx.outputs[3].amount, 1) + self.assertEqual(tx.outputs[3].base, 1) + self.assertEqual(pypeg2.compose(tx.outputs[3].conditions, output.Condition), + "SIG(7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ)") + self.assertEqual(len(tx.unlocks), 2) + self.assertEqual(tx.unlocks[0].index, 0) + self.assertEqual(tx.unlocks[0].parameters[0].index, 0) + self.assertEqual(tx.unlocks[1].index, 1) + self.assertEqual(tx.unlocks[1].parameters[0].index, 0) + self.assertEqual(tx.raw(), """Version: 2 +Type: Transaction +Currency: test_currency +Locktime: 0 +Issuers: +7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ +Inputs: +T:7518C700E78B56CC21FB1DDC6CBAB24E0FACC9A798F5ED8736EA007F38617D67:4 +D:FCAD5A388AC8A811B45A9334A375585E77071AA9F6E5B6896582961A6C66F365:2 +Unlocks: +0:SIG(0) +1:SIG(0) +Outputs: +1:0:SIG(FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn) +10:1:SIG(FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn) +14:0:SIG(7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ) +1:1:SIG(7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ) Comment:""" + " \n")