usawa

Signed, immutable accounting.
Log | Files | Refs | Submodules | LICENSE

commit 1bdab059ff18a345f7f8784c17fbcee9b46d2444
parent 379e0eaf941337f388d821386ade7d47c3c2ff51
Author: lash <dev@holbrook.no>
Date:   Thu,  2 Apr 2026 08:44:22 -0600

Add revert tool

Diffstat:
Mdummy/setup.cfg | 1+
Mdummy/tests/store.py | 38++++++++++++++++++++++++++++++++++++++
Mdummy/usawa/context.py | 11+++++++++++
Mdummy/usawa/entry.py | 26+++++++++++++++++++++-----
Mdummy/usawa/ledger.py | 4+++-
Adummy/usawa/runnable/revert.py | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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()