feat: merge a link's extra into the payout payment (v1.2.2-aio.2) #3
5 changed files with 66 additions and 2 deletions
feat: merge a link's extra into the payout payment (v1.2.2-aio.2)
Adds an optional `extra` (JSON) field to a withdraw link. When the link
is claimed, that `extra` is merged onto the payout payment's `extra`, so
a caller can tag the resulting payment with metadata an external listener
keys on — the link is the only place to attach it (the customer-facing
LNURL-withdraw payout otherwise carries just `{tag, withdrawal_link_id}`).
Motivating use: bitSpire cash-in settlements. The operator's spirekeeper
listener fires a `cash_in` settlement (fee split to the platform) only on
an outbound payment stamped `source=bitspire`; before this there was no
way to stamp an LNURL-withdraw payout, so cash-ins never settled. bitSpire
now creates the cash-in link for the NET amount with
`extra={source, type:cash_in, principal_sats, fee_sats, ...}` and the
settlement fires on claim.
- models: `extra: dict | None` on CreateWithdrawData + WithdrawLink.
LNbits' db layer (de)serializes dict columns to/from JSON natively
(same as Payment.extra) — no per-field validator needed.
- migrations_fork.py: `withdraw_link.extra TEXT` under `withdraw_fork`,
keeping the upstream-tracked migrations.py byte-identical for clean
rebases (aiolabs/lnbits#8 pattern).
- views_lnurl: `extra={**(link.extra or {}), "tag": ..., "withdrawal_link_id": ...}`
— the withdraw extension's own keys are written last so a caller cannot
clobber them.
Verified end-to-end on the dev stack: a stamped link's payout carries the
merged extra and drives a spirekeeper cash_in settlement + super-fee payout.
commit
9c0e58a87c
|
|
@ -2,7 +2,7 @@
|
|||
"name": "Withdraw Links",
|
||||
"short_description": "Make LNURL withdraw links",
|
||||
"tile": "/withdraw/static/image/lnurl-withdraw.png",
|
||||
"version": "1.2.2",
|
||||
"version": "1.2.2-aio.2",
|
||||
"min_lnbits_version": "1.3.0",
|
||||
"contributors": [
|
||||
{
|
||||
|
|
|
|||
1
crud.py
1
crud.py
|
|
@ -32,6 +32,7 @@ async def create_withdraw_link(
|
|||
webhook_headers=data.webhook_headers,
|
||||
webhook_body=data.webhook_body,
|
||||
custom_url=data.custom_url,
|
||||
extra=data.extra,
|
||||
number=0,
|
||||
)
|
||||
await db.insert("withdraw.withdraw_link", withdraw_link)
|
||||
|
|
|
|||
44
migrations_fork.py
Normal file
44
migrations_fork.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
"""
|
||||
Fork-specific database migrations for the aiolabs withdraw extension.
|
||||
|
||||
These migrations are tracked separately under `withdraw_fork` in the
|
||||
`dbversions` table (loaded by `lnbits/core/helpers.py:migrate_extension_database`),
|
||||
so they do not collide with upstream's `m{NNN}_*` numbering in
|
||||
`migrations.py`. Keeping the upstream-tracked file untouched means
|
||||
`git pull upstream` stays rebase-clean for schema changes.
|
||||
|
||||
Conventions:
|
||||
- Sequential numbering starting from m001.
|
||||
- Each migration is `async def m{NNN}_<description>(db)`.
|
||||
- DDL must be idempotent: a fresh install runs every migration; an
|
||||
install that already carries the column must not crash. Use
|
||||
`_alter_add_column_safe` so re-runs are no-ops.
|
||||
"""
|
||||
|
||||
|
||||
async def _alter_add_column_safe(db, sql: str) -> None:
|
||||
"""ALTER TABLE ADD COLUMN that swallows duplicate-column errors, so a
|
||||
re-run on a DB that already has the column is a silent no-op."""
|
||||
try:
|
||||
await db.execute(sql)
|
||||
except Exception as exc:
|
||||
msg = str(exc).lower()
|
||||
if "duplicate column" in msg or "already exists" in msg:
|
||||
return
|
||||
raise
|
||||
|
||||
|
||||
async def m001_aio_withdraw_schema(db):
|
||||
"""
|
||||
Apply every aiolabs schema delta on top of upstream withdraw.
|
||||
|
||||
`withdraw_link.extra` — arbitrary JSON merged into the payout payment's
|
||||
`extra` when the link is claimed (see views_lnurl). Lets a caller tag the
|
||||
resulting payment with settlement/attribution metadata an external listener
|
||||
can key on — e.g. bitSpire stamps {source, type, principal_sats, fee_sats,
|
||||
...} so the spirekeeper cash-in settlement fires off an LNURL-withdraw
|
||||
payout. Stored as TEXT; (de)serialized to a dict by the WithdrawLink model.
|
||||
"""
|
||||
await _alter_add_column_safe(
|
||||
db, "ALTER TABLE withdraw.withdraw_link ADD COLUMN extra TEXT"
|
||||
)
|
||||
10
models.py
10
models.py
|
|
@ -16,6 +16,12 @@ class CreateWithdrawData(BaseModel):
|
|||
webhook_body: str = Query(None)
|
||||
custom_url: str = Query(None)
|
||||
enabled: bool = Query(True)
|
||||
# Arbitrary JSON merged into the payout payment's `extra` when this link is
|
||||
# claimed (see views_lnurl). Lets a caller tag the resulting payment with
|
||||
# settlement/attribution metadata an external listener can key on — e.g.
|
||||
# bitSpire stamps {source, type, principal_sats, fee_sats, ...} so the
|
||||
# spirekeeper cash-in settlement fires off an LNURL-withdraw payout.
|
||||
extra: dict | None = None
|
||||
|
||||
|
||||
class WithdrawLink(BaseModel):
|
||||
|
|
@ -37,6 +43,10 @@ class WithdrawLink(BaseModel):
|
|||
webhook_headers: str = Query(None)
|
||||
webhook_body: str = Query(None)
|
||||
custom_url: str = Query(None)
|
||||
# Persisted as TEXT (JSON); merged into the payout payment's `extra` on
|
||||
# claim. LNbits' db layer (de)serializes dict-typed columns to/from JSON
|
||||
# natively (same as Payment.extra) — no per-field validator needed.
|
||||
extra: dict | None = None
|
||||
created_at: datetime
|
||||
enabled: bool = Query(True)
|
||||
lnurl: str | None = Field(
|
||||
|
|
|
|||
|
|
@ -141,7 +141,16 @@ async def api_lnurl_callback(
|
|||
wallet_id=link.wallet,
|
||||
payment_request=pr,
|
||||
max_sat=link.max_withdrawable,
|
||||
extra={"tag": "withdraw", "withdrawal_link_id": link.id},
|
||||
# Merge the link's caller-supplied `extra` onto the payout so an
|
||||
# external listener can key on it (e.g. bitSpire cash-in
|
||||
# settlements via spirekeeper). The withdraw extension's own
|
||||
# `tag`/`withdrawal_link_id` are written last so a caller cannot
|
||||
# clobber them.
|
||||
extra={
|
||||
**(link.extra or {}),
|
||||
"tag": "withdraw",
|
||||
"withdrawal_link_id": link.id,
|
||||
},
|
||||
)
|
||||
await increment_withdraw_link(link)
|
||||
# If the payment succeeds, delete the record with the unique_hash.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue