commit d228482a4dc102002d7df8fbf2d65da4e467feae
parent 4a04d9a8bcbbbf56f984eb785247a08ff2dc39af
Author: lash <dev@holbrook.no>
Date: Mon, 5 Jan 2026 18:56:42 +0100
Add tool stub for adding entry to ledger
Diffstat:
4 files changed, 171 insertions(+), 11 deletions(-)
diff --git a/dummy/usawa/crypto.py b/dummy/usawa/crypto.py
@@ -32,6 +32,16 @@ class Wallet:
raise NotImplementedError
+ def privkey(self):
+ """Return the private key data in the wallet.
+
+ :returns: Private key data.
+ :rtype: bytes
+ :todo: Raise local error if sign fail
+ """
+ raise NotImplementedError
+
+
def verify(self, v, sig):
"""Verify signature data against the given message.
@@ -80,6 +90,11 @@ class DemoWallet(Wallet):
return self.pubk.encode()
+ def privkey(self, passphrase=None):
+ """Implements usawa.Wallet.privkey
+ """
+ return self.pk.encode()
+
def verify(self, v, sig):
"""Implements usawa.Wallet.verify
"""
diff --git a/dummy/usawa/runnable/add.py b/dummy/usawa/runnable/add.py
@@ -0,0 +1,75 @@
+import logging
+import urllib.parse
+import argparse
+import uuid
+import datetime
+
+from usawa import Ledger, Entry, EntryPart, DemoWallet, UnitIndex, load
+from usawa.store import LedgerStore
+from whee.valkey import ValkeyStore
+
+logging.basicConfig(level=logging.DEBUG)
+logg = logging.getLogger()
+
+
+def parse_type(v):
+ if v not in ['expense', 'income', 'liability', 'asset']:
+ raise ValueError('invalid type')
+ return v
+
+
+def parse_account(v):
+ logg.warning('account parsing is noop')
+ return v
+
+
+def parse_amount(uidx, sym, v):
+ return uidx.from_floatstring(sym, v)
+
+
+argp = argparse.ArgumentParser()
+argp.add_argument('-f', type=str, help='load ledger metadata from XML file')
+argp.add_argument('--topic', type=str, help='hexadecimal topic to load')
+arg = argp.parse_args()
+
+ledger = None
+unit = 'BTC'
+uidx = UnitIndex(unit)
+logg.warning('hardcoding unit index default sym, need unitindex xml parser')
+logg.warning('using default sym for all entries for now')
+if arg.f:
+ ledger_tree = load(arg.f)
+ ledger = Ledger.from_tree(ledger_tree, uidx)
+
+db = ValkeyStore('')
+store = LedgerStore(db, ledger)
+pk = store.get_key()
+wallet = DemoWallet(privatekey=pk)
+dt = datetime.datetime.now()
+
+v = input('Entry description: ')
+dsc = v
+
+pair = []
+amount = None
+for k in ['src', 'dst']:
+ v = input('Entry {} type: '.format(k))
+ typ = parse_type(v)
+
+ v = input('Entry {} account: '.format(k))
+ account = parse_account(v)
+
+ if amount != None:
+ amount *= -1.0
+ else:
+ v = input('Entry {} amount: '.format(k))
+ amount = parse_amount(uidx, unit, v)
+
+ pair.append(EntryPart(typ, account, amount))
+
+ref = uuid.uuid4()
+logg.debug('generated ref {}'.format(ref))
+
+entry = Entry(pair[0], pair[1], unit, ledger.serial, dt, parent=ledger.current(), description=dsc, ref=str(ref))
+entry.sign(wallet)
+ledger.add_entry(entry)
diff --git a/dummy/usawa/runnable/create.py b/dummy/usawa/runnable/create.py
@@ -1,7 +1,10 @@
+import os
import logging
import urllib.parse
from usawa import Ledger, DemoWallet, UnitIndex
+from usawa.store import LedgerStore
+from whee.valkey import ValkeyStore
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
@@ -28,6 +31,7 @@ def parse_unit(v):
raise ValueError('invalid unit string')
return v.upper()
+
print("Creating new ledger")
v = input("Topic: ")
@@ -37,23 +41,49 @@ if len(v) > 0:
topic = bytes.fromhex(r)
logg.debug('topic {} -> {}'.format(v, topic))
-v = input("Default unit: (default: BTC)")
+v = input("Default unit: (default: BTC): ")
if len(v) == 0:
v = 'BTC'
unit = parse_unit(v)
-v = input("Unit decimals (default: 2):")
+v = input("Unit decimals (default: 2): ")
if len(v) == 0:
v = 2
dec = int(v)
uidx = UnitIndex(unit, precision=dec)
-v = input("Source URI:")
+v = input("Source URI: ")
src = None
if len(v) > 0:
o = urllib.parse.urlparse(v)
src = urllib.parse.urlunparse(o)
+v = input("XML output filename (default: start.xml):")
+fp = None
+if len(v) == 0:
+ fp = os.path.join('.', 'start.xml')
+fp = os.path.realpath(fp)
+
+
ledger = Ledger(uidx, topic=topic, src=src)
-print(ledger.to_string())
+
+
+db = ValkeyStore('')
+store = LedgerStore(db, ledger)
+pk = None
+wallet = None
+try:
+ pk = store.get_key()
+except FileNotFoundError:
+ logg.info('no default key found')
+ wallet = DemoWallet()
+ store.add_key(wallet)
+if wallet == None:
+ wallet = DemoWallet(privatekey=pk)
+ logg.info('loaded existing key. {}'.format(wallet.pubkey().hex()))
+
+logg.info('writing XML to file: {}'.format(fp))
+f = open(fp, 'wb')
+f.write(ledger.to_string())
+f.close()
diff --git a/dummy/usawa/store.py b/dummy/usawa/store.py
@@ -16,8 +16,11 @@ PFX_ENTRY = b'\x04'
logg = logging.getLogger('usawa.store')
-def pfx_key():
- return PFX_KEY
+def pfx_key(pubkey=None):
+ v = PFX_KEY
+ if pubkey == None:
+ return v
+ return v + pubkey
def pfx_ledger_topic(topic):
@@ -88,7 +91,6 @@ class LedgerStore(Interface):
self.__o = implementation
- # WIP implementation of couchdb?
def start(self):
serial = 0
k = pfx_ledger_topic(self.ledger.topic)
@@ -101,7 +103,6 @@ class LedgerStore(Interface):
self.ledger.serial = serial
- # WIP implementation of couchdb?
def lock(self):
k = pfx_ledger_lock(self.ledger.topic)
v = None
@@ -114,7 +115,6 @@ class LedgerStore(Interface):
# atomic until here
- # WIP implementation of couchdb?
def unlock(self):
k = pfx_ledger_lock(self.ledger.topic)
v = self.__o.delete(k)
@@ -150,14 +150,54 @@ class LedgerStore(Interface):
return Entry.unwrap(v, acl=acl)
+ """Flush ledger and load all entries from store.
+
+ The existing state will always be lost.
+
+ If the load fails, the ledger will be reset before returning.
+
+ :raises FileNotFoundError: If an entry cannot be found.
+ """
def load(self):
- logg.debug('load ledger {}'.format(self.ledger.acl))
+ self.ledger.reset()
+ logg.debug('load ledger from store {}'.format(self.ledger))
while True:
o = None
try:
o = self.get_entry(self.ledger.serial)
except FileNotFoundError:
+ self.ledger.reset()
break
- logg.debug('entry {}'.format(o))
self.ledger.add_entry(o, modify_tree=True)
self.ledger.next_serial()
+
+
+ """Add signing key to the store.
+
+ If this is the first key in the store, it will be set as default.
+
+ :param wallet: The wallet object to store keys for.
+ :type wallet: usawa.Wallet implementation
+ :param default: If True, this key will be set as default key.
+ :type default: bool
+ :todo: Currently the signing key is stored literally. It needs encryption!
+ """
+ def add_key(self, wallet, default=False):
+ k = pfx_key()
+ try:
+ self.__o.get(k)
+ except FileNotFoundError:
+ default = True
+ pubkey = wallet.pubkey()
+ if default:
+ self.__o.put(k, pubkey, exist_ok=True)
+ k = pfx_key(pubkey=pubkey)
+ self.__o.put(k, wallet.privkey())
+
+
+ def get_key(self, pubkey=None):
+ if pubkey == None:
+ k = pfx_key()
+ pubkey = self.__o.get(k)
+ k = pfx_key(pubkey=pubkey)
+ return self.__o.get(k)