commit 82b88dbe27b06b336bf716491a4f617c57950cdf
parent f3f2c3c7b5f5f770b785b7bdbd29a1c022c63594
Author: lash <dev@holbrook.no>
Date: Thu, 1 Jan 2026 11:51:53 +0100
Rename package to usawa, add docs for ledger
Diffstat:
21 files changed, 833 insertions(+), 677 deletions(-)
diff --git a/dummy/create.py b/dummy/create.py
@@ -6,7 +6,7 @@ import lxml.etree
import confini
import nacl.signing
-from svcontas import Entry, EntryPart, DemoWallet, ACL, get_units, init_ledger
+from usawa import Entry, EntryPart, DemoWallet, ACL, get_units, init_ledger
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
diff --git a/dummy/demo.py b/dummy/demo.py
@@ -4,7 +4,7 @@ import datetime
from lxml import etree
-from svcontas import load, get_units, init_ledger
+from usawa import load, get_units, init_ledger
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
diff --git a/dummy/svcontas/constant.py b/dummy/svcontas/constant.py
@@ -1,4 +0,0 @@
-DEFAULTPARENT = b'\x00' * 64
-NS = 'http://svcontas.defalsify.org/'
-NAMESPACES = {None: NS}
-NSPREFIX = '{' + NS + '}'
diff --git a/dummy/svcontas/entry.py b/dummy/svcontas/entry.py
@@ -1,268 +0,0 @@
-import enum
-import logging
-import datetime
-import uuid
-import hashlib
-
-from lxml import etree
-import rencode
-
-from .constant import DEFAULTPARENT, NSPREFIX
-from .crypto import DemoWallet
-from .error import ACLError, VerifyError
-from .xml import nsmap
-
-logg = logging.getLogger('svcontas.entry')
-
-
-class KeyStoreFormat(enum.IntEnum):
- LITERAL = 0
- INDEXED = 1
-
-
-class EntryPart:
-
- def __init__(self, typ, account, amount, src=False):
- self.typ = typ
- self.account = account
- self.amount = amount
- self.issrc = src
-
-
- @staticmethod
- def from_tree(tree, src=False):
- typ = tree.get('type')
- amount = int(tree.find('amount', namespaces=nsmap()).text)
- account = tree.find('account', namespaces=nsmap()).text
- return EntryPart(typ, account, amount, src=src)
-
-
- def apply_tree(self, tree):
- tag = 'dst'
- if self.issrc:
- tag = 'src'
- part = etree.Element(tag, type=self.typ)
- o = etree.Element('account')
- o.text = self.account
- part.append(o)
-
- o = etree.Element('amount')
- o.text = str(self.amount)
- part.append(o)
-
- tree.append(part)
- return part
-
-
- def __str__(self):
- pfx = 'dst'
- if self.issrc:
- pfx = 'src'
- return '[{}] {}:{} {}'.format(pfx, self.typ, self.account, self.amount)
-
-
-class Entry:
-
- # TODO: parent only 0 if serial 0
- def __init__(self, src, dst, unit, serial, tx_date, ref=None, description=None, parent=None, tx_datereg=None):
- if isinstance(parent, str):
- parent = bytes.fromhex(parent)
- elif parent == None:
- parent = DEFAULTPARENT
- elif len(parent) != 64:
- raise ValueError('invalid parent hash')
- if ref == None:
- ref = str(uuid.uuid4())
- self.ref = ref
- self.parent = parent
- self.unit = unit
- self.serial = serial
- self.dt = tx_date
- if tx_datereg == None:
- tx_datereg = datetime.datetime.now()
- self.dtreg = tx_datereg
- self.attachment = []
- self.sigs = {}
- self.description = description
- self.src = src
- self.dst = dst
-
-
- def attach(self, mime, algo, digest, description=None, slug=None):
- self.attachment.append((mime, algo, digest, description, slug,))
-
-
- def add_signature(self, keyid, sigdata):
- self.sigs[keyid] = sigdata
-
-
- @staticmethod
- def from_tree(tree, unitindex, min=0):
- o = tree.find('data', namespaces=nsmap())
- serial = int(o.find('serial', namespaces=nsmap()).text)
- if min > serial:
- raise ValueError('entry serial preceeds ledger')
- unit = o.find('unit', namespaces=nsmap()).text
- unitindex.sym(unit)
-
- ref = o.find('ref', namespaces=nsmap()).text
- parent = o.find('parent', namespaces=nsmap()).text
- description = o.find('description', namespaces=nsmap())
- if description != None:
- description = description.text
- dt = datetime.date.fromisoformat(o.find('date', namespaces=nsmap()).text)
- dtreg = datetime.datetime.strptime(o.find('dateTimeRegistered', namespaces=nsmap()).text, '%Y-%m-%dT%H:%M:%SZ')
- src = EntryPart.from_tree(tree.find('src', namespaces=nsmap()), src=True)
- dst = EntryPart.from_tree(tree.find('dst', namespaces=nsmap()))
-
- r = Entry(src, dst, unit, serial, dt, ref=ref, parent=parent, tx_datereg=dtreg, description=description)
- for sig in tree.iter(NSPREFIX + 'sig'):
- r.add_signature(sig.get('keyid'), bytes.fromhex(sig.text))
- return r
-
-
- def serialize(self):
- src = [self.src.typ, self.src.account, self.src.amount]
- dst = [self.dst.typ, self.dst.account, self.dst.amount]
- d = [
- self.parent,
- self.serial,
- self.ref,
- self.dtreg.strftime('%Y%m%d%H%M%S'),
- self.dt.strftime('%Y%m%d'),
- self.unit,
- self.description,
- src,
- dst,
- ]
- logg.debug('serialize entry {}'.format(d))
- return rencode.dumps(d)
-
-
- @staticmethod
- def deserialize(data):
- v = rencode.loads(data)
- parent = v[0]
- serial = v[1]
- ref = v[2]
- date_reg = datetime.datetime.strptime(v[3].decode('utf-8'), '%Y%m%d%H%M%S')
- date = datetime.datetime.strptime(v[4].decode('utf-8'), '%Y%m%d')
- unit = v[5]
- description = v[6]
- src_data = v[7]
- dst_data = v[8]
- src = EntryPart(src_data[0], src_data[1], src_data[2], src=True)
- dst = EntryPart(dst_data[0], dst_data[1], dst_data[2])
- return Entry(src, dst, unit, serial, date, ref=ref, description=description, parent=parent, tx_datereg=date_reg)
-
-
- def sum(self):
- b = self.serialize()
- h = hashlib.new('sha512')
- h.update(b)
- return (h.digest(), b)
-
-
- def sign(self, wallet):
- (z, b) = self.sum()
- r = wallet.sign(z)
- pubk_hx = wallet.pubkey().hex()
- self.sigs[pubk_hx] = r
- logg.debug('added signature from key {}'.format(pubk_hx))
- return (z, r, b,)
-
-
- def wrap(self, wallet=None):
- digest = None
- sig = None
- data = None
- if wallet != None:
- (digest, sig, data) = self.sign(wallet)
- hdr = []
- sigs = []
- for k in self.sigs:
- hdr.append([
- KeyStoreFormat.LITERAL.value,
- bytes.fromhex(k)
- ])
- sigs.append(self.sigs[k])
-
- if len(sigs) == 0:
- raise VerifyError()
-
- d = [
- hdr,
- sigs,
- data,
- ]
- return rencode.dumps(d)
-
-
- @staticmethod
- def unwrap(data, acl=None):
- v = rencode.loads(data)
- # TODO: demo only takes into account single signature
- pubkey_bytes = v[0][0][1]
- if acl != None:
- label = None
- try:
- label = acl.have(pubkey_bytes)
- except KeyError:
- raise ACLError()
- if not acl.may(label, 0x01):
- raise ACLError()
- wallet = DemoWallet(publickey=pubkey_bytes)
- # TODO: demo only takes into account single signature
- sig = v[1][0]
- entry = Entry.deserialize(v[2])
- (z, b) = entry.sum()
- wallet.verify(z, sig)
- return entry
-
-
-
- def to_tree(self):
- #tree = etree.Element('entry', type=self.typ)
- tree = etree.Element('entry')
- data = etree.Element('data')
-
- o = etree.Element('parent')
- o.text = self.parent.hex()
- data.append(o)
-
- o = etree.Element('ref')
- o.text = self.ref
- data.append(o)
-
- o = etree.Element('serial')
- o.text = str(self.serial)
- data.append(o)
-
- o = etree.Element('unit')
- o.text = self.unit
- data.append(o)
-
- o = etree.Element('date')
- o.text = self.dt.strftime('%Y-%m-%d')
- data.append(o)
-
- o = etree.Element('dateTimeRegistered')
- o.text = self.dtreg.strftime('%Y-%m-%dT%H:%M:%SZ')
- data.append(o)
-
- if self.description:
- o = etree.Element('description')
- o.text = self.description
- data.append(o)
-
- self.src.apply_tree(tree)
- self.dst.apply_tree(tree)
-
- tree.append(data)
-
- for k in self.sigs.keys():
- o = etree.Element('sig', type='ed25519', keyid=k)
- o.text = self.sigs[k].hex()
- tree.append(o)
-
- return tree
diff --git a/dummy/svcontas/ledger.py b/dummy/svcontas/ledger.py
@@ -1,302 +0,0 @@
-import os
-import datetime
-import logging
-import hashlib
-
-import lxml
-import rencode
-
-from .crypto import DemoWallet
-from .xml import nsmap, XML_FORMAT_VERSION
-from .constant import NSPREFIX, DEFAULTPARENT
-from .entry import Entry
-
-logg = logging.getLogger('svcontas.ledger')
-
-
-class RunningTotal:
-
- def __init__(self, sym, unitindex, asset=0, liability=0, income=0, expense=0):
- self.sym = sym
- self.asset = asset
- self.liability = liability
- self.income = income
- self.expense = expense
- self.unitindex = unitindex
-
-
- def get_balance(self):
- return self.asset - self.liability
-
-
- def get_result(self):
- return self.income - self.expense
-
-
- def income_delta(self, v):
- self.income += v
-
-
- def expense_delta(self, v):
- self.expense += v
-
-
- def asset_delta(self, v):
- self.asset += v
-
-
- def liability_delta(self, v):
- self.liability += v
-
-
- def apply_entry(self, entry):
- src = entry.src.typ
- dst = entry.dst.typ
- src_isbalance = src in ['liability', 'asset']
- dst_isbalance = dst in ['liability', 'asset']
-
- src_amount = entry.src.amount
- dst_amount = entry.dst.amount
- if src_isbalance and dst_isbalance:
- if dst == 'liability':
- src_amount *= -1
- dst_amount *= -1
- fn = getattr(self, entry.src.typ + '_delta')
- fn(src_amount)
- fn = getattr(self, entry.dst.typ + '_delta')
- fn(dst_amount)
-
- logg.debug('applied entry {} src {} dst {} balance {}'.format(entry.serial, entry.src, entry.dst, self.unitindex.to_floatstring(self.sym, self.get_balance())))
-
-
- def __str__(self):
- return 'running total {}: income {} expense {} asset {} liability {}'.format(self.sym, self.income, self.expense, self.asset, self.liability)
-
-
-class Ledger:
-
- def __init__(self, unitindex, tree=None, acl=None, serial=0, base=None, topic=None):
- self.uidx = unitindex
- self.sigs = {}
- self.entries = {}
- self.running = {}
- self.tree = tree
- if self.tree == None:
- self.reset()
- self.serial = serial
- if topic == None:
- topic = os.urandom(64)
- if base == None:
- h = hashlib.sha512()
- h.update(topic)
- h.update(DEFAULTPARENT)
- base = h.digest()
- self.base = base
- self.topic = topic
- self.cur = base
- self.acl = acl
- logg.debug('ledger base {} from topic {}'.format(self.base.hex(), self.topic.hex()))
-
-
- def peek(self):
- return self.serial + 1
-
-
- def next_serial(self):
- self.serial += 1
- return self.serial
-
-
- def reset(self, src='defalsify.org', topic=None):
- self.entries[self.uidx.base] = []
- self.running[self.uidx.base] = RunningTotal(self.uidx.base, self.uidx)
- self.tree = lxml.etree.XML('<ledger xmlns="http://svcontas.defalsify.org/" version="{}"></ledger>'.format(XML_FORMAT_VERSION))
- #self.tree = lxml.etree.Element('ledger', nsmap=nsmap())
- o = lxml.etree.SubElement(self.tree, NSPREFIX + 'topic', nsmap=nsmap())
- if topic == None:
- topic = os.urandom(64)
- topic = topic.hex()
- o.text = topic
- o = lxml.etree.SubElement(self.tree, NSPREFIX + 'retrieved', nsmap=nsmap())
- o.text = datetime.datetime.strftime(datetime.datetime.now(), '%Y-%m-%dT%H:%M:%SZ')
- #self.tree.append(o)
- o = lxml.etree.SubElement(self.tree, NSPREFIX + 'src', nsmap=nsmap())
- o.text = src
-
- units = lxml.etree.SubElement(self.tree, NSPREFIX + 'units', nsmap=nsmap())
- units.attrib['base'] = self.uidx.base
- for v in self.uidx.syms():
- unit = lxml.etree.SubElement(units, NSPREFIX + 'unit', nsmap=nsmap())
- unit.attrib['sym'] = v
- o = lxml.etree.SubElement(unit, NSPREFIX + 'precision', nsmap=nsmap())
- o.text = str(self.uidx.get(v))
- #unit.append(o)
- o = lxml.etree.SubElement(unit, NSPREFIX + 'exchange', nsmap=nsmap())
- o.text = str(self.uidx.ex(v))
- #unit.append(o)
- #units.append(unit)
- #self.tree.append(units)
-
- incoming = lxml.etree.SubElement(self.tree, NSPREFIX + 'incoming', nsmap=nsmap())
- incoming.attrib['serial'] = '0'
-
- real = lxml.etree.SubElement(incoming, NSPREFIX + 'real', nsmap=nsmap())
- real.attrib['unit'] = self.uidx.base
- o = lxml.etree.SubElement(real, NSPREFIX + 'asset', nsmap=nsmap())
- o.text = '0'
- #real.append(o)
- o = lxml.etree.SubElement(real, NSPREFIX + 'liability', nsmap=nsmap())
- o.text = '0'
- #real.append(o)
- #incoming.append(real)
-
- o = lxml.etree.SubElement(incoming, NSPREFIX + 'digest', nsmap=nsmap())
- o.attrib['algo'] = 'sha512'
- o.text = DEFAULTPARENT.hex()
- #incoming.append(o)
- #self.tree.append(incoming)
-
-
- # TODO: should append after last
- def add_identity(self, keyid, did, typ='web'):
- root = self.tree
- tree = root.find('resolver', namespaces=nsmap())
- if tree == None:
- tree = root.find('units', namespaces=nsmap())
- if tree == None:
- logg.debug('exception tree {}'.format(lxml.etree.tostring(self.tree)))
- raise Exception('cannot find units node')
- o = lxml.etree.Element(NSPREFIX + 'identity', nsmap=nsmap())
- o.attrib['keyid'] = keyid
- o.attrib['didtype'] = typ
- o.text = did
- tree.addnext(o)
-
-
- # TODO: should append after last
- def add_resolver(self, uri, algo='sha256', proto='https'):
- tree = self.tree.find('units', namespaces=nsmap())
- o = lxml.etree.Element(NSPREFIX + 'resolver', nsmap=nsmap())
- o.attrib['algo'] = algo
- o.attrib['proto'] = proto
- o.text = uri
- tree.addnext(o)
-
-
- # TODO: add check against trusted pubkey list
- def check_sigs(self, entry):
- have = False
- valid_keys = None
- if self.acl == None:
- valid_keys = list(entry.sigs.keys())
- else:
- valid_keys = list(self.acl.pubkeys(binary=False))
- logg.debug('testing valid keys {}'.format(valid_keys))
- for k in valid_keys:
- b = bytes.fromhex(k)
- try:
- sig = entry.sigs[k]
- except KeyError:
- continue
- wallet = DemoWallet(publickey=b)
- v = entry.sum()
- r = wallet.verify(v[0], sig)
- have = True
- logg.debug('having sig {}'.format(r.hex()))
- return have
-
-
- def add_entry(self, entry, modify_tree=True):
- if not self.check_sigs(entry):
- raise ValueError('entry must have at least one valid signature')
- try:
- entries = self.entries[entry.serial]
- except KeyError:
- self.entries[entry.serial] = []
- #entries = self.entries[entry.serial]
- self.serial = entry.serial
- oldbase = self.base
- self.cur = entry.sum()[0]
- entry.parent = oldbase
- self.entries[entry.serial].append(entry)
- self.running[entry.unit].apply_entry(entry)
- if self.tree != None and modify_tree:
- self.tree.append(entry.to_tree())
- logg.debug('entryunit {} {}'.format(entry.unit, self.running[entry.unit]))
-
-
- def add_signature(self, sigdata, identity):
- self.sigs[identity] = sigdata
- logg.debug('add sig from key{}: {}'.format(identity, sigdata))
-
-
- @staticmethod
- def from_tree(tree, unitindex, acl=None):
- topic_node = tree.find('topic', namespaces=nsmap())
- topic = bytes.fromhex(topic_node.text)
-
- units = tree.find('units', namespaces=nsmap())
- unit = units.get('base')
- part = tree.find('incoming', namespaces=nsmap())
- serial = int(part.get('serial'))
- o = part.find('digest', namespaces=nsmap()).text # verify that is sha512
- r = Ledger(unitindex, topic=topic, tree=tree, acl=acl, serial=serial, base=bytes.fromhex(o))
-
- for sig in part.iter(NSPREFIX + 'sig'):
- keyid = sig.get('keyid')
- digest = sig.text
- r.add_signature(digest, keyid)
-
- o = part.find('real', namespaces=nsmap())
- asset = int(o.find('asset', namespaces=nsmap()).text)
- liability = int(o.find('liability', namespaces=nsmap()).text)
- r.real = RunningTotal(unit, unitindex, asset=asset, liability=liability)
-
- for v in part.iter(NSPREFIX + 'virt'):
- income = int(v.find('income', namespaces=nsmap()).text)
- expense = int(v.find('expense', namespaces=nsmap()).text)
- asset = int(v.find('asset', namespaces=nsmap()).text)
- liability = int(v.find('liability', namespaces=nsmap()).text)
- sym = v.get('unit')
- r.running[sym] = RunningTotal(sym, unitindex, income=income, expense=expense, asset=asset, liability=liability)
- logg.debug(r.running[sym])
-
- if r.running.get(unit) == None:
- r.running[unit] = RunningTotal(unit, unitindex)
-
- r.apply_tree(tree)
- logg.debug('loaded ledger tree last serial {}'.format(r.serial))
- return r.check()
-
-
- def apply_tree(self, tree):
- start = self.serial
- last = 0
- for v in tree.iter(NSPREFIX + 'entry'):
- logg.debug('processing entry {}'.format(v))
- o = Entry.from_tree(v, self.uidx, min=self.serial)
- self.add_entry(o, modify_tree=False)
- if o.serial > last:
- last = o.serial
- self.serial = last
- logg.info('last entry from tree serial ' + str(self.serial))
-
-
- def to_tree(self):
- return self.tree
-
-
- def check(self):
- return self
-
-
- def to_string(self):
- return lxml.etree.tostring(self.tree)
-
-
- def current(self):
- return self.cur
-
-
- def __str__(self):
- return "state: " + self.base.hex()
diff --git a/dummy/svcontas/unit.py b/dummy/svcontas/unit.py
@@ -1,94 +0,0 @@
-import logging
-
-from .constant import NSPREFIX
-from .xml import nsmap
-
-logg = logging.getLogger('svcontas.unit')
-
-BASE_UNIT = 'BTC'
-
-
-class UnitIndex:
-
- def __init__(self, base):
- self.base = base
- self.detail = {base: 0}
- self.exchange = {base: 1}
-
-
- def add(self, sym, precision=2, ex=1):
- self.detail[sym] = precision
- self.exchange[sym] = ex
-
-
- @staticmethod
- def from_tree(tree):
- r = UnitIndex(tree.get('base'))
- logg.debug('base {}'.format(tree))
- for o in tree.iter(NSPREFIX + 'unit'):
- logg.debug('add unit ' + o.get('sym'))
- r.detail[o.get('sym')] = int(o.find('precision', namespaces=nsmap()).text)
- r.exchange[o.get('sym')] = int(o.find('ex', namespaces=nsmap()).text)
- r.check()
- return r
-
-
- def check(self):
- self.get(self.base)
- return self
-
-
- def get(self, k):
- return self.detail[k]
-
-
- def sym(self, k):
- _ = self.get(k)
- return k
-
-
- def ex(self, k):
- return self.exchange[k]
-
-
- def syms(self):
- return list(self.detail.keys())
-
-
- def to_floatstring(self, sym, v, allow_negative=True):
- neg = v < 0
- if neg and not allow_negative:
- raise ValueError('negative value not allowed')
- v = abs(v)
- c = self.detail[sym]
- i = c * -1
- s = str(v)
- l = len(s)
- if l < c:
- ss = '0' * c
- s = '0' + ss[:c-l] + s
- r = s[:i] + '.' + s[i:]
- if neg:
- r = '-' + r
- return r
-
-
- def from_floatstring(self, sym, v, allow_negative=True):
- neg = False
- if v[0] == '-':
- if not allow_negative:
- raise ValueError('negative value not allowed')
- neg = True
- v = v[1:]
- c = self.detail[sym]
- s = v.split('.')
- if len(s) == 1:
- return int(s[0]) * (10**c)
- r = s[1]
- l = len(r)
- if l < c:
- r += '0' * (c - l)
- r = s[0] + r
- if neg:
- r *= -1
- return int(r)
diff --git a/dummy/tests/entry.py b/dummy/tests/entry.py
@@ -4,8 +4,8 @@ import unittest
import os
import copy
-from svcontas import EntryPart, Entry, DemoWallet, ACL
-from svcontas.error import ACLError
+from usawa import EntryPart, Entry, DemoWallet, ACL
+from usawa.error import ACLError
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
diff --git a/dummy/tests/ledger.py b/dummy/tests/ledger.py
@@ -7,8 +7,8 @@ import copy
import lxml.etree
from whee.mem import MemStore
-from svcontas import Ledger, UnitIndex, EntryPart, Entry, DemoWallet
-from svcontas.store import LedgerStore
+from usawa import Ledger, UnitIndex, EntryPart, Entry, DemoWallet
+from usawa.store import LedgerStore
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
diff --git a/dummy/tests/store.py b/dummy/tests/store.py
@@ -7,8 +7,8 @@ import copy
import lxml.etree
from whee.mem import MemStore
-from svcontas import Ledger, UnitIndex, EntryPart, Entry, DemoWallet
-from svcontas.store import LedgerStore
+from usawa import Ledger, UnitIndex, EntryPart, Entry, DemoWallet
+from usawa.store import LedgerStore
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
diff --git a/dummy/tests/test.xml b/dummy/tests/test.xml
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
-<ledger xmlns="http://svcontas.defalsify.org/">
+<ledger xmlns="http://usawa.defalsify.org/">
<topic>fca5eb40383af54fb270283251683f97f8804f35036264bb657e72f1d53b6bcf6afc532b02d2216dff01a2df69f5515c887833b98ed15a8bc6433fb28c5b4834</topic>
<retrieved>2025-11-02T13:09:55Z</retrieved>
<src>playalastunas.org</src>
diff --git a/dummy/svcontas/__init__.py b/dummy/usawa/__init__.py
diff --git a/dummy/usawa/cmd.py b/dummy/usawa/cmd.py
@@ -0,0 +1,8 @@
+import enum
+import rencode
+
+class Op(enum.IntEnum):
+ NOOP = 0
+ UPDATE = 1
+
+
diff --git a/dummy/usawa/constant.py b/dummy/usawa/constant.py
@@ -0,0 +1,4 @@
+DEFAULTPARENT = b'\x00' * 64
+NS = 'http://usawa.defalsify.org/'
+NAMESPACES = {None: NS}
+NSPREFIX = '{' + NS + '}'
diff --git a/dummy/svcontas/crypto.py b/dummy/usawa/crypto.py
diff --git a/dummy/usawa/entry.py b/dummy/usawa/entry.py
@@ -0,0 +1,270 @@
+import enum
+import logging
+import datetime
+import uuid
+import hashlib
+
+from lxml import etree
+import rencode
+
+from .constant import DEFAULTPARENT, NSPREFIX
+from .crypto import DemoWallet
+from .error import ACLError, VerifyError
+from .xml import nsmap
+
+logg = logging.getLogger('usawa.entry')
+
+
+class KeyStoreFormat(enum.IntEnum):
+ LITERAL = 0
+ INDEXED = 1
+
+
+class EntryPart:
+
+ def __init__(self, typ, account, amount, src=False):
+ self.typ = typ
+ self.account = account
+ self.amount = amount
+ self.issrc = src
+
+
+ @staticmethod
+ def from_tree(tree, src=False):
+ typ = tree.get('type')
+ amount = int(tree.find('amount', namespaces=nsmap()).text)
+ account = tree.find('account', namespaces=nsmap()).text
+ return EntryPart(typ, account, amount, src=src)
+
+
+ def apply_tree(self, tree):
+ tag = 'dst'
+ if self.issrc:
+ tag = 'src'
+ part = etree.Element(tag, type=self.typ)
+ o = etree.Element('account')
+ o.text = self.account
+ part.append(o)
+
+ o = etree.Element('amount')
+ o.text = str(self.amount)
+ part.append(o)
+
+ tree.append(part)
+ return part
+
+
+ def __str__(self):
+ pfx = 'dst'
+ if self.issrc:
+ pfx = 'src'
+ return '[{}] {}:{} {}'.format(pfx, self.typ, self.account, self.amount)
+
+
+class Entry:
+
+ digest_algo = 'sha512'
+
+ # TODO: parent only 0 if serial 0
+ def __init__(self, src, dst, unit, serial, tx_date, ref=None, description=None, parent=None, tx_datereg=None):
+ if isinstance(parent, str):
+ parent = bytes.fromhex(parent)
+ elif parent == None:
+ parent = DEFAULTPARENT
+ elif len(parent) != 64:
+ raise ValueError('invalid parent hash')
+ if ref == None:
+ ref = str(uuid.uuid4())
+ self.ref = ref
+ self.parent = parent
+ self.unit = unit
+ self.serial = serial
+ self.dt = tx_date
+ if tx_datereg == None:
+ tx_datereg = datetime.datetime.now()
+ self.dtreg = tx_datereg
+ self.attachment = []
+ self.sigs = {}
+ self.description = description
+ self.src = src
+ self.dst = dst
+
+
+ def attach(self, mime, algo, digest, description=None, slug=None):
+ self.attachment.append((mime, algo, digest, description, slug,))
+
+
+ def add_signature(self, keyid, sigdata):
+ self.sigs[keyid] = sigdata
+
+
+ @staticmethod
+ def from_tree(tree, unitindex, min=0):
+ o = tree.find('data', namespaces=nsmap())
+ serial = int(o.find('serial', namespaces=nsmap()).text)
+ if min > serial:
+ raise ValueError('entry serial preceeds ledger')
+ unit = o.find('unit', namespaces=nsmap()).text
+ unitindex.sym(unit)
+
+ ref = o.find('ref', namespaces=nsmap()).text
+ parent = o.find('parent', namespaces=nsmap()).text
+ description = o.find('description', namespaces=nsmap())
+ if description != None:
+ description = description.text
+ dt = datetime.date.fromisoformat(o.find('date', namespaces=nsmap()).text)
+ dtreg = datetime.datetime.strptime(o.find('dateTimeRegistered', namespaces=nsmap()).text, '%Y-%m-%dT%H:%M:%SZ')
+ src = EntryPart.from_tree(tree.find('src', namespaces=nsmap()), src=True)
+ dst = EntryPart.from_tree(tree.find('dst', namespaces=nsmap()))
+
+ r = Entry(src, dst, unit, serial, dt, ref=ref, parent=parent, tx_datereg=dtreg, description=description)
+ for sig in tree.iter(NSPREFIX + 'sig'):
+ r.add_signature(sig.get('keyid'), bytes.fromhex(sig.text))
+ return r
+
+
+ def serialize(self):
+ src = [self.src.typ, self.src.account, self.src.amount]
+ dst = [self.dst.typ, self.dst.account, self.dst.amount]
+ d = [
+ self.parent,
+ self.serial,
+ self.ref,
+ self.dtreg.strftime('%Y%m%d%H%M%S'),
+ self.dt.strftime('%Y%m%d'),
+ self.unit,
+ self.description,
+ src,
+ dst,
+ ]
+ logg.debug('serialize entry {}'.format(d))
+ return rencode.dumps(d)
+
+
+ @staticmethod
+ def deserialize(data):
+ v = rencode.loads(data)
+ parent = v[0]
+ serial = v[1]
+ ref = v[2]
+ date_reg = datetime.datetime.strptime(v[3].decode('utf-8'), '%Y%m%d%H%M%S')
+ date = datetime.datetime.strptime(v[4].decode('utf-8'), '%Y%m%d')
+ unit = v[5]
+ description = v[6]
+ src_data = v[7]
+ dst_data = v[8]
+ src = EntryPart(src_data[0], src_data[1], src_data[2], src=True)
+ dst = EntryPart(dst_data[0], dst_data[1], dst_data[2])
+ return Entry(src, dst, unit, serial, date, ref=ref, description=description, parent=parent, tx_datereg=date_reg)
+
+
+ def sum(self):
+ b = self.serialize()
+ h = hashlib.new(self.digest_algo)
+ h.update(b)
+ return (h.digest(), b)
+
+
+ def sign(self, wallet):
+ (z, b) = self.sum()
+ r = wallet.sign(z)
+ pubk_hx = wallet.pubkey().hex()
+ self.sigs[pubk_hx] = r
+ logg.debug('added signature from key {}'.format(pubk_hx))
+ return (z, r, b,)
+
+
+ def wrap(self, wallet=None):
+ digest = None
+ sig = None
+ data = None
+ if wallet != None:
+ (digest, sig, data) = self.sign(wallet)
+ hdr = []
+ sigs = []
+ for k in self.sigs:
+ hdr.append([
+ KeyStoreFormat.LITERAL.value,
+ bytes.fromhex(k)
+ ])
+ sigs.append(self.sigs[k])
+
+ if len(sigs) == 0:
+ raise VerifyError()
+
+ d = [
+ hdr,
+ sigs,
+ data,
+ ]
+ return rencode.dumps(d)
+
+
+ @staticmethod
+ def unwrap(data, acl=None):
+ v = rencode.loads(data)
+ # TODO: demo only takes into account single signature
+ pubkey_bytes = v[0][0][1]
+ if acl != None:
+ label = None
+ try:
+ label = acl.have(pubkey_bytes)
+ except KeyError:
+ raise ACLError()
+ if not acl.may(label, 0x01):
+ raise ACLError()
+ wallet = DemoWallet(publickey=pubkey_bytes)
+ # TODO: demo only takes into account single signature
+ sig = v[1][0]
+ entry = Entry.deserialize(v[2])
+ (z, b) = entry.sum()
+ wallet.verify(z, sig)
+ return entry
+
+
+
+ def to_tree(self):
+ #tree = etree.Element('entry', type=self.typ)
+ tree = etree.Element('entry')
+ data = etree.Element('data')
+
+ o = etree.Element('parent')
+ o.text = self.parent.hex()
+ data.append(o)
+
+ o = etree.Element('ref')
+ o.text = self.ref
+ data.append(o)
+
+ o = etree.Element('serial')
+ o.text = str(self.serial)
+ data.append(o)
+
+ o = etree.Element('unit')
+ o.text = self.unit
+ data.append(o)
+
+ o = etree.Element('date')
+ o.text = self.dt.strftime('%Y-%m-%d')
+ data.append(o)
+
+ o = etree.Element('dateTimeRegistered')
+ o.text = self.dtreg.strftime('%Y-%m-%dT%H:%M:%SZ')
+ data.append(o)
+
+ if self.description:
+ o = etree.Element('description')
+ o.text = self.description
+ data.append(o)
+
+ self.src.apply_tree(tree)
+ self.dst.apply_tree(tree)
+
+ tree.append(data)
+
+ for k in self.sigs.keys():
+ o = etree.Element('sig', type='ed25519', keyid=k)
+ o.text = self.sigs[k].hex()
+ tree.append(o)
+
+ return tree
diff --git a/dummy/svcontas/error.py b/dummy/usawa/error.py
diff --git a/dummy/usawa/ledger.py b/dummy/usawa/ledger.py
@@ -0,0 +1,413 @@
+import os
+import datetime
+import logging
+import hashlib
+
+import lxml
+import rencode
+
+from .crypto import DemoWallet
+from .xml import nsmap, XML_FORMAT_VERSION
+from .constant import NSPREFIX, DEFAULTPARENT
+from .entry import Entry
+
+logg = logging.getLogger('usawa.ledger')
+
+
+class RunningTotal:
+
+ def __init__(self, sym, unitindex, asset=0, liability=0, income=0, expense=0):
+ self.sym = sym
+ self.asset = asset
+ self.liability = liability
+ self.income = income
+ self.expense = expense
+ self.unitindex = unitindex
+
+
+ def get_balance(self):
+ return self.asset - self.liability
+
+
+ def get_result(self):
+ return self.income - self.expense
+
+
+ def income_delta(self, v):
+ self.income += v
+
+
+ def expense_delta(self, v):
+ self.expense += v
+
+
+ def asset_delta(self, v):
+ self.asset += v
+
+
+ def liability_delta(self, v):
+ self.liability += v
+
+
+ def apply_entry(self, entry):
+ src = entry.src.typ
+ dst = entry.dst.typ
+ src_isbalance = src in ['liability', 'asset']
+ dst_isbalance = dst in ['liability', 'asset']
+
+ src_amount = entry.src.amount
+ dst_amount = entry.dst.amount
+ if src_isbalance and dst_isbalance:
+ if dst == 'liability':
+ src_amount *= -1
+ dst_amount *= -1
+ fn = getattr(self, entry.src.typ + '_delta')
+ fn(src_amount)
+ fn = getattr(self, entry.dst.typ + '_delta')
+ fn(dst_amount)
+
+ logg.debug('applied entry {} src {} dst {} balance {}'.format(entry.serial, entry.src, entry.dst, self.unitindex.to_floatstring(self.sym, self.get_balance())))
+
+
+ def __str__(self):
+ return 'running total {}: income {} expense {} asset {} liability {}'.format(self.sym, self.income, self.expense, self.asset, self.liability)
+
+
+class Ledger:
+ """Ledger represents a signed and verified chain of transaction entries.
+
+ Apart from the entries chain, it also holds metadata required to expand underlying assets referenced by the entries, aswell as resolution and verification of identities of signatories.
+
+ If populated by an XML tree, all unit definitions encountered will be added to the provided UnitIndex. All other parameters will be ignored.
+
+ If the serial number parameter is non-zero, the base must be a non-zero digest value.
+
+ :param unitindex: The unit index to resolve transaction values used in the entries.
+ :type unitindex: usawa.UnitIndex
+ :param tree: A verified XML tree to use to populate the ledger.
+ :type tree: lxml.etree.ElementTree
+ :param acl: A collection of public keys to use to verify signatures. Will override existing public key lists.
+ :type acl: usawa.ACL
+ :param serial: Serial number to start the current state of the ledger on.
+ :type serial: int
+ :param base: Hexadecimal digest value defining the state of the ledger at the corresponding serial number. The digest type is defined in usawa.Entry.digest_algo
+ :type base: str
+ :param topic: Hexadecimal userdata providing context of the ledger.
+ :type topic: str
+ :todo: Add warnings for ignored parameters
+ """
+
+ def __init__(self, unitindex, tree=None, acl=None, serial=0, base=None, topic=None):
+ self.uidx = unitindex
+ self.sigs = {}
+ self.entries = {}
+ self.running = {}
+ self.tree = tree
+ if self.tree == None:
+ self.reset()
+ self.serial = serial
+ if topic == None:
+ topic = os.urandom(64)
+ if base == None:
+ h = hashlib.sha512()
+ h.update(topic)
+ h.update(DEFAULTPARENT)
+ base = h.digest()
+ self.base = base
+ self.topic = topic
+ self.cur = base
+ self.acl = acl
+ logg.debug('ledger base {} from topic {}'.format(self.base.hex(), self.topic.hex()))
+
+
+ """Retrieve the serial that will be assigned to the next entry.
+
+ :rtype: int
+ :return: Serial
+ """
+ def peek(self):
+ return self.serial + 1
+
+ """Increment the serial and return the incremented serial, to be used for the next entry.
+
+ :rtype: int
+ :return: Serial
+ """
+ def next_serial(self):
+ self.serial += 1
+ return self.serial
+
+
+ """Remove all entries from the ledger, and reset all metadata to defaults.
+
+ :param src: URI to the source of ledger information.
+ :type src: str
+ :rtype: None
+ """
+ def reset(self, src='defalsify.org', topic=None):
+ self.entries[self.uidx.base] = []
+ self.running[self.uidx.base] = RunningTotal(self.uidx.base, self.uidx)
+ self.tree = lxml.etree.XML('<ledger xmlns="http://usawa.defalsify.org/" version="{}"></ledger>'.format(XML_FORMAT_VERSION))
+ #self.tree = lxml.etree.Element('ledger', nsmap=nsmap())
+ o = lxml.etree.SubElement(self.tree, NSPREFIX + 'topic', nsmap=nsmap())
+ if topic == None:
+ topic = os.urandom(64)
+ topic = topic.hex()
+ o.text = topic
+ o = lxml.etree.SubElement(self.tree, NSPREFIX + 'retrieved', nsmap=nsmap())
+ o.text = datetime.datetime.strftime(datetime.datetime.now(), '%Y-%m-%dT%H:%M:%SZ')
+ #self.tree.append(o)
+ o = lxml.etree.SubElement(self.tree, NSPREFIX + 'src', nsmap=nsmap())
+ o.text = src
+
+ units = lxml.etree.SubElement(self.tree, NSPREFIX + 'units', nsmap=nsmap())
+ units.attrib['base'] = self.uidx.base
+ for v in self.uidx.syms():
+ unit = lxml.etree.SubElement(units, NSPREFIX + 'unit', nsmap=nsmap())
+ unit.attrib['sym'] = v
+ o = lxml.etree.SubElement(unit, NSPREFIX + 'precision', nsmap=nsmap())
+ o.text = str(self.uidx.get(v))
+ #unit.append(o)
+ o = lxml.etree.SubElement(unit, NSPREFIX + 'exchange', nsmap=nsmap())
+ o.text = str(self.uidx.ex(v))
+ #unit.append(o)
+ #units.append(unit)
+ #self.tree.append(units)
+
+ incoming = lxml.etree.SubElement(self.tree, NSPREFIX + 'incoming', nsmap=nsmap())
+ incoming.attrib['serial'] = '0'
+
+ real = lxml.etree.SubElement(incoming, NSPREFIX + 'real', nsmap=nsmap())
+ real.attrib['unit'] = self.uidx.base
+ o = lxml.etree.SubElement(real, NSPREFIX + 'asset', nsmap=nsmap())
+ o.text = '0'
+ #real.append(o)
+ o = lxml.etree.SubElement(real, NSPREFIX + 'liability', nsmap=nsmap())
+ o.text = '0'
+ #real.append(o)
+ #incoming.append(real)
+
+ o = lxml.etree.SubElement(incoming, NSPREFIX + 'digest', nsmap=nsmap())
+ o.attrib['algo'] = 'sha512'
+ o.text = DEFAULTPARENT.hex()
+ #incoming.append(o)
+ #self.tree.append(incoming)
+
+
+ """Add a decentralized identity definition for signers of the ledger.
+
+ :param keyid: Hexadecimal representation of the key identifier.
+ :type keyid: str
+ :param did: Did provider.
+ :type did: str
+ :param typ: Type of did provider, default 'web'.
+ :type did: str
+ :todo: should append after last
+ """
+ def add_identity(self, keyid, did, typ='web'):
+ root = self.tree
+ tree = root.find('resolver', namespaces=nsmap())
+ if tree == None:
+ tree = root.find('units', namespaces=nsmap())
+ if tree == None:
+ logg.debug('exception tree {}'.format(lxml.etree.tostring(self.tree)))
+ raise Exception('cannot find units node')
+ o = lxml.etree.Element(NSPREFIX + 'identity', nsmap=nsmap())
+ o.attrib['keyid'] = keyid
+ o.attrib['didtype'] = typ
+ o.text = did
+ tree.addnext(o)
+
+
+ """Add an endpoint to resolve content by digest.
+
+ :param location: The endpoint location, host and path or only path, depending on the scheme.
+ :type path: str
+ :param algo: Digest algorithm to use to retrieve and verify content, default 'sha256'.
+ :type path: str
+ :param proto: URI scheme used to connect to the endpoint.
+ :param algo: str
+ :todo: should append after last
+ """
+ def add_resolver(self, location, algo='sha256', scheme='https'):
+ tree = self.tree.find('units', namespaces=nsmap())
+ o = lxml.etree.Element(NSPREFIX + 'resolver', nsmap=nsmap())
+ o.attrib['algo'] = algo
+ o.attrib['proto'] = proto
+ o.text = path
+ tree.addnext(o)
+
+ """Check signature on individual ledger entry.
+
+ :param entry: The individual entry to verify.
+ :type entry: usawa.Entry
+ """
+ def check_sigs(self, entry):
+ have = False
+ valid_keys = None
+ if self.acl == None:
+ valid_keys = list(entry.sigs.keys())
+ else:
+ valid_keys = list(self.acl.pubkeys(binary=False))
+ logg.debug('testing valid keys {}'.format(valid_keys))
+ for k in valid_keys:
+ b = bytes.fromhex(k)
+ try:
+ sig = entry.sigs[k]
+ except KeyError:
+ continue
+ wallet = DemoWallet(publickey=b)
+ v = entry.sum()
+ r = wallet.verify(v[0], sig)
+ have = True
+ logg.debug('having sig {}'.format(r.hex()))
+ return have
+
+
+ """Append entry to ledger. The entry must have a valid signature from a trusted public key.
+
+ :param entry: The entry to append.
+ :type entry: usawa.Entry
+ :param modify_tree: If True, also append the entry to the XML export.
+ :type modify_tree: boolean
+ :todo: modify_tree is too low-level for this API
+ """
+ def add_entry(self, entry, modify_tree=True):
+ if not self.check_sigs(entry):
+ raise ValueError('entry must have at least one valid signature')
+ try:
+ entries = self.entries[entry.serial]
+ except KeyError:
+ self.entries[entry.serial] = []
+ #entries = self.entries[entry.serial]
+ self.serial = entry.serial
+ oldbase = self.base
+ self.cur = entry.sum()[0]
+ entry.parent = oldbase
+ self.entries[entry.serial].append(entry)
+ self.running[entry.unit].apply_entry(entry)
+ if self.tree != None and modify_tree:
+ self.tree.append(entry.to_tree())
+ logg.debug('entryunit {} {}'.format(entry.unit, self.running[entry.unit]))
+
+
+ """Add a signature on the ledger.
+
+ :todo: not an appropriate API function?
+ :todo: implement validity checks for signature.
+ """
+ def add_signature(self, sigdata, identity):
+ self.sigs[identity] = sigdata
+ logg.debug('add sig from key{}: {}'.format(identity, sigdata))
+
+
+ """Create a new ledger from a parsed XML document.
+
+ Does not check validity of tree against schema.
+
+ :param tree: A parsed and validated XML tree.
+ :type tree: lxml.etree.ElementTree
+ :param unitindex: Definition of all units used in the XML tree.
+ :type unitindex: usawa.UnitIndex
+ :param acl: List of public keys to validate signatures against. Overrides the keys in the entry object.
+ :type acl: usawa.ACL
+ :todo: Specify in docs which exception raised if unit not found in index.
+ """
+ @staticmethod
+ def from_tree(tree, unitindex, acl=None):
+ topic_node = tree.find('topic', namespaces=nsmap())
+ topic = bytes.fromhex(topic_node.text)
+
+ units = tree.find('units', namespaces=nsmap())
+ unit = units.get('base')
+ part = tree.find('incoming', namespaces=nsmap())
+ serial = int(part.get('serial'))
+ o = part.find('digest', namespaces=nsmap()).text # verify that is sha512
+ r = Ledger(unitindex, topic=topic, tree=tree, acl=acl, serial=serial, base=bytes.fromhex(o))
+
+ for sig in part.iter(NSPREFIX + 'sig'):
+ keyid = sig.get('keyid')
+ digest = sig.text
+ r.add_signature(digest, keyid)
+
+ o = part.find('real', namespaces=nsmap())
+ asset = int(o.find('asset', namespaces=nsmap()).text)
+ liability = int(o.find('liability', namespaces=nsmap()).text)
+ r.real = RunningTotal(unit, unitindex, asset=asset, liability=liability)
+
+ for v in part.iter(NSPREFIX + 'virt'):
+ income = int(v.find('income', namespaces=nsmap()).text)
+ expense = int(v.find('expense', namespaces=nsmap()).text)
+ asset = int(v.find('asset', namespaces=nsmap()).text)
+ liability = int(v.find('liability', namespaces=nsmap()).text)
+ sym = v.get('unit')
+ r.running[sym] = RunningTotal(sym, unitindex, income=income, expense=expense, asset=asset, liability=liability)
+ logg.debug(r.running[sym])
+
+ if r.running.get(unit) == None:
+ r.running[unit] = RunningTotal(unit, unitindex)
+
+ r.apply_tree(tree)
+ logg.debug('loaded ledger tree last serial {}'.format(r.serial))
+ return r.check()
+
+
+ """Append all entries from XML tree to ledger.
+
+ :param tree: A parsed XML tree.
+ :todo: Not an API method.
+ """
+ def apply_tree(self, tree):
+ start = self.serial
+ last = 0
+ for v in tree.iter(NSPREFIX + 'entry'):
+ logg.debug('processing entry {}'.format(v))
+ o = Entry.from_tree(v, self.uidx, min=self.serial)
+ self.add_entry(o, modify_tree=False)
+ if o.serial > last:
+ last = o.serial
+ self.serial = last
+ logg.info('last entry from tree serial ' + str(self.serial))
+
+
+ """Return XML tree representation of the state of the ledger object.
+
+ :returns: XML tree.
+ :rtype: lxml.etree.ElementTree
+ """
+ def to_tree(self):
+ return self.tree
+
+
+ """Verify digest chain and signatures in ledger.
+
+ :todo: implement, currently a no-op
+ """
+ def check(self):
+ return self
+
+
+ """Return a string representation of the XML tree.
+
+ :returns: XML document in UTF-8 format.
+ :rtype: str
+ """
+ def to_string(self):
+ return lxml.etree.tostring(self.tree)
+
+
+ """Returns the digest of the current state of the ledger.
+
+ The digest is calculated on the full chain of entries currently in the ledger.
+
+ The digest type is defined in the usawa.Entry.digest_algo.
+
+ :returns: Digest.
+ :rtype: bytes
+ """
+ def current(self):
+ return self.cur
+
+
+ def __str__(self):
+ return "state: " + self.base.hex()
diff --git a/dummy/usawa/runnable/server.py b/dummy/usawa/runnable/server.py
@@ -0,0 +1,35 @@
+import socket
+import logging
+
+logging.basicConfig(level=logging.DEBUG)
+logg = logging.getLogger()
+
+
+def parse(v):
+ logg.debug('parsing {}'.format(v.hex()))
+
+def main():
+ scks = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ scks.bind(('', 32327,))
+ scks.listen(1)
+ while True:
+ logg.debug('waiting for connection')
+ (sckc, address) = scks.accept()
+ logg.info('connect: {}'.format(address))
+ c = 0
+ data = bytearray()
+ while True:
+ b = sckc.recv(2048)
+ if len(b) == 0:
+ logg.info('connection broken: {}'.format(address))
+ break
+ for v in b:
+ if v == 0x0a:
+ logg.debug('command boundary reached')
+ parse(bytes(data))
+ data.append(v)
+ logg.debug('read {}: {}'.format(len(b), b.hex()))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/dummy/svcontas/store.py b/dummy/usawa/store.py
diff --git a/dummy/usawa/unit.py b/dummy/usawa/unit.py
@@ -0,0 +1,94 @@
+import logging
+
+from .constant import NSPREFIX
+from .xml import nsmap
+
+logg = logging.getLogger('usawa.unit')
+
+BASE_UNIT = 'BTC'
+
+
+class UnitIndex:
+
+ def __init__(self, base):
+ self.base = base
+ self.detail = {base: 0}
+ self.exchange = {base: 1}
+
+
+ def add(self, sym, precision=2, ex=1):
+ self.detail[sym] = precision
+ self.exchange[sym] = ex
+
+
+ @staticmethod
+ def from_tree(tree):
+ r = UnitIndex(tree.get('base'))
+ logg.debug('base {}'.format(tree))
+ for o in tree.iter(NSPREFIX + 'unit'):
+ logg.debug('add unit ' + o.get('sym'))
+ r.detail[o.get('sym')] = int(o.find('precision', namespaces=nsmap()).text)
+ r.exchange[o.get('sym')] = int(o.find('ex', namespaces=nsmap()).text)
+ r.check()
+ return r
+
+
+ def check(self):
+ self.get(self.base)
+ return self
+
+
+ def get(self, k):
+ return self.detail[k]
+
+
+ def sym(self, k):
+ _ = self.get(k)
+ return k
+
+
+ def ex(self, k):
+ return self.exchange[k]
+
+
+ def syms(self):
+ return list(self.detail.keys())
+
+
+ def to_floatstring(self, sym, v, allow_negative=True):
+ neg = v < 0
+ if neg and not allow_negative:
+ raise ValueError('negative value not allowed')
+ v = abs(v)
+ c = self.detail[sym]
+ i = c * -1
+ s = str(v)
+ l = len(s)
+ if l < c:
+ ss = '0' * c
+ s = '0' + ss[:c-l] + s
+ r = s[:i] + '.' + s[i:]
+ if neg:
+ r = '-' + r
+ return r
+
+
+ def from_floatstring(self, sym, v, allow_negative=True):
+ neg = False
+ if v[0] == '-':
+ if not allow_negative:
+ raise ValueError('negative value not allowed')
+ neg = True
+ v = v[1:]
+ c = self.detail[sym]
+ s = v.split('.')
+ if len(s) == 1:
+ return int(s[0]) * (10**c)
+ r = s[1]
+ l = len(r)
+ if l < c:
+ r += '0' * (c - l)
+ r = s[0] + r
+ if neg:
+ r *= -1
+ return int(r)
diff --git a/dummy/svcontas/xml.py b/dummy/usawa/xml.py