commit 1bdab059ff18a345f7f8784c17fbcee9b46d2444
parent 379e0eaf941337f388d821386ade7d47c3c2ff51
Author: lash <dev@holbrook.no>
Date: Thu, 2 Apr 2026 08:44:22 -0600
Add revert tool
Diffstat:
6 files changed, 160 insertions(+), 6 deletions(-)
diff --git a/dummy/setup.cfg b/dummy/setup.cfg
@@ -46,3 +46,4 @@ console_scripts =
usawa-view = usawa.runnable.view:main
usawa-import = usawa.runnable.import:main
usawa-export = usawa.runnable.export:main
+ usawa-revert = usawa.runnable.revert:main
diff --git a/dummy/tests/store.py b/dummy/tests/store.py
@@ -194,5 +194,43 @@ class TestStore(unittest.TestCase):
self.assertEqual(o.get_ref(binary=True), uuid_for_foo.bytes)
+ def test_store_more(self):
+ uidx = UnitIndex('FOO')
+ ledger = Ledger(uidx, topic=b'foobar')
+ store = LedgerStore(self.store, ledger)
+ wallet = DemoWallet()
+
+ dst = EntryPart('FOO', 'asset', 'foo', 1337)
+ src = EntryPart('FOO', 'income', 'foo', 1337, debit=True)
+ o = Entry(1, datetime.datetime.strptime('2025-11-11', '%Y-%m-%d'), ref=str(uuid.uuid4()), description='foo', tx_datereg=self.dtreg, unitindex=uidx)
+ o.add_part(src)
+ o.add_part(dst)
+
+ fp = os.path.join(testdir, 'test.xml')
+ asset = Asset.from_file(fp, description='foobar')
+ store.add_asset(asset)
+ o.attach(asset)
+ o.sign(wallet)
+ store.add_entry(o, update_ledger=True)
+
+ dst = EntryPart('FOO', 'expense', 'bar', 42)
+ src = EntryPart('FOO', 'asset', 'bar', -42, debit=True)
+ now = datetime.datetime.now(datetime.UTC)
+ o = Entry(2, now, parent=ledger.cur, ref=str(uuid.uuid4()), description='bar', unitindex=uidx)
+ o.add_part(src)
+ o.add_part(dst)
+ o.sign(wallet)
+ store.add_entry(o)
+
+ acl = ACL.from_wallet(wallet)
+ ledger = Ledger(uidx, topic=b'foobar')
+ store = LedgerStore(self.store, ledger)
+ store.load()
+ #r = store.get_entry(o.serial, acl=acl)
+ #self.assertEqual(r.ref, o.ref)
+ #self.assertEqual(r.description, o.description)
+ #self.assertEqual(r.attachment[0].description, 'foobar')
+
+
if __name__ == '__main__':
unittest.main()
diff --git a/dummy/usawa/context.py b/dummy/usawa/context.py
@@ -73,6 +73,17 @@ class UsawaContext:
self.load_wallet()
+ def commit(self, ledger=None):
+ if ledger == None:
+ ledger = self.ledger
+ ledger.truncate()
+ ledger.sign()
+ f = open(self.ledger_path_out, 'w')
+ f.write(ledger.to_string())
+ f.close()
+ logg.info('wrote new ledger state {} to {}'.format(ledger, self.ledger_path_out))
+
+
def getpw(self):
pw = self.cfg.get('WALLET_KEY_PASSPHRASE')
if pw == None:
diff --git a/dummy/usawa/entry.py b/dummy/usawa/entry.py
@@ -214,11 +214,26 @@ class Entry(UsawaElement):
@staticmethod
- def empty(tx_date=None, ref=None, description=None, parent=None, tx_datereg=None, unitindex=None, tags=None):
+ def empty(tx_date=None, ref=None, description=None, parent=None, tx_datereg=None, unitindex=None, tags=None, serial=-1):
if not tx_date:
tx_date = datetime.datetime.utcnow()
logg.debug('txdate is {}'.format(tx_date))
- return Entry(-1, tx_date, ref=ref, description=description, parent=parent, tx_datereg=tx_datereg, unitindex=unitindex, tags=tags)
+ return Entry(serial, tx_date, ref=ref, description=description, parent=parent, tx_datereg=tx_datereg, unitindex=unitindex, tags=tags)
+
+
+ def clone(self, unitindex=None, description=None, invert_parts=False):
+ if description == None:
+ description = self.description + ' (clone)'
+ o = Entry(-1, self.dt, ref=self.ref, description=self.description, unitindex=unitindex, tags=self.tags)
+ for v in self.debit:
+ if invert_parts:
+ v.amount *= -1
+ o.add_part(v)
+ for v in self.credit:
+ if invert_parts:
+ v.amount *= -1
+ o.add_part(v)
+ return o
"""Add an entry part to the entry.
@@ -646,10 +661,11 @@ class Entry(UsawaElement):
else:
raise ValueError('invalid lookup algo')
- b = lxml.etree.canonicalize(tree, strip_text="True", exclude_tags=['lookup'])
- h.update(b.encode('utf-8'))
+ s = lxml.etree.canonicalize(tree, strip_text="True", exclude_tags=['lookup'])
+ logg.debug('doing entry {}'.format(s))
+ h.update(s.encode('utf-8'))
- return (h.digest().hex(), b,)
+ return (h.digest().hex(), s,)
"""Generate canonical XML for signature material.
diff --git a/dummy/usawa/ledger.py b/dummy/usawa/ledger.py
@@ -490,9 +490,11 @@ class Ledger(UsawaElement):
:raises VerifyError: When entry is missing valid signature.
"""
def add_entry(self, entry, check_parent=True):
+ parent = entry.parent.hex()
if check_parent and self.cur != entry.parent:
- raise ValueError('entry parent {} does not match ledger state {}'.format(entry.parent.hex(), self.cur.hex()))
+ raise ValueError('entry parent {} does not match ledger state {}'.format(parent, self.cur.hex()))
self.check_sigs(entry)
+ logg.debug('entry state ok: ' + parent)
for fn in self.pre_cb:
if not fn(entry):
diff --git a/dummy/usawa/runnable/revert.py b/dummy/usawa/runnable/revert.py
@@ -0,0 +1,86 @@
+import argparse
+import os
+import uuid
+import logging
+import sys
+import uuid
+
+from usawa import Entry, Ledger
+from usawa.store import LedgerStore
+import usawa.config
+from usawa.context import UsawaContext
+
+logging.basicConfig(level=logging.WARNING)
+logg = logging.getLogger()
+
+QUERYTYPE_SERIAL = 1
+QUERYTYPE_UUID = 2
+QUERYTYPE_SHA512 = 3
+
+
+def parse_entry_spec(v):
+ try:
+ uu = uuid.UUID(v)
+ return (QUERYTYPE_UUID, str(uu),)
+ except ValueError:
+ pass
+
+ try:
+ r = int(v)
+ return (QUERYTYPE_SERIAL, r,)
+ except ValueError:
+ pass
+
+ r = bytes.fromhex(v)
+ if len(r) != 64:
+ raise ValueError()
+ return (QUERYTYPE_SHA512, v,)
+
+
+def main():
+ argp = argparse.ArgumentParser()
+ argp.add_argument('-c', type=str, help='override config dir')
+ argp.add_argument('-p', action='store_true', help='use passphrase to open wallet')
+ argp.add_argument('-v', type=str, choices=['info','debug','warning','error'], help='be verbose')
+ argp.add_argument('-i', type=str, help='ledger xml file input')
+ argp.add_argument('-o', type=str, help='ledger xml file output')
+ argp.add_argument('entry_spec', type=str, help='uuid, serial or hash of entry to reverse')
+ args = argp.parse_args()
+
+ if args.v:
+ logg.setLevel(getattr(logging, args.v.upper()))
+
+ cfg = usawa.config.load_config(config_dir=args.c)
+ ctx = UsawaContext(cfg)
+ ctx.init(args)
+
+ r = parse_entry_spec(args.entry_spec)
+ if r[0] != QUERYTYPE_SERIAL:
+ raise NotImplementedError("only serial lookup implemented")
+
+ o = Entry.empty(serial=r[1])
+ v = ctx.store.get_entry(o)
+
+ #ctx.store.load()
+ ledger = Ledger(ctx.uidx, topic=ctx.ledger.topic)
+ ledger.set_wallet(ctx.wallet)
+ store = LedgerStore(ctx.db, ledger)
+ store.load()
+ o = v.clone(invert_parts=True)
+ o.parent = ledger.cur
+ o.ref = str(uuid.uuid4())
+ o.description = 'revert: ' + v.ref + ' (' + str(v.serial) + ')'
+ o.serial = ledger.serial
+ o.sign(ctx.wallet)
+ v = input(str(o) + "\nCommit? (YES to confirm)): ")
+ if v != 'YES':
+ sys.exit(0)
+
+ store.add_entry(o, update_ledger=True)
+ if ctx.resolver != None:
+ ctx.resolver.put_entry(o, lookup='sha512')
+ ctx.commit(ledger=ledger)
+
+
+if __name__ == '__main__':
+ main()