commit 224d472023aad3f5e5c4fff5d7dcae3540ea7cf8
parent f8eeccc6dedaa0fccd5a6bb54b2a113cf96692bb
Author: lash <dev@holbrook.no>
Date: Thu, 6 Nov 2025 04:55:41 +0000
Define xsd schema
Diffstat:
5 files changed, 167 insertions(+), 47 deletions(-)
diff --git a/README b/README
@@ -12,8 +12,6 @@ running.xml is an example of a fully flattened ledger with signature chains and
* How to resolve content hashes (attachments)
* Reference to more information about identities behind public keys.
-It is still missing a schema, that's next step.
-
present.xsl is a small stylesheet stub just to demonstrate how xsl transformations work.
---
@@ -49,4 +47,12 @@ python create.py --xml-file two.xml -a foobar -t expense 12 > two.xml
etc..
```
+---
+
+schema.xsd is the schema for the ledger xml object.
+Validate the xml with:
+
+```
+xmllint --schema schema.xsd --pretty 1 file.xml
+```
diff --git a/dummy/create.py b/dummy/create.py
@@ -5,16 +5,11 @@ import lxml.etree
import confini
import nacl.signing
-from svcontas import Ledger, Entry, DemoWallet, State, get_units, init_ledger
+from svcontas import Ledger, Entry, DemoWallet, State, get_units, init_ledger, nsmap
seed = bytes.fromhex('2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae')
-state_serial = 0
-state_digest = b'00' * 64
-
-
-
if __name__ == '__main__':
now = datetime.datetime.now()
argp = argparse.ArgumentParser()
@@ -40,5 +35,6 @@ if __name__ == '__main__':
entry.sign(wallet)
ledger.add_entry(entry)
tree = ledger.to_tree()
- r = lxml.etree.tostring(tree, method='c14n2', strip_text=True)
+ #r = lxml.etree.tostring(tree, method='c14n2', strip_text=True, inclusive_ns_prefixes='sv')
+ r = lxml.etree.tostring(tree, method='xml', standalone=True, xml_declaration=True, encoding='UTF-8')
print(r.decode('utf-8'))
diff --git a/dummy/empty.xml b/dummy/empty.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<ledger>
+<ledger xmlns="http://svcontas.defalsify.org">
<retrieved>2025-11-02T13:09:55Z</retrieved>
<src>playalastunas.org</src>
<units base="BTC">
@@ -17,24 +17,25 @@
<identity keyid="f1d2d2f924e986ac86fdf7b36c94bcdf32beec15" didtype="web">nondominium.org/lash/</identity>
<identity keyid="f1d2d2f924e986ac86fdf7b36c94bcdf32beec15" didtype="web">holbrook.no</identity>
<incoming serial="231">
- <digest algo="sha512">b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944cb5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c</digest>
- <sig type='ed25519' keyid="58851ae2166b3f1454193e0a7e821402dd1c1a91">1f74a9c0196a025f27d4e940c4abedfa2d37504f268aea359659cb65b85bd4d7974369507950006964c93391d3d4580ab8064f6d30a62908468ef771be952e95</sig>
- <sig type='ed25519' keyid="566c38287d3f31c7e50836cae58e426c6bccc52d">117a57c72ed210b91469307a1c2e73fe2d5ee306cd8ccf1a9db4ecb15d38ecbbfc97d62fec4ab8aadb08c531f2d1ede34cb6e4d3987bcba63322a0767e532e13</sig>
<real unit="BTC">
<asset>6323141</asset>
<liability>0</liability>
</real>
- <virt symbol="USD">
+ <virt unit="USD">
<income>4213</income>
<expense>77718</expense>
<asset>11400</asset>
<liability>22284</liability>
</virt>
- <virt symbol="BTC">
+ <virt unit="BTC">
<income>3249191</income>
<expense>0</expense>
<asset>3249191</asset>
<liability>0</liability>
</virt>
+ <digest algo="sha512">b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944cb5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c</digest>
+ <sig type='ed25519' keyid="58851ae2166b3f1454193e0a7e821402dd1c1a91">1f74a9c0196a025f27d4e940c4abedfa2d37504f268aea359659cb65b85bd4d7974369507950006964c93391d3d4580ab8064f6d30a62908468ef771be952e95</sig>
+ <sig type='ed25519' keyid="566c38287d3f31c7e50836cae58e426c6bccc52d">117a57c72ed210b91469307a1c2e73fe2d5ee306cd8ccf1a9db4ecb15d38ecbbfc97d62fec4ab8aadb08c531f2d1ede34cb6e4d3987bcba63322a0767e532e13</sig>
+
</incoming>
</ledger>
diff --git a/dummy/schema.xsd b/dummy/schema.xsd
@@ -0,0 +1,113 @@
+<?xml version="1.0" ?>
+
+<xs:schema xmlns:xs = "http://www.w3.org/2001/XMLSchema"
+ targetNamespace = "http://svcontas.defalsify.org"
+ xmlns = "http://svcontas.defalsify.org"
+ elementFormDefault = "qualified">
+
+ <xs:element name="ledger">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element name="retrieved" type="xs:dateTime" />
+ <xs:element name="src" type="xs:string" minOccurs="1" maxOccurs="1" />
+ <xs:element name="units" type="Units" minOccurs="1" maxOccurs="1" />
+ <xs:element name="resolver" type="Resolver" />
+ <xs:element name="identity" type="Identity" maxOccurs="unbounded" />
+ <xs:element name="incoming" type="Incoming" minOccurs="0" maxOccurs="1" />
+ <xs:element name="entry" type="Entry" minOccurs="0" maxOccurs="unbounded" />
+ </xs:sequence>
+ </xs:complexType>
+ </xs:element>
+
+ <xs:complexType name="Incoming">
+ <xs:sequence>
+ <xs:element name="real" type="Balance" />
+ <xs:element name="virt" type="Balance" minOccurs="0" maxOccurs="unbounded" />
+ <xs:element name="digest" type="Digest" minOccurs="1" maxOccurs="1"/>
+ <xs:element name="sig" type="Signature" minOccurs="1" maxOccurs="unbounded" />
+ </xs:sequence>
+ <xs:attribute name="serial" type="xs:positiveInteger" />
+ </xs:complexType>
+
+ <xs:complexType name="Balance">
+ <xs:sequence>
+ <xs:element name="income" type="xs:integer" minOccurs="0"/>
+ <xs:element name="expense" type="xs:integer" minOccurs="0" />
+ <xs:element name="asset" type="xs:integer" />
+ <xs:element name="liability" type="xs:integer" />
+ </xs:sequence>
+ <xs:attribute name="unit" type="xs:string" />
+ </xs:complexType>
+
+ <xs:complexType name="Units">
+ <xs:sequence>
+ <xs:element name="unit" type="Unit" minOccurs="1" maxOccurs="unbounded" />
+ </xs:sequence>
+ <xs:attribute name="base" type="xs:string" />
+ </xs:complexType>
+
+ <xs:complexType name="Digest">
+ <xs:simpleContent>
+ <xs:extension base="xs:string">
+ <xs:attribute name="algo" type="xs:string" />
+ </xs:extension>
+ </xs:simpleContent>
+ </xs:complexType>
+
+ <xs:complexType name="Signature">
+ <xs:simpleContent>
+ <xs:extension base="xs:string">
+ <xs:attribute name="keyid" type="xs:string" />
+ <xs:attribute name="type" type="xs:string" />
+ </xs:extension>
+ </xs:simpleContent>
+ </xs:complexType>
+
+ <xs:complexType name="Unit">
+ <xs:sequence>
+ <xs:element name="precision" type="xs:positiveInteger" maxOccurs="1" minOccurs="1" />
+ <xs:element name="ex" type="xs:positiveInteger" maxOccurs="1" minOccurs="1" />
+ </xs:sequence>
+ <xs:attribute name="sym" type="xs:string" />
+ </xs:complexType>
+
+ <xs:complexType name="Identity">
+ <xs:simpleContent>
+ <xs:extension base="xs:string">
+ <xs:attribute name="keyid" type="xs:string" />
+ <xs:attribute name="didtype" type="xs:string" />
+ </xs:extension>
+ </xs:simpleContent>
+ </xs:complexType>
+
+ <xs:complexType name="Resolver">
+ <xs:simpleContent>
+ <xs:extension base="xs:string">
+ <xs:attribute name="algo" type="xs:string" />
+ <xs:attribute name="proto" type="xs:string" />
+ </xs:extension>
+ </xs:simpleContent>
+ </xs:complexType>
+
+ <xs:complexType name="Entry">
+ <xs:sequence>
+ <xs:element name="data" type="EntryData" />
+ <xs:element name="sig" type="Signature" minOccurs="1" maxOccurs="unbounded" />
+ </xs:sequence>
+ <!-- TODO: attribute enum -->
+ <xs:attribute name="type" type="xs:string" />
+ </xs:complexType>
+
+ <xs:complexType name="EntryData">
+ <xs:sequence>
+ <xs:element name="parent" type="xs:string" />
+ <xs:element name="ref" type="xs:string" />
+ <xs:element name="serial" type="xs:positiveInteger" />
+ <xs:element name="unit" type="xs:string" />
+ <xs:element name="date" type="xs:date" />
+ <xs:element name="dateTimeRegistered" type="xs:dateTime" />
+ <xs:element name="account" type="xs:string" />
+ <xs:element name="amount" type="xs:positiveInteger" />
+ </xs:sequence>
+ </xs:complexType>
+</xs:schema>
diff --git a/dummy/svcontas/__init__.py b/dummy/svcontas/__init__.py
@@ -12,7 +12,12 @@ logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
DEFAULTPARENT = b'\x00' * 64
+NS = 'http://svcontas.defalsify.org'
+NAMESPACES = {None: NS}
+NSPREFIX = '{' + NS + '}'
+def nsmap():
+ return NAMESPACES
class State:
@@ -92,9 +97,11 @@ class UnitIndex:
@staticmethod
def from_tree(tree):
r = UnitIndex(tree.get('base'))
- for o in tree.iter('unit'):
- r.detail[o.get('sym')] = int(o.find('precision').text)
- r.exchange[o.get('sym')] = int(o.find('ex').text)
+ 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
@@ -186,20 +193,20 @@ class Entry:
@staticmethod
def from_tree(tree, unitindex):
- o = tree.find('data')
- amount = int(o.find('amount').text)
- unit = unitindex.get(o.find('unit').text)
- serial = int(o.find('serial').text)
- account = o.find('account').text
- ref = o.find('ref').text
- parent = o.find('parent').text
- description = o.find('description')
+ o = tree.find('data', namespaces=nsmap())
+ amount = int(o.find('amount', namespaces=nsmap()).text)
+ unit = unitindex.get(o.find('unit', namespaces=nsmap()).text)
+ serial = int(o.find('serial', namespaces=nsmap()).text)
+ account = o.find('account', namespaces=nsmap()).text
+ 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').text)
- dtreg = datetime.datetime.strptime(o.find('dateTimeRegistered').text, '%Y-%m-%dT%H:%M:%SZ')
+ 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')
r = Entry(tree.get('type'), amount, unit, serial, account, dt, ref=ref, parent=parent, tx_datereg=dtreg, description=description)
- for sig in tree.iter('sig'):
+ for sig in tree.iter(NSPREFIX + 'sig'):
r.add_signature(sig.get('keyid'), bytes.fromhex(sig.text))
return r
@@ -336,7 +343,6 @@ class Ledger:
def __init__(self, serial, base, unitindex, tree=None):
self.uidx = unitindex
- #self.base = bytes.fromhex(base)
self.sigs = {}
self.entries = {}
self.running = {}
@@ -360,13 +366,11 @@ class Ledger:
def add_entry(self, entry, modify_tree=True):
if not self.check_sigs(entry):
raise ValueError('entry must have at least one valid signature')
- self.running[entry.unit].apply_entry(entry)
try:
entries = self.entries[entry.serial]
except KeyError:
self.entries[entry.serial] = []
- entries = self.entries[entry.serial]
- #self.state.base = entry.sum()
+ #entries = self.entries[entry.serial]
self.state.poke(entry.serial, entry.sum())
self.entries[entry.serial].append(entry)
self.running[entry.unit].apply_entry(entry)
@@ -382,28 +386,28 @@ class Ledger:
@staticmethod
def from_tree(tree, unitindex):
- part = tree.find('incoming')
+ part = tree.find('incoming', namespaces=nsmap())
serial = int(part.get('serial'))
- o = part.find('digest').text # verify that is sha512
+ o = part.find('digest', namespaces=nsmap()).text # verify that is sha512
r = Ledger(serial, bytes.fromhex(o), unitindex, tree=tree)
- for sig in part.iter('sig'):
+ for sig in part.iter(NSPREFIX + 'sig'):
keyid = sig.get('keyid')
digest = sig.text
r.add_signature(digest, keyid)
- o = part.find('real')
- asset = int(o.find('asset').text)
- liability = int(o.find('liability').text)
+ 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('.', unitindex, asset=asset, liability=liability)
logg.debug(r.real)
- for v in part.iter('virt'):
- income = int(v.find('income').text)
- expense = int(v.find('expense').text)
- asset = int(v.find('asset').text)
- liability = int(v.find('liability').text)
- sym = v.get('symbol')
+ 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])
@@ -412,7 +416,7 @@ class Ledger:
def apply_tree(self, tree):
- for v in tree.iter('entry'):
+ for v in tree.iter(NSPREFIX + 'entry'):
logg.debug('processing entry {}'.format(v))
o = Entry.from_tree(v, self.uidx)
self.add_entry(o, modify_tree=False)
@@ -435,7 +439,7 @@ def init_ledger(tree, units):
def get_units(tree):
- o = tree.find('units')
+ o = tree.find('units', namespaces=nsmap())
return UnitIndex.from_tree(o)