commit 93638ab8d231912e1226acb00ce1aebbf6fa7a07
parent 452035fba633b71198cb3c1529704345a9275c04
Author: lash <dev@holbrook.no>
Date: Tue, 17 Feb 2026 07:08:43 +0000
Merge branch 'master' into lash/resolvers
Diffstat:
9 files changed, 156 insertions(+), 4 deletions(-)
diff --git a/dummy/doc/internals.texi b/dummy/doc/internals.texi
@@ -9,8 +9,6 @@ Signatures are calculated and embedded on three elements individually, while at
The following describes any transformation applied to the XML. Also, any @code{sig} elements are removed before .
-Where expendient, @url{https://dwarfstd.org/doc/Dwarf3.pdf,LEB128s} encoding is used for variable-length integer encoding.
-
@anchor{serialize_attachment}
@subsection Attachment
@@ -49,6 +47,8 @@ The lookup key descriptions below are enumerated. Each element in the list shoul
Any optional and undefined elements @emph{must} contain a @emph{null value} in the serialization.
+Where expendient, @url{https://dwarfstd.org/doc/Dwarf3.pdf,LEB128s} encoding is used for variable-length integer encoding.
+
@xref{store,Cache store} for more details.
@subsection Ledger
diff --git a/dummy/tests/import.xml b/dummy/tests/import.xml
@@ -0,0 +1 @@
+<ledger xmlns="http://usawa.defalsify.org/" version="1"><topic>66a739edb189684585bde211f9c29f3a47616584cbe82175f88cb4a6329f9748aea04553db62e5b1bfbd7d121356e91fe2c6142a3d2ec9664099d0be203b87e4</topic><generated>2026-02-14T09:11:33Z</generated><src>defalsify.org</src><units base="BTC"><unit sym="BTC"><precision>2</precision><exchange>1000000000</exchange></unit></units><identity keyid="3b54648d60bb8a5b9e84fa0057f79b3a5996e511682e80176dc948dcbff5a4fc" didtype="usawa"/><incoming serial="0"><real unit="BTC"><income>13370000</income><expense>421300</expense><asset>13370000</asset><liability>421300</liability></real><digest algo="sha512">00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000</digest><sig keyid="3b54648d60bb8a5b9e84fa0057f79b3a5996e511682e80176dc948dcbff5a4fc" type="ed25519">5f05a6c2d7f9b9f9a391ef6d6ea45baf813f1cead8aa647b6168b855d8300ed8e99c952142e08e8ac34ba5680e49ae1eabb98881f62429ec785f2057ff08b809</sig></incoming><entry><data><parent>00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000</parent><ref>6b4fb8cf-ae86-42c4-a51e-fe344a25c4ea</ref><serial>1</serial><date>2026-02-14</date><dateTimeRegistered>2026-02-14T09:09:31Z</dateTimeRegistered><description>Foobar</description><debit type="income"><unit>BTC</unit><account>general</account><amount>-13370000</amount></debit><credit type="asset"><unit>BTC</unit><account>general</account><amount>13370000</amount></credit><attachment mime="application/xml" uuid="b221358b-c6b8-433c-8ea2-14b5cb282f15"><digest algo="sha256">77473684a53bd344add4f55f66432e56955a134dda637e3021f4f2592ffe717b</digest><filename>test.xml</filename></attachment><attachment mime="text/plain" uuid="cb3f94e9-98d9-4f2a-9ff6-86e32ec02148"><digest algo="sha256">fb981668c18a279e285fc4d83fba1e836cc84dd4daa73c9697d3cfd2d8aca6e0</digest><filename>LICENSE</filename></attachment></data><sig type="ed25519" keyid="3b54648d60bb8a5b9e84fa0057f79b3a5996e511682e80176dc948dcbff5a4fc">f2a099adf8f5c8da17a4ddc0c3e65fc4f868da2c038691ff01311ce3018e7a65c7090fb2e312a283340b75f7c821134a3613c890c52f27c956a51010cf8d1b06</sig></entry><entry><data><parent>d2ec3d9132d40ce747a66049921fe864907e8ed8730d289a4c54bfcd6a9b8f3adcdead00f5c03cf4f5348fde609254461b16c78e1b482e05ff26281a58c76fc2</parent><ref>044e45ca-07b0-4496-bb32-61107f7c1796</ref><serial>2</serial><date>2026-02-14</date><dateTimeRegistered>2026-02-14T09:10:14Z</dateTimeRegistered><description>Barbarbar</description><debit type="expense"><unit>BTC</unit><account>luxury</account><amount>-421300</amount></debit><credit type="liability"><unit>BTC</unit><account>creditcard</account><amount>421300</amount></credit></data><sig type="ed25519" keyid="3b54648d60bb8a5b9e84fa0057f79b3a5996e511682e80176dc948dcbff5a4fc">425ae11c2d1808873c2da336a9c26386b6e45dd327f599186cca2766248bde62b38473e8a271119a6c382fa620f4c5a6de3471c2032824f76299360ef9e00009</sig></entry></ledger>
+\ No newline at end of file
diff --git a/dummy/tests/store.py b/dummy/tests/store.py
@@ -104,5 +104,22 @@ class TestStore(unittest.TestCase):
store.load(acl=acl)
+ def test_store_import(self):
+ fp = os.path.join(testdir, 'import.xml')
+ ledger = Ledger.from_file(fp)
+ store = LedgerStore(self.store, ledger)
+ store.put_all(store_assets=True)
+
+ # TODO: less hacky test, perhaps a ledger.rewind() to get to zero state with everything else intact?
+ topic = ledger.topic
+ uidx = ledger.uidx
+ acl = ledger.acl
+ ledger = Ledger(uidx, topic=topic, acl=acl)
+ store = LedgerStore(self.store, ledger)
+ store.load(acl=acl)
+ # TODO: improve this test
+ self.assertEqual(len(ledger.entries), 2)
+
+
if __name__ == '__main__':
unittest.main()
diff --git a/dummy/usawa/entry.py b/dummy/usawa/entry.py
@@ -487,7 +487,18 @@ class Entry:
return entry
+
+ """Verify signature on entry.
+
+ At least one signature must be valid for one of the public keys in the wallet or ACL.
+ :param wallet: Wallet holding a public key to verify.
+ :type wallet: usawa.Wallet
+ :param acl: A collection of public keys to verify.
+ :type acl: usawa.ACL
+ :raises VerifyError: Invalid signature.
+ :raises ValueError: Neither wallet nor ACL supplied.
+ """
def verify(self, wallet=None, acl=None):
if wallet == None and acl == None:
raise ValueError('verify needs at least one of wallet or acl')
diff --git a/dummy/usawa/ledger.py b/dummy/usawa/ledger.py
@@ -140,6 +140,7 @@ class RunningTotal:
return RunningTotal(unit, asset=asset, liability=liability)
+
"""Generate an XML tree from the current state of the object.
The XML generated can be used as a "real" or "virt" sub-element of the ledger/incoming/ element.
@@ -566,6 +567,16 @@ class Ledger:
return ledger.check()
+ @staticmethod
+ def from_file(filepath):
+ f = open(filepath, 'rb')
+ v = f.read()
+ f.close()
+ tree = lxml.etree.fromstring(v)
+ return Ledger.from_tree(tree)
+
+
+
"""Append all entries from XML tree to ledger.
:param tree: A parsed XML tree.
diff --git a/dummy/usawa/runnable/add.py b/dummy/usawa/runnable/add.py
@@ -29,6 +29,8 @@ class Context:
self.output = None
self.f = None
self.attach = []
+ self.valkey_host = None
+ self.valkey_port = None
def close(self):
@@ -72,6 +74,9 @@ class Context:
o = Asset.from_file(v)
ctx.attach.append(o)
+ ctx.valkey_host = args.valkey_host
+ ctx.valkey_port = args.valkey_port
+
return ctx
@@ -129,6 +134,9 @@ argp.add_argument('-d', '--description', dest='description', type=str, help='int
argp.add_argument('-u', '--unit', type=str, default=UnitIndex.default_unit, help='Unit to use for transaction')
argp.add_argument('--unit-precision', dest='unit_precision', type=int, default=UnitIndex.default_precision, help='Unit precision')
argp.add_argument('--unit-rate', dest='unit_precision', type=float, default=1.0, help='Unit exchange rate')
+argp.add_argument('--valkey-host', dest='valkey_host', type=str, default='localhost', help='Valkey host')
+argp.add_argument('--valkey-port', dest='valkey_port', type=int, default=6379, help='Valkey port')
+
argp.add_argument('ledger_xml_file', type=str, help='load ledger metadata from XML file')
arg = argp.parse_args()
ctx = Context.from_args(arg)
@@ -140,7 +148,7 @@ ledger_tree = load(arg.ledger_xml_file)
uidx = UnitIndex.from_tree(ledger_tree)
ledger = Ledger.from_tree(ledger_tree)
-db = ValkeyStore('')
+db = ValkeyStore('', host=ctx.valkey_host, port=ctx.valkey_port)
store = LedgerStore(db, ledger)
pk = store.get_key()
wallet = DemoWallet(privatekey=pk)
diff --git a/dummy/usawa/runnable/import.py b/dummy/usawa/runnable/import.py
@@ -0,0 +1,73 @@
+import os
+import sys
+import logging
+import urllib.parse
+import argparse
+import uuid
+import datetime
+
+from usawa import Ledger, Entry, EntryPart, DemoWallet, load, ACL
+from usawa.constant import CATEGORIES
+from usawa.store import LedgerStore
+from whee.valkey import ValkeyStore
+
+logging.basicConfig(level=logging.DEBUG)
+logg = logging.getLogger()
+
+
+class Context:
+
+ def __init__(self):
+ self.unit = None
+ self.uidx = None
+ self.output = None
+ self.f = None
+ self.valkey_host = None
+ self.valkey_port = None
+
+
+ def close(self):
+ if self.f and self.f != sys.stdout:
+ self.f.close()
+
+
+ def open(self, output):
+ if output == '<stdout>':
+ self.f = sys.stdout.buffer
+ logg.debug('output is stdout')
+ else:
+ self.f = open(output, 'wb')
+ return self
+
+ @staticmethod
+ def from_args(args):
+ ctx = Context()
+ if args.output != None:
+ ctx.output = os.path.realpath(args.output)
+ else:
+ ctx.output = '<stdout>'
+
+ ctx.valkey_host = args.valkey_host
+ ctx.valkey_port = args.valkey_port
+
+ return ctx
+
+
+argp = argparse.ArgumentParser()
+argp.add_argument('-o', type=str, dest='output', help='output file for resulting XML document')
+argp.add_argument('--valkey-host', dest='valkey_host', type=str, default='localhost', help='Valkey host')
+argp.add_argument('--valkey-port', dest='valkey_port', type=int, default=6379, help='Valkey port')
+argp.add_argument('ledger_xml_file', type=str, help='load ledger metadata from XML file')
+arg = argp.parse_args()
+ctx = Context.from_args(arg)
+
+ledger = Ledger.from_file(arg.ledger_xml_file)
+
+storedb = ValkeyStore('', host=ctx.valkey_host, port=ctx.valkey_port)
+store = LedgerStore(storedb, ledger)
+#pk = store.get_key()
+#wallet = DemoWallet(privatekey=pk)
+#acl = ACL.from_wallet(wallet)
+#store.load(acl=acl)
+store.put_all(store_assets=True)
+sys.stdout.buffer.write(ledger.to_string())
diff --git a/dummy/usawa/runnable/view.py b/dummy/usawa/runnable/view.py
@@ -22,6 +22,8 @@ class Context:
self.uidx = None
self.output = None
self.f = None
+ self.valkey_host = None
+ self.valkey_port = None
def close(self):
@@ -44,11 +46,17 @@ class Context:
ctx.output = os.path.realpath(args.output)
else:
ctx.output = '<stdout>'
+
+ ctx.valkey_host = args.valkey_host
+ ctx.valkey_port = args.valkey_port
+
return ctx
argp = argparse.ArgumentParser()
argp.add_argument('-o', type=str, dest='output', help='output file for resulting XML document')
+argp.add_argument('--valkey-host', dest='valkey_host', type=str, default='localhost', help='Valkey host')
+argp.add_argument('--valkey-port', dest='valkey_port', type=int, default=6379, help='Valkey port')
argp.add_argument('ledger_xml_file', type=str, help='load ledger metadata from XML file')
arg = argp.parse_args()
ctx = Context.from_args(arg)
@@ -58,7 +66,7 @@ ledger_tree = load(arg.ledger_xml_file)
uidx = UnitIndex.from_tree(ledger_tree)
ledger = Ledger.from_tree(ledger_tree)
-storedb = ValkeyStore('')
+storedb = ValkeyStore('', host=ctx.valkey_host, port=ctx.valkey_port)
store = LedgerStore(storedb, ledger)
pk = store.get_key()
wallet = DemoWallet(privatekey=pk)
diff --git a/dummy/usawa/store.py b/dummy/usawa/store.py
@@ -282,3 +282,25 @@ class LedgerStore(Interface):
"""
def get(self, k):
return self.__o.get(k)
+
+
+ """Store all entries in the ledger state.
+
+ Errors due to duplicate entry and asset insert attempts will be ignored.
+
+ :param store_assets: Add all attachment assets from each entry.
+ :type store_assets: boolean
+ :raises FileExistsError: If duplicate entry is found.
+ """
+ def put_all(self, store_assets=False):
+ for k in self.ledger.entries.keys():
+ entry = self.ledger.entries[k]
+ try:
+ self.add_entry(entry, update_ledger=False)
+ except FileExistsError as e:
+ logg.info('putall skip duplicate entry {}'.format(entry))
+ for asset in entry.attachment:
+ try:
+ self.add_asset(asset)
+ except FileExistsError:
+ logg.info('putall skip duplicate asset {}'.format(asset))