Commit graph

57 commits

Author SHA1 Message Date
439c47ceae docs: repoint migrated issue refs to spirekeeper numbers
Follow-up to the satmachineadmin->spirekeeper issue migration. The 20
open issues were recreated on aiolabs/spirekeeper with reassigned
numbers; this repoints in-repo references to the migrated issues at
their new spirekeeper numbers (#3->#1, #4->#2, #8->#4, #9->#5, #10->#6,
#17->#11, #21->#12, #28->#16, #44->#20). References to closed/non-
migrated satmachineadmin issues (#20/#22/#26/#29/#32/#37/#38/#39) stay
pointing at the original repo where they were resolved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:42:56 +02:00
12f39226f0 feat(v2): fee_transport — kind-30078 publisher for operator fee config (#39 2/3)
Adds the second operator-pushed kind-30078 document type alongside
cassette config (#29). Wire format locked at coord-log §2026-06-01T14:25Z.

models.py:
- FeePayloadComponents — producer-mandatory `components` sub-object
  with super + operator splits per direction. Consumer-optional in v1
  but ships on every payload from this producer for audit + future-
  promo extensibility.
- FeeConfigPayload — the wire-format envelope. Pydantic validators
  enforce: cash_*_fee_fraction in [0, 0.15] (cap per direction);
  |total - (super + operator)| < 1e-6 (consistency assert per the
  §07:33Z lnbits advisory, mirrored on bitspire's #57 consumer side);
  schema_version integer ≥ 1.

fee_transport.py:
- build_fee_payload(super_config, machine) — compose + validate in
  one call; returned payload is wire-shippable. Raises ValueError
  (via Pydantic) if the constructed totals violate the cap. That
  shouldn't happen in practice because the API guards in
  views_api._assert_machine_fee_cap_safe + _assert_super_config_cap_safe
  refuse cap-violating writes; if it does, refuse-to-publish rather
  than ship a malformed event.
- publish_fee_config(machine, super_config, operator_user_id) —
  builds, encrypts, signs, publishes via the shared
  publish_encrypted_kind_30078 helper from nostr_publish. d-tag is
  `bitspire-fees:<atm_pubkey_hex>` per spec; recipient is the ATM
  npub canonicalised to hex; signer is the operator.
- Soft-fail discipline matches cassette_transport.publish_to_atm —
  transport-layer errors (RelayUnavailable / SignerUnavailable /
  OperatorIdentityMissing) log WARN + return None so trigger callers
  (api_create_machine etc.) don't break on transient transport hiccups.
  Cap violations are NOT soft-fail since they indicate an API-guard
  bypass and need operator attention.

Tests (18 cases, all green):
- 9 FeeConfigPayload validator cases (well-formed accept, wire round-
  trip, cap violations per direction, exact-cap acceptance, sum/
  components mismatch per direction, schema_version ≥ 1, zero-zero
  free-charge ATM)
- 4 build_fee_payload composition cases (basic, asymmetric directions,
  super-only-no-operator default, cap violation at build time)
- 5 publish_fee_config soft-fail discipline cases (relay unavailable,
  signer unavailable, operator identity missing, publish success with
  d-tag + recipient + payload-shape assertions, cap violation raises
  before reaching publish)

182/182 tests green.

Refs: aiolabs/satmachineadmin#37 (parent), #39 (Layer 2), coord-log
§2026-06-01T14:25Z (locked wire format), §2026-06-01T07:33Z (lnbits
consistency-assert advisory).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 20:00:29 +02:00
1babdfbf06 feat(v2): principal-based fee split — fixes super under-payment (#38 3/5)
Replaces the broken fraction-of-fee math with fraction-of-principal,
direction-aware. Pre-#38: super_fee_fraction was interpreted as
`round(fee_sats * super_fraction)`, paying super ~13× below intent on
every cashout since the bitspire wire-shape landed. Post-#38: super
and operator shares are computed independently against principal
using the per-direction fractions from SuperConfig + Machine.

Per workspace CLAUDE.md "Backwards-compatibility on pre-public-launch
code" (v2-bitspire hasn't shipped to users), no compat shims:

- calculations.py: delete `split_two_stage_commission` (legacy
  fraction-of-fee). Keep `split_principal_based` as the sole split fn.
- migrations.py m009: extend to also DROP the deprecated
  `super_fee_fraction` column after backfilling its value into the
  new directional fields.
- models.py: drop `super_fee_fraction` from SuperConfig +
  UpdateSuperConfigData entirely.
- bitspire.py parse_settlement: new signature takes `super_config:
  SuperConfig` instead of `super_fee_fraction: float`. Resolves
  directional fractions from super_config + machine by tx_type, then
  computes via split_principal_based. Raises SettlementInvariantError
  on unknown tx_type.
- tasks.py: pass `super_config` through to parse_settlement; assert
  non-None (m001 inserts the singleton at install time — None is an
  impossible state).
- partial-dispense ratio path in distribution.py is unchanged — still
  uses `settlement.platform_fee_sats / settlement.fee_sats` from the
  landed row, which is the right invariant (lock at landing) and
  independent of the per-direction config.

Tests:
- Rename `test_two_stage_split.py` → `test_operator_split_legs.py`.
  Drop the legacy-function test classes. Keep TestAllocateOperatorSplitLegs
  (still-production fn) and TestPartialDispenseSplitRatio (inline ratio
  math in distribution.py).
- New `test_principal_based_fees.py`: pure-math tests for
  `split_principal_based` (six cases including a direct regression
  test pinning the pre-#38 bug at 240→3000 sats per 100k principal at
  3% super), plus parse_settlement directional dispatch tests
  (cash-in routes through cash-in fractions; cash-out through
  cash-out; unknown tx_type raises; zero-zero free-charge ATM; cross-
  direction guard).

Migration verified end-to-end via container restart: super_config
columns post-m009 = id/super_fee_wallet_id/updated_at/
super_cash_in_fee_fraction/super_cash_out_fee_fraction (no
super_fee_fraction). dca_machines + dca_settlements gained the
expected new columns. 156/156 tests green.

Refs: aiolabs/satmachineadmin#37 (parent), #38 (this layer). Closes
the load-bearing super under-payment bug standalone.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 11:24:09 +02:00
d87d0db324 feat(v2): m009 + models — split fee fractions by direction (#38 1/5)
Adds the schema delta + Pydantic mirror for per-direction fee
configuration:

- super_config gains super_cash_in_fee_fraction / super_cash_out_fee_fraction
  (backfilled from the deprecated singleton on migrate-up so live config
  preserves intent).
- dca_machines gains operator_cash_in_fee_fraction / operator_cash_out_fee_fraction
  (default 0; operator-settable per machine via the upcoming UI).
- dca_settlements gains fee_mismatch_sats BIGINT NULL — Phase-1 observability
  column per coord-log §2026-06-01T07:00Z (lnbits) + option A locked.
- MAX_FEE_FRACTION_PER_DIRECTION = 0.15 lives in calculations.py as the
  single source of truth (defense-in-depth cap, mirrored on the consumer
  side per aiolabs/lamassu-next#57).

Pydantic validators on the new fields keep [0, 1] range checks; the
per-direction cap validation lives on the CRUD path in the next commit
(needs cross-row context: super-config change must validate against all
machines, machine change against current super-config).

Closes one step of #38 (Layer 1 of the operator-configurable fee
architecture, parent #37). Subsequent commits add CRUD, principal-based
split math (fixes the load-bearing super under-payment bug), and the
UI surface.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 10:18:37 +02:00
d448fab0d2 chore(v2): lint pass — black + ruff auto-fix + mypy regressions (#29 v1.1)
Pre-merge lint hygiene on the PR #30 touched files:

- `black` reformatted 9 files (cassette_transport, crud, models, tasks,
  views_api, nip44, all 3 cassette test files, migrations). Cosmetic:
  line lengths, trailing commas, multi-line argument layout.
- `ruff check --fix` cleared 176 of 202 errors auto-fixed. Mostly
  `UP006` `typing.Optional` → `| None` modernization, `I001` import
  sort order, `UP035` typing-extensions cleanup.
- Two new mypy regressions introduced by the migration commit dcb7de0
  fixed:
  - `crud.py:apply_bootstrap_state` — annotated `existing_first: dict
    | None` on the dedup fetch.
  - `tasks.py:_cassette_consumer_tick` — `# type: ignore[arg-type]` on
    the `nostr_client.relay_manager.add_subscription` call; nostrclient's
    upstream typing declares `list[str]` for filters but the actual
    Nostr protocol takes `list[<filter-dict>]`. The runtime accepts it
    (live smoke at 13:43Z dispatched `nip44_decrypt` cleanly through
    this subscription); the typing mismatch is upstream's.

Remaining lint state, intentionally not addressed in this commit
(all pre-existing baseline, not regressions):
- 8 mypy errors in `calculations.py` + the unchanged-by-this-PR parts
  of `crud.py` — pre-existing on v2-bitspire.
- 26 ruff style warnings: 14 are N805 false-positives on Pydantic
  validators (`cls` first-arg is correct for `@validator`-decorated
  methods); 4 are N818 exception-name-suffix preferences on my new
  exception classes (renaming would touch many call sites; keep
  `OperatorIdentityMissing` / `SignerUnavailable` / `RelayUnavailable`
  / `_NostrclientUnavailable` as-is for clarity); 5 are E501 line-too-
  long on docstrings (the long lines are formatted for clarity);
  1 RUF002 unicode-minus in a docstring.

Tests: 155 passed, 1 pre-existing async-plugin failure unchanged.
Live smoke (both publish + consume directions through the bunker)
unaffected — this is purely a code-style pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 15:50:14 +02:00
427cad33de refactor(v2): cassette models — position-keyed wire shape (#29 v1.1)
The wire format flips from
  {"denominations": {"<denom>": {"position", "count"}}}
to
  {"positions": {"<pos>": {"denomination", "count"}}}

per coord-log 2026-05-30T18:30Z + 18:45Z.

Per-row editable surface changes:
  - denomination becomes mutable (operator swaps cartridges during refill)
  - count remains mutable
  - position becomes the row identity (hardware bay number)

Removed: the no_duplicate_positions validator (no longer relevant — position
is the key, dups are impossible at the dict level) and the implicit
"denomination unique" assumption. Multiple positions with the same
denomination are now operationally valid per bitspire 18:45Z.

CassetteConfig model adds state_denomination (Optional[int]) for v2
reverse-channel reconciliation diff highlighting.

Tests under tests/test_cassette_configs.py and test_cassette_state_consumer.py
will fail at this commit — they reference the old denomination-keyed
shape. They get rewritten in v1.1 commit f. Branch tip is green only after
commit f lands; this and the next 3 commits are intermediate states.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 22:23:51 +02:00
13684e7134 feat(v2): m007 cassette_configs schema + Pydantic models (#29 v1)
Add the operator-side schema for per-machine ATM cassette inventory
(aiolabs/satmachineadmin#29). Schema choice mirrors the ATM-side
denomination-as-key invariant audited at coord-log 2026-05-30T06:40Z
across bitspire/atm-tui/src/db.zig:31, lamassu-next state-store.ts:54,
and hal-service.ts:116/189 — every ATM layer keys on denomination, so
the operator-side PK is (machine_id, denomination) to make
duplicate-denomination payloads impossible at the schema boundary.

Reserved nullable columns (state_count, state_at, state_event_id) hold
the latest bitspire-cassettes-state:<atm_pubkey_hex> event the ATM
publishes. v1 populates them on bootstrap-event receipt; v1 UI doesn't
render reconciliation. v2 reconciliation UI consumes them without a
migration.

Pydantic models in this commit:
  - CassetteConfig — read model for a stored row
  - UpsertCassetteConfigData — operator-edit form (count and/or position)
  - CassettePayloadRow — one denomination's wire-format values
  - PublishCassettesPayload — the full kind-30078 content payload,
    bidirectional (operator → ATM and ATM → operator share the shape).
    Validates int-coerced denomination keys, positive ints, no duplicate
    positions, and exposes to_wire_dict() that re-stringifies keys for
    JSON compatibility.

CRUD + transport + API + UI land in subsequent commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 18:00:11 +02:00
d717a6e214 refactor(v2): canonical sat-amount vocabulary + delete Lamassu-era reverse-derivation
Cross-codebase decision logged at memory `reference_sat_amount_vocabulary.md`
and at `~/dev/coordination/log.md` (2026-05-26). Canonical names with
explicit units across satmachineadmin, lamassu-next, atm-tui:

  - `wire_sats` — actual Lightning payment amount (direction-agnostic;
                  was `gross_sats`, only "gross" for cash-out)
  - `principal_sats` — market-rate sats before commission (unchanged)
  - `fee_sats` — commission (was `commission_sats` internally;
                 already the wire format)
  - `fee_fraction` — commission rate as unit fraction in [0, 1]
                     (was `*_pct` / `fee_percent`; eliminates the
                     latent 100x bug from `feePercent * 100` on the
                     lamassu-next side)

Invariants enforced in bitspire._assert_sat_invariants on every
parsed settlement — range (all sats >= 0, 0 <= fee_fraction <= 1) +
direction-specific sum:

  - cash-out: wire_sats == principal_sats + fee_sats
  - cash-in:  wire_sats == principal_sats - fee_sats
              AND fee_sats <= principal_sats

Breaches raise SettlementInvariantError; tasks._handle_payment
records the row as `status='rejected'` with the exception message
and skips distribution. Attribution failure path symmetric.

Schema changes (m001 + m006):

  - dca_settlements.gross_sats              -> wire_sats
  - dca_settlements.commission_sats         -> fee_sats
  - super_config.super_fee_pct              -> super_fee_fraction
  - dca_commission_splits.pct               -> fraction
  - dca_machines.fallback_commission_pct    DROPPED (obsolete)
  - dca_settlements.used_fallback_split     DROPPED (obsolete)

m006 idempotently renames + drops columns on existing installs;
m001 lays down the canonical schema for fresh installs.

Obsolete code removed (Lamassu-era reverse-derivation):

  - calculations.calculate_commission — back-derived principal+fee
    from gross-with-commission-baked-in. v2 stamps both directly.
  - calculations.calculate_exchange_rate — bitSpire stamps directly.
  - bitspire._parse_fallback — sole caller of calculate_commission.
  - Machine.fallback_commission_fraction — only read by _parse_fallback.
  - DcaSettlement.used_fallback_split — only written by _parse_fallback.

parse_settlement now raises SettlementMetadataError if Payment.extra
lacks the bitSpire stamp or required absolute sat fields. No silent
back-derivation; upstream-bug surfacing via dashboard rejection.

Frontend (JS + Quasar templates) updated for the column renames and
the removed fallback fields. Settlements table renders "Wire" + "Fee"
columns; the "(fallback split)" warning badge is gone.

Tests:
  - test_calculations.py: kept distribution tests; deleted
    calculate_commission + calculate_exchange_rate tests.
  - test_two_stage_split.py: renamed variables; rewrote docstring
    value literals (e.g. `super_fee_fraction=0.30` not `=30%`).
  - test_nostr_attribution.py: dropped fallback_commission_fraction
    from machine fixture.
  - 72/72 pass on regtest container.

Cross-codebase follow-ups tracked in coordination log:
  - lamassu-next: rename `fee_percent` -> `fee_fraction` on
    Payment.extra + state.db; drop the `* 100` at lightning.ts:780.
  - atm-tui: read `fee_fraction` column in db.zig.

Memory artefacts:
  - reference_sat_amount_vocabulary.md (canonical + invariants)
  - feedback_pct_to_fraction_renames_need_value_sweep.md (gotcha)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 20:08:30 +02:00
d2e682712d feat(v2): lock deposit currency to machine.fiat_code (closes #26)
Each machine handles exactly one currency today (operator-set on
`dca_machines.fiat_code`). The deposit's currency is fully determined
by the machine it's recorded against, so it shouldn't be operator-
chooseable in the first place.

Surfaced during 2026-05-16 E2E testing: Jordan had a "15 USD" deposit
recorded against an EUR Sintra (operator typo in the freeform currency
input). The balance summary is currency-blind (`SUM(amount)` over
mixed currencies), so on the next cash-out the system distributed
15 EUR worth of sats on the strength of that 15 USD row. Worked out
by chance; could have over-paid by ~10% if the actual EUR/USD rate
had been further off.

Fix:
  - `CreateDepositData` / `UpdateDepositData` no longer carry a
    `currency` field. Any client-submitted value is silently dropped
    at Pydantic validation, before reaching the handler.
  - `api_create_deposit` resolves the machine's `fiat_code` and
    passes it to `create_deposit(..., currency=...)` as a required
    keyword arg. The deposit row's `currency` column always matches
    the machine going forward.
  - UI: the freeform `<q-input label="Currency">` becomes a read-only
    `<q-chip>` slot on the amount field, sourced from the new
    `depositMachineFiatCode` computed (resolves via the selected
    client's machine).
  - `m005_lock_deposit_currency_to_machine_fiat_code` migration
    backfills existing rows: every `dca_deposits.currency` gets
    rewritten to match its joined `dca_machines.fiat_code`. Greg's
    stray `15 USD` row becomes `15 EUR` (the right answer at today's
    invariant).

Multi-currency-per-machine support is explicitly out of scope here;
when hardware ships that reads multiple denominations across
currencies, the relevant changes are documented in issue #26's
"Future" section (dca_machines.fiat_codes set, currency-aware
balance summary, etc.). The current fix is "lock the input side";
that future work is "unlock it but constrained to the machine's
declared set".

3 new unit tests (`tests/test_deposit_currency.py`) lock in the
model-contract guarantees. Total suite 89 passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 18:03:34 +02:00
cfad4e341c feat(v2)(ui): operator-side LP UI matches the new dca_lp authority split
Operator no longer chooses the LP's wallet / DCA mode / autoforward —
those belong to the LP, written via satmachineclient. The Add LP /
Edit LP dialogs reduce to (machine, user_id, optional username,
status). The clients table loses the wallet / mode / autoforward
columns and gains an "Onboarded" column showing whether the LP has a
`dca_lp` row yet (server-side LEFT JOIN; `DcaClient.lp_onboarded`).

Deposit creation gate (the structural enforcement of "must onboard
first"):
- Picker annotates each LP option with "— pending onboarding" and
  disables un-onboarded LP rows.
- Selecting an un-onboarded LP shows an inline deep-orange banner
  explaining the LP needs to open satmachineclient once.
- The Record button is `:disable`d in that state. The backend
  refuses with HTTP 422 anyway (see previous commit) — UI is just
  the first line of feedback.

Backend wiring:
- `DcaClient` model gains `lp_onboarded: bool = False`, populated
  at SELECT time via a shared `_CLIENT_SELECT` / `_CLIENT_FROM`
  fragment that LEFT JOINs `dca_lp` on `user_id`. All four list/
  single-row read paths use it: by-id, by-(machine,user), by-machine,
  by-operator, by-user. No extra round-trip per row.
- CSV export drops the removed columns; adds `lp_onboarded`.

All 86 unit tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 10:12:23 +02:00
80b5a6d785 refactor(v2): hoist LP state (wallet, mode, autoforward) into dca_lp table
LP-level preferences were denormalised across every `dca_clients` row
of a given user. Every LP enrolment carried its own wallet_id /
dca_mode / fixed_mode_daily_limit / autoforward_ln_address /
autoforward_enabled — and satmachineclient's `update_lp_autoforward`
did a multi-row UPDATE to keep them in sync. That sync dance was the
smell: user-level intent stored at machine-enrolment granularity.

New shape:

  dca_lp  (user_id PK, dca_wallet_id, default_dca_mode,
           fixed_mode_daily_limit, autoforward_ln_address,
           autoforward_enabled, ...)
  dca_clients  (id, machine_id, user_id, username, status, ...)
        // pure (machine, LP) enrolment — wallet/mode/autoforward gone

Authority split:
  - LP writes dca_lp via satmachineclient (Phase 2, separate commit).
  - Operator writes dca_clients via satmachineadmin. They cannot
    choose the LP's destination wallet — it's resolved from dca_lp
    at distribution time. Better trust hygiene.

Onboarding gate:
  - `api_create_deposit` refuses (HTTP 422) when the target LP has
    no dca_lp row. Forces every LP through a "yes, I am here and
    this is where I want my sats" gesture via satmachineclient
    before any fiat starts accumulating against them.

Schema:
  - m001 canonical schema updated: slim `dca_clients`, new `dca_lp`.
    Fresh installs land here directly.
  - m004 idempotent migration for installs that already have the
    legacy `dca_clients.wallet_id` column: creates dca_lp,
    backfills from the latest dca_clients row per user (window
    function), then DROP COLUMN on the moved fields. Greg's live
    test data survives the upgrade.

Distribution:
  - `get_flow_mode_clients_for_machine` INNER JOINs dca_lp so
    un-onboarded LPs are filtered out (no destination wallet).
  - `_pay_one_dca_leg`, `_attempt_autoforward`, `settle_lp_balance`
    all fetch `dca_lp` via the new `get_dca_lp(user_id)` helper.
    Wallet + autoforward read from prefs, not from client.

Models:
  - `DcaClient` loses 5 fields. `CreateDcaClientData` reduces to
    (machine_id, user_id, username). `UpdateDcaClientData` keeps
    only operator-controlled fields (username, status).
  - New `DcaLpPreferences` + `UpsertDcaLpData` models for the
    per-user surface (satmachineclient writes these in Phase 2).

CRUD:
  - New: `get_dca_lp`, `lp_is_onboarded`, `upsert_dca_lp` (the
    latter takes a `fallback_wallet_id` for first-onboarding when
    satmachineclient auto-seeds from the LP's default LNbits wallet).
  - `create_dca_client` insert reduces to the new column set.

Tests: 86 unit tests still green.

Next:
  - Phase 1c (this repo): UI simplification for operator's
    Add/Edit LP dialogs + deposit-gating UX.
  - Phase 2 (satmachineclient): own dca_lp writes + auto-init with
    the LP's default LNbits wallet on first dashboard visit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 10:05:54 +02:00
1feaba80ed refactor(v2): rename net_sats → principal_sats for semantic clarity
`net` is financial-accounting ambiguous (net of what?). In the
bitSpire/DCA context this column is specifically the principal the
operator distributes to LPs (gross − commission), not a generic net
amount. Renaming locally before any bitSpire firmware locks the
wire-level name; lamassu-next#44 should adopt the same name.

Scope:
- migrations.py: m003 ALTER TABLE … RENAME COLUMN, idempotent probe
  pattern matching m002. Also updates the m001 canonical schema so
  fresh installs land on the new column directly.
- models.py: `CreateDcaSettlementData.principal_sats` /
  `DcaSettlement.principal_sats`. Field-doc comment updated.
- bitspire.py: both happy path and fallback path return
  `principal_sats=…`. Reads `extra.get("principal_sats")` from the
  bitSpire payload (lamassu-next#44 should follow this rename).
- crud.py: INSERT column list + `apply_partial_dispense(
  new_principal_sats=…)` keyword.
- distribution.py: every `settlement.net_sats` → `settlement.
  principal_sats`; partial-dispense memo + helper signatures updated;
  the leg-order docblock at the top reads "principal_sats".
- tasks.py: landed-settlement log line.
- static/js/index.js: settlements-table column `principal_sats` with
  label "Principal (→ LPs)".
- templates/satmachineadmin/index.html: q-td key + binding.

All 86 unit tests still pass. No backwards-compat shim — v2-bitspire
isn't released; the rename is a clean break.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 23:21:32 +02:00
9414a18f82 feat(v2): reject settlements that fail nostr attribution cross-check (S5 G5)
When LNbits' nostr-transport stamps `nostr_sender_pubkey` and
`nostr_event_id` onto Payment.extra (post aiolabs/lnbits PR #4), the
listener now cross-checks the signer against the resolved machine's
`machine_npub` before any distribution. Mismatch / absence / unparseable
pubkey → settlement is recorded with `status='rejected'` and the
reason in `error_message`, distribution is skipped.

Wire shape:

  bitspire.SettlementAttributionError + assert_nostr_attribution()
    Raises on absence, mismatch, or unparseable pubkey on either side.
    Normalises both `machine.machine_npub` (operator UI accepts hex
    or `npub1...`) and the stamped sender through
    `lnbits.utils.nostr.normalize_public_key` so the comparison is
    canonical-hex on both sides.

  tasks._handle_payment
    parse_settlement -> stamp nostr_event_id onto bitspire_event_id ->
    try assert_nostr_attribution: on failure, insert row with
    initial_status='rejected' + error_message, return without
    spawning process_settlement.

  crud.create_settlement_idempotent
    Now takes `initial_status` (required) and `error_message`.
    Normal path passes 'pending'; rejected path passes 'rejected'
    with the reason. Single-statement insert — no two-step pending->
    errored dance.

  crud.get_stuck_settlements_for_operator
    New `rejected` bucket alongside `errored` / `stuck_pending` /
    `stuck_processing`. Distinct because retry is wrong for these:
    the row was misrouted, not operationally failed.

  models.DcaSettlement.status enum extended with 'rejected'.
    Worklist response model carries the new bucket; API + UI plumbed
    end-to-end.

  static/js/index.js + templates/satmachineadmin/index.html
    New 'rejected' worklist bucket (deep-orange, gpp_bad icon).
    Force-reset button now scoped to stuck_pending / stuck_processing
    only — was 'not errored' which would have shown on rejected too.

10 unit tests in tests/test_nostr_attribution.py cover hex<->hex,
hex<->bech32, case-insensitivity, every absent variant, mismatch,
and unparseable on either side. All pass.

Closes the consumer-side of aiolabs/satmachineadmin#19 (G5).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:39:30 +02:00
5de9cd5205 feat(v2): commission split target accepts wallet id, invoice key, LN address, or LNURL
Closes the operator-facing limitation that commission split legs could
only target the operator's own LNbits wallets. Adopting the splitpayments
pattern — a single `target` string accepts:
  - LNbits wallet id (UUID-shaped) — direct internal pay
  - LNbits wallet invoice key — resolved via get_wallet_for_key, then
    internal pay (lets the operator split to any LNbits user who shares
    their invoice key)
  - Lightning address (user@domain) — resolved via LNURL-pay
  - LNURL string (LNURL1...) — resolved via LNURL-pay

Schema (m001 update — fresh-install only; no operator data in production):
  dca_commission_splits.wallet_id → target

Backend (distribution.py):
  - New _pay_split_leg helper: routes the leg by target type. External
    targets (@ or LNURL prefix) go through get_pr_from_lnurl + pay_invoice;
    internal targets go through create_invoice + pay_invoice (the original
    path), with get_wallet_for_key as the first resolution step so
    invoice keys work as well as wallet ids.
  - _pay_operator_splits delegates per-leg payment to the new helper.
  - dca_payments rows still record the leg as leg_type='operator_split';
    external targets land destination_ln_address (the human-readable
    target), internal targets land destination_wallet_id.
  - Errors are caught and surfaced via the existing failed-leg path
    so /retry can re-run them.

Frontend (commission tab):
  - Each leg gets a per-row q-btn-toggle: "My wallet" vs "Lightning
    address / LNURL / invoice key". Wallet mode shows the q-select of
    the operator's own wallets (previous behaviour); external mode
    shows a free-text q-input.
  - On load, targetKind is inferred from whether the stored target
    matches one of the operator's wallet ids (renders as 'wallet')
    or not (renders as 'external'). The kind is UI-only, not persisted.
  - Leg row laid out in a bordered card so the toggle + 3-column layout
    don't crowd at narrow widths.

Refs: aiolabs/satmachineadmin#9

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:37:33 +02:00
ecef916dda fix(v2): decouple listener + skipped-leg audit (fix bundle 2)
Closes H4, H5, M8 from the v2-bitspire review (omnibus follow-up #11).

H4 — Decouple invoice listener from distribution.
  tasks._handle_payment now spawns process_settlement on a background
  task instead of awaiting it. The LNbits invoice queue is shared
  across every extension on the node; under load (a machine with 50
  LPs, a stalled internal payment, etc.) the previous synchronous path
  could freeze the queue for everyone. Concurrency is safe because
  fix bundle 1's claim_settlement_for_processing already prevents
  double-processing on listener re-fires.

  RUF006 fix: hold strong refs to in-flight tasks via a module-level
  set so the GC doesn't collect them mid-flight (asyncio.create_task
  only weakly references its task). Tasks self-clean via
  add_done_callback(set.discard).

H5 + M8 — Skipped-leg audit rows for stranded sats.
  Previously, four paths in distribution.py logged a warning and left
  sats in the machine wallet, marking the settlement 'processed' with
  no row-level visibility into where the un-paid sats sit:
    1. _pay_super_fee: super_fee_pct > 0 but super_fee_wallet_id unset
    2. _pay_operator_splits: no commission ruleset (default + override)
    3. _pay_dca_distributions: exchange_rate = 0 (fallback path)
    4. _pay_dca_distributions: no eligible LPs with positive balance
  Plus a fifth case the review didn't enumerate but is the same shape:
    5. _pay_dca_distributions: no flow-mode LPs at the machine at all

  Each now writes a dca_payments row with status='skipped', the
  intended leg_type (super_fee / operator_split / dca), the stranded
  amount in amount_sats, and a human-readable error_message explaining
  why. New _record_skipped_leg helper consolidates the pattern.

  This makes stranded sats visible in:
    - The machine detail dialog's settlements rows (the legs are
      filtered into the audit blob alongside completed/failed legs)
    - The payments CSV export
    - GET /api/v1/dca/payments?leg_type=...

  'skipped' is a documented leg-status value now (alongside pending /
  completed / failed / voided / refunded) — no schema change since
  status is TEXT.

Knock-on fix: void_open_legs_for_settlement (used by partial-dispense
recompute) now also includes status='skipped' in its WHERE clause so a
re-run doesn't double-count the audit rows from a prior attempt.

72/72 tests still pass. Lint clean.

Refs: aiolabs/satmachineadmin#11 — fix bundle 2 
Remaining in #11: H6 (partial-dispense split ratio) + fix bundle 3
(dead-code purge) + the M and N items.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 18:49:16 +02:00
578f2c142d feat(v2): abandoned-tx queue + force-reset for stuck settlements (P3f)
Completes the P3 operator-UX cluster. Surfaces settlements that didn't
process cleanly as a queryable worklist so operators can investigate +
retry without scanning the full settlement history.

New endpoints:

  GET    /api/v1/dca/settlements/stuck?threshold_minutes=30
    Returns StuckSettlementsResponse with three buckets:
      - errored: distribution failed; existing /retry endpoint handles
      - stuck_pending: landed but never picked up (listener crashed
        before invoking process_settlement)
      - stuck_processing: claim taken but no completion in N minutes;
        processor crashed mid-flight, processing_claim is set but no
        terminal state landed

  POST   /api/v1/dca/settlements/{id}/force-reset
    Operator escape hatch for genuinely stuck settlements. Flips
    'pending'/'processing' → 'errored' so the /retry endpoint can take
    over. Refuses unless the settlement is older than threshold_minutes
    (default 30) so operators can't accidentally interrupt a
    slow-but-running settlement. Age check uses created_at as proxy.

CRUD:
- get_stuck_settlements_for_operator(uid, threshold_minutes) joins
  dca_settlements → dca_machines and returns the three lists
  scoped per operator. No age filter on 'errored' (operators always
  want to see those); age filter applies to 'pending'/'processing'.
- force_reset_stuck_settlement(id) UPDATEs 'pending'/'processing' to
  'errored', clears processing_claim, sets a marker error_message.

The retry endpoint shipped in fix bundle 1 (commit 3ede66f) is the
intended downstream — operator sees stuck-processing row, hits force-
reset (flips to errored), then hits retry (flips to pending, voids
failed legs, re-runs process_settlement via the claim path).

34 routes registered. 72/72 tests pass.

Refs: aiolabs/satmachineadmin#9 — completes P3 operator-UX cluster

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:43:20 +02:00
3ede66ff92 fix(v2)(security): wallet IDOR + settlement-processing concurrency
Closes the HIGH-severity security finding from the v2 branch review:
operator A could register a machine pointing at operator B's wallet_id
(or update their machine to do so), then drain B's wallet via the
settlement processor's pay_invoice call. LNbits' pay_invoice doesn't
enforce caller identity at the backend layer — wallet_id is trusted as
the source-of-truth for the source wallet.

Two-layer defence:

1. **API layer.** New _assert_wallet_owned_by helper in views_api.py
   refuses any wallet_id from the request body that doesn't resolve to a
   wallet owned by the authenticated operator. Applied on
   api_create_machine and api_update_machine. Pattern lifted from the
   existing api_settle_client_balance which already did this for
   funding_wallet_id (260-265 in the original file).

2. **DB layer.** m007 adds a UNIQUE index on dca_machines.wallet_id —
   even if a future endpoint forgets the API check, the DB rejects two
   rows claiming the same wallet. CREATE UNIQUE INDEX is portable across
   SQLite and PostgreSQL (ALTER TABLE ADD CONSTRAINT is not on SQLite).

Same commit also addresses concurrency findings H1+H2+H3 from the
architectural review (race conditions on process_settlement +
no retry path for errored settlements):

- m007 also adds processing_claim TEXT to dca_settlements.
- crud.claim_settlement_for_processing does optimistic-lock via
  UPDATE ... SET status='processing', processing_claim=:token
  WHERE id=:id AND status='pending'  (portable; no UPDATE...RETURNING).
  Read-back compares the token; only one concurrent caller wins.
- crud.reset_settlement_for_retry voids failed legs and flips
  'errored' → 'pending' so process_settlement re-runs them. Completed
  legs are LEFT IN PLACE — we never re-pay sats that already moved.
- crud.mark_settlement_status clears processing_claim on terminal
  states so a fresh claim attempt won't see a stale token.
- distribution.process_settlement now uses the claim instead of the
  status-read-and-check pattern. Concurrent listener re-fires +
  partial-dispense recomputes can't double-pay legs.
- New endpoint:
    POST /api/v1/dca/settlements/{id}/retry  (operator-scoped)
  Refuses if status != 'errored' (400). Resets, then re-runs
  process_settlement via the claim path.

DcaSettlement gains a processing_claim: Optional[str] field. Visible to
operators in settlement detail; stale claims (status='processing' for
many minutes) are a "processor crashed mid-flight" signal — operator
can manually mark errored + retry.

32 routes registered. 72/72 tests pass.

Refs: aiolabs/satmachineadmin#9 — closes the v2-branch security finding
and HIGH-priority concurrency findings from the internal review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:37:58 +02:00
d0a947b7e6 feat(v2): balance settlement at current rate (P3e)
Closes the v1 feature request satmachineadmin#4 (balance settlement for
small remaining LP balances). Operator hits 'Settle' on an LP, specifies
the exchange rate they're willing to honor, and the system pays out the
remaining fiat balance in sats from the operator's chosen funding wallet.

Avoids the Zeno's-paradox of vanishing tiny proportional shares — small
balances no longer drag on forever; they get cleanly zeroed.

New endpoint:
  POST /api/v1/dca/clients/{client_id}/settle
  body: SettleBalanceData {funding_wallet_id, exchange_rate,
                            amount_fiat?, notes?}

Flow (distribution.settle_lp_balance):
  1. Get LP's remaining balance summary
  2. amount_fiat capped at remaining (defaults to full remaining)
  3. amount_sats = round(amount_fiat * exchange_rate)
  4. Internal transfer funding_wallet → client.wallet via
     create_invoice(internal=True) + pay_invoice
  5. Records leg_type='settlement' in dca_payments

Two ownership checks at the API boundary: client (via machine→operator)
and funding_wallet_id (via lnbits.core.crud.get_wallet → wallet.user
== current operator). 400 (not 404) if funding wallet isn't owned —
operators can identify their own wallets so leaking existence is fine.

Updated get_client_balance_summary to count both leg_type='dca' AND
leg_type='settlement' completed legs against the LP's remaining
balance. Without this update, settled amounts would leave the LP's
balance unchanged in the summary and re-fire on the next bitSpire tx.

Exchange rate is operator-supplied and required — explicit so there's
no ambiguity about what rate was used. Operator can use exchange spot,
market midpoint, or a favorable rate as a gesture; the rate is recorded
on the dca_payments row alongside amount_fiat for audit.

72/72 tests still pass. 31 routes total.

Refs: aiolabs/satmachineadmin#9, closes #4 (in spirit, marked once
verified end-to-end)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:17:41 +02:00
2883eb7b79 feat(v2): partial-dispense + operator notes on settlements (P3d)
Closes the v1 feature request satmachineadmin#3 (partial transaction
processing) and adds operator-authored audit notes on settlements.

Schema (m006_add_settlement_notes):
  ALTER TABLE dca_settlements ADD COLUMN notes TEXT

The notes column is append-only (prepend with timestamp, never edit in
place). Stores both system-generated audit memos (partial-dispense
recompute provenance) and operator-authored free-form notes (cash-
drawer reconciliation context, off-LN refund records, etc.).

Partial-dispense endpoint:
  POST /api/v1/dca/settlements/{id}/partial-dispense
  body: PartialDispenseData {dispensed_fraction OR dispensed_sats, notes}

Recompute path (in distribution.apply_partial_dispense_and_redistribute):
  1. Refuse if any leg has status='completed' (Lightning can't claw back)
  2. Resolve new_gross from dispensed_fraction or dispensed_sats
  3. Linear-scale net/commission/fiat — preserves the original commission
     ratio exactly; only rounding may drift by 1 sat
  4. Re-stage-1 split using the CURRENT super_fee_pct (super may have
     changed the rate since the original landed)
  5. Build a memo capturing original values + reason + new values
  6. Void pending/failed legs (status → 'voided')
  7. Overwrite the settlement's monetary fields + prepend memo to notes
  8. Reset status to 'pending' → process_settlement re-runs distribution

Operator notes endpoint:
  POST /api/v1/dca/settlements/{id}/notes
  body: AppendSettlementNoteData {note}

Each operator note is timestamped (UTC) and tagged with the author's
user_id so the audit trail is accountable. Non-empty, max 2000 chars.

72/72 tests still pass. 30 routes total. The full-directory ruff number
ballooned to ~500 because it includes legacy transaction_processor.py
(orphaned, not imported anywhere) and other v1 cruft on the branch.
Files I actively maintain are clean.

Note: a richer queryable audit history (filter by author / time range /
action type / etc.) is being tracked as a separate future-work issue.
The notes-column approach here is the v1 audit story; the dedicated
history table will be additive.

Refs: aiolabs/satmachineadmin#9, closes #3 (in spirit, marked
once verified end-to-end)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:46:33 +02:00
cba327d0f0 fix(v2): use payment_hash as settlement idempotency key
The initial m005 made bitspire_event_id the UNIQUE idempotency key on
dca_settlements, but settlements arriving through LNbits' invoice
listener (the canonical path per nostr-transport-branch architecture)
don't carry a Nostr event id at the Payment level — that's the
underlying transport's concern, not exposed to extensions.

The natural unique key is payment_hash:
- every LN invoice has a globally unique payment_hash
- subscription replays / dispatcher double-fires dedup via UNIQUE
- it's always present on the Payment object the invoice_listener delivers

Reshape the dca_settlements column constraints:
- payment_hash: TEXT NOT NULL UNIQUE (was: NOT NULL + separate index)
- bitspire_event_id: TEXT (was: NOT NULL UNIQUE) — kept nullable for
  a future path where we subscribe to raw kind-21000 Nostr events
  directly, bypassing the Payment system

Also rename the CRUD helper: get_settlement_by_event_id →
get_settlement_by_payment_hash, and update create_settlement_idempotent
to dedup on payment_hash. CreateDcaSettlementData / DcaSettlement
adjust accordingly.

The schema is unshipped (v2-bitspire branch is local only) — fixing
m005 in-place is appropriate. The separate dca_telemetry path for
kind-30078/30079 events already uses (machine_id, beacon_received_at)
semantics, so the UNIQUE-by-Nostr-event-id pattern isn't needed there
either.

Caught during P1a design before subscribing to register_invoice_listener.

Refs: aiolabs/satmachineadmin#9, aiolabs/lamassu-next#44

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:46:08 +02:00
013e3d5f6b feat(v2): rewrite models.py for v2 schema
Replaces the Lamassu-era data models (LamassuConfig, StoredLamassuTransaction,
single-config CRUD carriers) with the v2 Pydantic surface matching m005:

- Machine / CreateMachineData / UpdateMachineData: per-operator multi-machine
  registry keyed by Nostr npub. Replaces single-row LamassuConfig + SSH fields.
- DcaClient now scoped per (machine_id, user_id). Includes autoforward fields
  for satmachineadmin#8 (best-effort LN-address forwarding for LPs).
- DcaDeposit gains machine_id + creator_user_id (audit trail finding from v1).
- DcaSettlement: idempotency carrier for bitSpire kind-21000 events. Carries
  platform_fee_sats + operator_fee_sats as absolute ints (v1 hook for the v2
  customer-discount engine — see plan).
- CommissionSplitLeg / CommissionSplit / SetCommissionSplitsData: operator's
  remainder-distribution rules. SetCommissionSplitsData validates legs sum
  to 1.0 at the boundary so crud.py only sees valid sets.
- DcaPayment dropped transaction_type in favor of leg_type discriminator
  (dca | super_fee | operator_split | settlement | autoforward | refund).
  Gains settlement_id + machine_id + operator_user_id + destination_*.
- TelemetrySnapshot: sparse beacon + fleet snapshot fields, all nullable so
  we degrade gracefully against today's minimal kind-30078 payload.
- SuperConfig / UpdateSuperConfigData: super-only platform-fee carrier.
- PartialDispenseData (satmachineadmin#3), SettleBalanceData (satmachineadmin#4):
  operator UX action carriers.

Stays on pydantic v1 @validator pattern + Optional[X] hints to match the
rest of the codebase. The UP045 / N805 lint noise is pre-existing tech debt
across the repo, not introduced here.

Refs: plan at ~/.claude/plans/snug-gliding-shamir.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:33:16 +02:00
28241e70c3 feat: add deposit edit and delete for pending deposits
Add PUT /api/v1/dca/deposits/{id} endpoint to update amount, currency,
and notes on pending deposits. Add DELETE endpoint to remove deposits
not yet inserted into the machine. Both endpoints reject confirmed
deposits. Frontend now shows edit/delete buttons only for pending rows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-26 16:00:04 +02:00
c83ebf43ab 01 Refactor currency handling to store amounts in GTQ: Removed currency conversion utilities, updated models and API endpoints to directly handle GTQ amounts, and modified transaction processing logic for consistency. Enhanced frontend to reflect these changes, ensuring accurate display and submission of GTQ values across the application.
Refactor GTQ storage migration: Moved the conversion logic for centavo amounts to GTQ into a new migration function, m004_convert_to_gtq_storage, ensuring proper data type changes and updates across relevant tables. This enhances clarity and maintains the integrity of the migration process.
2025-07-06 00:13:03 +02:00
aa71321c84 00 Add currency conversion utilities and update models for GTQ handling: Introduced currency conversion functions for GTQ and centavos, updated API and database models to handle GTQ amounts directly, and modified API endpoints to streamline deposit and balance summary responses. Adjusted frontend to reflect changes in currency representation, ensuring consistency across the application. 2025-07-05 23:38:23 +02:00
e222922a6a Update fiat amount handling in models and transaction processor: Modified models to store fiat amounts in centavos for precision. Adjusted transaction processing logic to calculate and display fiat values in centavos, ensuring consistency across the application. Enhanced comments for clarity on the new storage format. 2025-07-05 16:57:04 +02:00
7af0e47d48 Add max_daily_limit_gtq field to lamassu_config: Updated database schema and models to include max_daily_limit_gtq for configurable client limits. Enhanced API and frontend to support this new field, ensuring proper handling and validation in the user interface. 2025-07-05 16:57:04 +02:00
3a2f10f949 Add transaction_time field to DCA payments: Updated the database schema to include a new transaction_time column in the dca_payments table, modified the CreateDcaPaymentData model to accept transaction_time, and adjusted the create_dca_payment function to store the original ATM transaction time. Updated transaction processing to capture this new field. 2025-06-22 16:50:27 +02:00
75dd03b15a Refactor MyExtension to DCA Admin: Update extension name and description in config.json, remove legacy MyExtension CRUD operations and related API endpoints, and adjust router tags. Clean up unused files and methods to streamline the codebase for DCA administration functionality.
Refactor DCA Admin page endpoints: Update description, remove unused CRUD operations and API endpoints related to MyExtension, and streamline the code for improved clarity and functionality.

Remove QR Code dialog from MyExtension index.html: streamline the UI by eliminating unused dialog components, enhancing code clarity and maintainability.
2025-06-20 22:00:41 +02:00
dc35cae44e Add CRUD operations for Lamassu transactions: implement create, retrieve, and update functions for storing processed Lamassu transactions in the database. Enhance migration script to create the necessary database table and update models to include transaction data structures. Integrate transaction storage into the LamassuTransactionProcessor for improved audit and UI display capabilities. 2025-06-20 00:49:46 +02:00
df8b36fc0f Add commission wallet support in Lamassu configuration: update database schema to include commission_wallet_id, modify related models and CRUD operations, and implement commission payment functionality in transaction processing. Enhance UI components to allow selection of the commission wallet for improved user experience. 2025-06-19 17:08:07 +02:00
1c1f358d82 Add source wallet ID support for DCA distributions: update Lamassu configuration to include source_wallet_id, modify related models and CRUD operations, and enhance transaction processing to utilize the configured source wallet for payments. Update UI components to allow selection of the source wallet. 2025-06-19 16:35:11 +02:00
b615ed0504 Add username field to DCA clients: update database schema, CRUD operations, and UI components to support username functionality for improved user experience. 2025-06-18 17:04:54 +02:00
a107f825af Add last poll tracking to Lamassu configuration: update database schema to include last_poll_time and last_successful_poll fields, modify CRUD operations to record poll times, and enhance transaction processing to utilize these timestamps for improved polling accuracy. 2025-06-18 15:56:55 +02:00
8f046ad0c5 Add SSH tunnel support to Lamassu configuration: update database schema, models, and UI components to include SSH settings. Implement SSH tunnel setup and teardown in transaction processing for enhanced security. 2025-06-18 13:40:30 +02:00
1f7999a556 Add Lamassu database configuration: implement CRUD operations, polling tasks, and UI components for managing database settings. Introduce hourly transaction polling and manual poll functionality. 2025-06-18 10:56:05 +02:00
22ebdc76bb Add DCA CRUD operations and models for clients, deposits, and payments 2025-06-17 18:29:23 +02:00
Arc
dd1dea90c1 quickfix 2024-11-19 00:57:27 +00:00
Arc
911aabd7d7 Working 2024-11-18 22:45:33 +00:00
Arc
08dad16611 Mostly working 2024-11-18 21:54:27 +00:00
Arc
5a2ba743cf Added lnurl helper, and I hate mypy 2024-11-18 20:49:24 +00:00
Arc
451e89682e Improved comment 2024-11-18 15:49:09 +00:00
Arc
5e988aa9f7 cleaned up api 2024-11-18 15:46:06 +00:00
Arc
5bdc334249 Merge branch 'v1' into feat_lnurl_v1 2024-11-18 13:44:41 +00:00
Arc
8b2b36a4f9 another v1 fixup + moved lnurl stuff to models 2024-11-15 00:06:28 +00:00
dni ⚡
48b1069a9f feat: update to v1.0.0 2024-10-21 14:38:03 +02:00
dni ⚡
42b5edaf5d feat: code quality
format

add ci

fixup ci
2024-08-13 08:20:45 +02:00
benarc
db47653ca6 Cleanup 2024-07-20 18:27:05 +01:00
benarc
c50aaf5d8c More notes and black 2024-02-01 17:47:17 +00:00
benarc
2cac36be17 format 2024-02-01 17:18:55 +00:00
benarc
39339469bb lnurlwithdraw 2024-01-23 09:14:57 +00:00