2022-07-31 18:01:02 +02:00
|
|
|
import logging
|
2024-02-24 15:30:32 +01:00
|
|
|
import time
|
2021-07-25 18:15:26 +02:00
|
|
|
from enum import Enum
|
2021-07-27 21:10:25 +02:00
|
|
|
from typing import List, Optional, Union
|
2021-07-25 18:15:26 +02:00
|
|
|
|
2021-08-02 20:30:18 +02:00
|
|
|
from deepdiff import DeepDiff
|
2021-09-05 19:19:53 +02:00
|
|
|
from fastapi.param_functions import Query
|
2023-05-20 14:13:24 +02:00
|
|
|
from loguru import logger
|
2026-07-03 22:22:05 +02:00
|
|
|
from pydantic import BaseModel, model_validator
|
2021-10-03 20:55:28 +02:00
|
|
|
from pydantic.types import conint
|
2021-07-21 19:38:49 +02:00
|
|
|
|
2022-10-03 20:22:00 +02:00
|
|
|
import app.lightning.docs as docs
|
2025-03-19 16:13:50 +01:00
|
|
|
from app.api.error_report.report import Report
|
2025-03-25 09:48:40 +01:00
|
|
|
from app.external.result_type.src.result import Err, Ok, Result
|
|
|
|
|
from app.lightning.impl.cln_utils import parse_cln_msat
|
2025-03-19 16:13:50 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class LnNodeType(str, Enum):
|
|
|
|
|
LND_GRPC = "lnd_grpc"
|
|
|
|
|
CLN_JRPC = "cln_jrpc"
|
|
|
|
|
CLN_GRPC = "cln_grpc"
|
|
|
|
|
NONE = "none"
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def values_as_list() -> List[str]:
|
|
|
|
|
return ["lnd_grpc", "cln_jrpc", "cln_grpc", "none"]
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def from_string(cls, value: str) -> Result["LnNodeType", Report]:
|
|
|
|
|
try:
|
|
|
|
|
return Ok(cls(value))
|
|
|
|
|
except ValueError:
|
|
|
|
|
return Err(
|
|
|
|
|
Report(f"Invalid node type {value}").attach(
|
|
|
|
|
LnNodeType.values_as_list(), "available_types"
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
2021-07-21 19:38:49 +02:00
|
|
|
|
refactor: improve startup procedure
During startup the API will try to connect to Bitcoin Core and the
Lightning Node. If it can't connect it will check every "n" seconds
(currently 2s) and connect when available. A new SSE event
called "system_startup_info" is introduced. This event contains
all startup status information during the startup procedure.
The old wallet_locked event is obsolete.
Sample:
--------------------------
event: system_startup_info
data: {"bitcoin": "offline", "bitcoin_msg": "", "lightning": "offline", "lightning_msg": "Unable to connect to LND daemon, waiting..."}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "offline", "lightning_msg": "Unable to connect to LND daemon, waiting..."}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "locked", "lightning_msg": "Wallet locked, unlock it to enable full RPC access"}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "done", "lightning_msg": ""}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "bootstraping", "lightning_msg": "RPC not yet available"}
--------------------------
refs #97
2022-06-06 19:29:21 +02:00
|
|
|
class LnInitState(str, Enum):
|
|
|
|
|
OFFLINE = "offline"
|
|
|
|
|
BOOTSTRAPPING = "bootstrapping"
|
2022-06-17 21:14:33 +02:00
|
|
|
BOOTSTRAPPING_AFTER_UNLOCK = "bootstrapping_after_unlock"
|
refactor: improve startup procedure
During startup the API will try to connect to Bitcoin Core and the
Lightning Node. If it can't connect it will check every "n" seconds
(currently 2s) and connect when available. A new SSE event
called "system_startup_info" is introduced. This event contains
all startup status information during the startup procedure.
The old wallet_locked event is obsolete.
Sample:
--------------------------
event: system_startup_info
data: {"bitcoin": "offline", "bitcoin_msg": "", "lightning": "offline", "lightning_msg": "Unable to connect to LND daemon, waiting..."}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "offline", "lightning_msg": "Unable to connect to LND daemon, waiting..."}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "locked", "lightning_msg": "Wallet locked, unlock it to enable full RPC access"}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "done", "lightning_msg": ""}
--------------------------
event: system_startup_info
data: {"bitcoin": "done", "bitcoin_msg": "", "lightning": "bootstraping", "lightning_msg": "RPC not yet available"}
--------------------------
refs #97
2022-06-06 19:29:21 +02:00
|
|
|
DONE = "done"
|
|
|
|
|
LOCKED = "locked"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class InitLnRepoUpdate:
|
|
|
|
|
state: LnInitState
|
|
|
|
|
msg: Optional[str]
|
|
|
|
|
|
|
|
|
|
def __init__(self, state: LnInitState = LnInitState.OFFLINE, msg: str = ""):
|
|
|
|
|
self.state = state
|
|
|
|
|
self.msg = msg
|
|
|
|
|
|
|
|
|
|
def dict(self) -> dict:
|
|
|
|
|
return {
|
|
|
|
|
"state": self.state,
|
|
|
|
|
"msg": self.msg,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2021-11-22 21:54:42 +01:00
|
|
|
class OnchainAddressType(str, Enum):
|
|
|
|
|
P2WKH = "p2wkh"
|
|
|
|
|
NP2WKH = "np2wkh"
|
|
|
|
|
|
|
|
|
|
|
2021-07-25 18:15:26 +02:00
|
|
|
class InvoiceState(str, Enum):
|
2021-10-31 18:15:43 +01:00
|
|
|
OPEN = "open"
|
|
|
|
|
SETTLED = "settled"
|
|
|
|
|
CANCELED = "canceled"
|
|
|
|
|
ACCEPTED = "accepted"
|
2021-07-25 18:15:26 +02:00
|
|
|
|
2021-10-31 18:15:43 +01:00
|
|
|
@classmethod
|
2022-06-04 14:10:44 +02:00
|
|
|
def from_lnd_grpc(cls, id) -> "InvoiceState":
|
2021-10-31 18:15:43 +01:00
|
|
|
if id == 0:
|
|
|
|
|
return InvoiceState.OPEN
|
|
|
|
|
elif id == 1:
|
|
|
|
|
return InvoiceState.SETTLED
|
|
|
|
|
elif id == 2:
|
|
|
|
|
return InvoiceState.CANCELED
|
|
|
|
|
elif id == 3:
|
|
|
|
|
return InvoiceState.ACCEPTED
|
|
|
|
|
else:
|
|
|
|
|
raise NotImplementedError(f"InvoiceState {id} is not implemented")
|
2021-07-25 18:15:26 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
@classmethod
|
|
|
|
|
def from_cln_json(cls, id) -> "InvoiceState":
|
|
|
|
|
if id == "unpaid":
|
|
|
|
|
return InvoiceState.OPEN
|
|
|
|
|
elif id == "paid":
|
|
|
|
|
return InvoiceState.SETTLED
|
|
|
|
|
elif id == "expired":
|
|
|
|
|
return InvoiceState.CANCELED
|
fix: handle missing fields in CLN invoice data gracefully
Fixes #129, #128, and addresses part of raspiblitz/raspiblitz#3182
BOLT12 offers, keysend payments, and certain CLN invoice types don't
always include all fields that the API expects, causing KeyError
exceptions that crash the web interface.
Changes:
- Modified Invoice.from_cln_json() to use .get() with safe defaults
for all potentially missing fields (bolt11, amount_msat, payment_hash,
description, label, status, etc.)
- Added fallback logic for amount_msat to use amount_received_msat
when the primary field is missing
- Enhanced InvoiceState.from_cln_json() to handle unknown/missing
statuses gracefully with logging instead of raising exceptions
This allows the web interface to display all CLN invoices including
BOLT12 payments from services like OCEAN mining pool, while preserving
all existing payment data for standard BOLT11 invoices.
Tested on RaspiBlitz v1.12.0 with Core Lightning and OCEAN mining
pool BOLT12 payouts.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-03 17:37:19 -06:00
|
|
|
elif id == "unknown" or id is None:
|
|
|
|
|
# Handle missing or unknown status gracefully
|
|
|
|
|
return InvoiceState.OPEN
|
2022-06-04 14:10:44 +02:00
|
|
|
else:
|
fix: handle missing fields in CLN invoice data gracefully
Fixes #129, #128, and addresses part of raspiblitz/raspiblitz#3182
BOLT12 offers, keysend payments, and certain CLN invoice types don't
always include all fields that the API expects, causing KeyError
exceptions that crash the web interface.
Changes:
- Modified Invoice.from_cln_json() to use .get() with safe defaults
for all potentially missing fields (bolt11, amount_msat, payment_hash,
description, label, status, etc.)
- Added fallback logic for amount_msat to use amount_received_msat
when the primary field is missing
- Enhanced InvoiceState.from_cln_json() to handle unknown/missing
statuses gracefully with logging instead of raising exceptions
This allows the web interface to display all CLN invoices including
BOLT12 payments from services like OCEAN mining pool, while preserving
all existing payment data for standard BOLT11 invoices.
Tested on RaspiBlitz v1.12.0 with Core Lightning and OCEAN mining
pool BOLT12 payouts.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-03 17:37:19 -06:00
|
|
|
# Log warning and default to OPEN instead of crashing
|
|
|
|
|
logger.warning(f"Unknown InvoiceState '{id}', defaulting to OPEN")
|
|
|
|
|
return InvoiceState.OPEN
|
2022-06-04 14:10:44 +02:00
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def from_cln_grpc(cls, i) -> "InvoiceState":
|
|
|
|
|
if i.status == 0:
|
|
|
|
|
return InvoiceState.OPEN
|
|
|
|
|
elif i.status == 1:
|
|
|
|
|
return InvoiceState.SETTLED
|
|
|
|
|
elif i.status == 2:
|
|
|
|
|
return InvoiceState.CANCELED
|
|
|
|
|
else:
|
|
|
|
|
raise NotImplementedError(f"InvoiceState {id} is not implemented")
|
|
|
|
|
|
2021-07-25 18:15:26 +02:00
|
|
|
|
|
|
|
|
class InvoiceHTLCState(str, Enum):
|
2021-10-31 18:15:43 +01:00
|
|
|
ACCEPTED = "accepted"
|
|
|
|
|
SETTLED = "settled"
|
|
|
|
|
CANCELED = "canceled"
|
2021-07-25 18:15:26 +02:00
|
|
|
|
2021-10-31 18:15:43 +01:00
|
|
|
@classmethod
|
2022-06-04 14:10:44 +02:00
|
|
|
def from_lnd_grpc(cls, id) -> "InvoiceHTLCState":
|
2021-10-31 18:15:43 +01:00
|
|
|
if id == 0:
|
|
|
|
|
return InvoiceHTLCState.ACCEPTED
|
|
|
|
|
elif id == 1:
|
|
|
|
|
return InvoiceHTLCState.SETTLED
|
|
|
|
|
elif id == 2:
|
|
|
|
|
return InvoiceHTLCState.CANCELED
|
|
|
|
|
else:
|
|
|
|
|
raise NotImplementedError(f"InvoiceHTLCState {id} is not implemented")
|
2021-07-25 18:15:26 +02:00
|
|
|
|
|
|
|
|
|
2022-01-09 20:20:21 +01:00
|
|
|
class FeeRevenue(BaseModel):
|
|
|
|
|
day: int = Query(..., description="Fee revenue earned in the last 24 hours")
|
|
|
|
|
week: int = Query(..., description="Fee revenue earned in the last 7days")
|
|
|
|
|
month: int = Query(..., description="Fee revenue earned in the last month")
|
|
|
|
|
year: int = Query(
|
|
|
|
|
None,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"Fee revenue earned in the last year."
|
|
|
|
|
"Might be null if not implemented by backend."
|
|
|
|
|
),
|
2022-01-09 20:20:21 +01:00
|
|
|
)
|
|
|
|
|
total: int = Query(
|
|
|
|
|
None,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"Fee revenue earned in the last year."
|
|
|
|
|
"Might be null if not implemented by backend"
|
|
|
|
|
),
|
2022-01-09 20:20:21 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2022-06-04 14:10:44 +02:00
|
|
|
def from_lnd_grpc(cls, fee_report) -> "FeeRevenue":
|
2022-01-09 20:20:21 +01:00
|
|
|
return cls(
|
|
|
|
|
day=int(fee_report.day_fee_sum),
|
|
|
|
|
week=int(fee_report.week_fee_sum),
|
|
|
|
|
month=int(fee_report.month_fee_sum),
|
|
|
|
|
)
|
|
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
@classmethod
|
|
|
|
|
def from_cln_json(cls, fee_report) -> "FeeRevenue":
|
|
|
|
|
return cls(
|
|
|
|
|
day=int(fee_report["day_fee_sum"]),
|
|
|
|
|
week=int(fee_report["week_fee_sum"]),
|
|
|
|
|
month=int(fee_report["month_fee_sum"]),
|
|
|
|
|
)
|
|
|
|
|
|
2022-01-09 20:20:21 +01:00
|
|
|
|
2022-01-15 14:27:08 +01:00
|
|
|
class ForwardSuccessEvent(BaseModel):
|
|
|
|
|
timestamp_ns: int = Query(
|
|
|
|
|
...,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"The number of nanoseconds elapsed since "
|
|
|
|
|
"January 1, 1970 UTC when this circuit was completed."
|
|
|
|
|
),
|
2022-01-15 14:27:08 +01:00
|
|
|
)
|
|
|
|
|
chan_id_in: str = Query(
|
|
|
|
|
...,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"The incoming channel ID that carried the HTLC that created the circuit."
|
|
|
|
|
),
|
2022-01-15 14:27:08 +01:00
|
|
|
)
|
|
|
|
|
chan_id_out: str = Query(
|
|
|
|
|
...,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"The outgoing channel ID that carried the "
|
|
|
|
|
"preimage that completed the circuit."
|
|
|
|
|
),
|
2022-01-15 14:27:08 +01:00
|
|
|
)
|
|
|
|
|
amt_in_msat: int = Query(
|
|
|
|
|
...,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"The total amount (in millisatoshis) of the "
|
|
|
|
|
"incoming HTLC that created half the circuit."
|
|
|
|
|
),
|
2022-01-15 14:27:08 +01:00
|
|
|
)
|
|
|
|
|
amt_out_msat: str = Query(
|
|
|
|
|
...,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"The total amount (in millisatoshis) of the "
|
|
|
|
|
"outgoing HTLC that created the second half of the circuit."
|
|
|
|
|
),
|
2022-01-15 14:27:08 +01:00
|
|
|
)
|
|
|
|
|
fee_msat: int = Query(
|
|
|
|
|
...,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"The total fee (in millisatoshis) that this payment circuit carried."
|
|
|
|
|
),
|
2022-01-15 14:27:08 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2022-06-04 14:10:44 +02:00
|
|
|
def from_lnd_grpc(cls, evt) -> "ForwardSuccessEvent":
|
2022-01-15 14:27:08 +01:00
|
|
|
return cls(
|
2023-04-02 12:58:04 +02:00
|
|
|
timestamp_ns=int(evt.timestamp),
|
2022-01-15 14:27:08 +01:00
|
|
|
chan_id_in=int(evt.chan_id_in),
|
|
|
|
|
chan_id_out=int(evt.chan_id_out),
|
|
|
|
|
amt_in_msat=int(evt.amt_in_msat),
|
|
|
|
|
amt_out_msat=int(evt.amt_out_msat),
|
|
|
|
|
fee_msat=int(evt.fee_msat),
|
|
|
|
|
)
|
|
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
@classmethod
|
|
|
|
|
def from_cln_json(cls, fwd) -> "ForwardSuccessEvent":
|
|
|
|
|
return cls(
|
|
|
|
|
timestamp_ns=fwd["resolved_time"],
|
|
|
|
|
chan_id_in=fwd["in_channel"],
|
|
|
|
|
chan_id_out=fwd["out_channel"],
|
|
|
|
|
amt_in_msat=fwd["in_msatoshi"],
|
|
|
|
|
amt_out_msat=fwd["out_msatoshi"],
|
|
|
|
|
fee_msat=fwd["fee"],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def from_cln_grpc(cls, fwd) -> "ForwardSuccessEvent":
|
|
|
|
|
return cls(
|
|
|
|
|
timestamp_ns=fwd.received_time,
|
|
|
|
|
chan_id_in=fwd.in_channel,
|
|
|
|
|
chan_id_out=fwd.out_channel,
|
|
|
|
|
amt_in_msat=fwd.in_msat.msat,
|
|
|
|
|
amt_out_msat=fwd.out_msat.msat,
|
|
|
|
|
fee_msat=fwd.fee_msat.msat,
|
|
|
|
|
)
|
|
|
|
|
|
2022-01-15 14:27:08 +01:00
|
|
|
|
2021-07-25 18:15:26 +02:00
|
|
|
class Feature(BaseModel):
|
|
|
|
|
name: str
|
2022-06-04 14:10:44 +02:00
|
|
|
is_required: Optional[bool]
|
|
|
|
|
is_known: Optional[bool]
|
2021-07-25 18:15:26 +02:00
|
|
|
|
2021-09-20 17:00:36 +02:00
|
|
|
@classmethod
|
2022-06-04 14:10:44 +02:00
|
|
|
def from_lnd_grpc(cls, f) -> "Feature":
|
2021-09-20 17:00:36 +02:00
|
|
|
return cls(
|
|
|
|
|
name=f.name,
|
|
|
|
|
is_required=f.is_required,
|
|
|
|
|
is_known=f.is_known,
|
|
|
|
|
)
|
2021-07-25 18:15:26 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
@classmethod
|
|
|
|
|
def from_cln_json(cls, f) -> "Feature":
|
|
|
|
|
return cls(name=f)
|
|
|
|
|
|
2021-07-25 18:15:26 +02:00
|
|
|
|
|
|
|
|
class FeaturesEntry(BaseModel):
|
|
|
|
|
key: int
|
|
|
|
|
value: Feature
|
|
|
|
|
|
2021-09-20 17:00:36 +02:00
|
|
|
@classmethod
|
2022-06-04 14:10:44 +02:00
|
|
|
def from_lnd_grpc(cls, entry_key, feature) -> "FeaturesEntry":
|
2021-09-20 17:00:36 +02:00
|
|
|
return cls(
|
|
|
|
|
key=entry_key,
|
2022-06-04 14:10:44 +02:00
|
|
|
value=Feature.from_lnd_grpc(feature),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def from_cln_json(self, entry_key, feature):
|
|
|
|
|
return self(
|
|
|
|
|
key=entry_key,
|
|
|
|
|
value=Feature.from_cln_json(feature),
|
2021-09-20 17:00:36 +02:00
|
|
|
)
|
2021-07-25 18:15:26 +02:00
|
|
|
|
|
|
|
|
|
2022-12-11 19:24:31 +01:00
|
|
|
class Amp(BaseModel):
|
2021-07-25 18:15:26 +02:00
|
|
|
# An n-of-n secret share of the root seed from
|
|
|
|
|
# which child payment hashes and preimages are derived.
|
|
|
|
|
root_share: str
|
|
|
|
|
|
|
|
|
|
# An identifier for the HTLC set that this HTLC belongs to.
|
|
|
|
|
set_id: str
|
|
|
|
|
|
|
|
|
|
# A nonce used to randomize the child preimage and
|
|
|
|
|
# child hash from a given root_share.
|
|
|
|
|
child_index: int
|
|
|
|
|
|
|
|
|
|
# The payment hash of the AMP HTLC.
|
|
|
|
|
hash: str
|
|
|
|
|
|
|
|
|
|
# The preimage used to settle this AMP htlc.
|
|
|
|
|
# This field will only be populated if the invoice
|
|
|
|
|
# is in InvoiceState_ACCEPTED or InvoiceState_SETTLED.
|
|
|
|
|
preimage: str
|
|
|
|
|
|
2021-10-31 18:15:43 +01:00
|
|
|
@classmethod
|
2022-12-11 19:24:31 +01:00
|
|
|
def from_lnd_grpc(cls, a) -> "Amp":
|
2021-10-31 18:15:43 +01:00
|
|
|
return cls(
|
|
|
|
|
root_share=a.root_share.hex(),
|
|
|
|
|
set_id=a.set_id.hex(),
|
|
|
|
|
child_index=a.child_index,
|
|
|
|
|
hash=a.hash.hex(),
|
|
|
|
|
preimage=a.preimage.hex(),
|
|
|
|
|
)
|
2021-07-25 18:15:26 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class CustomRecordsEntry(BaseModel):
|
2021-09-05 08:56:53 +02:00
|
|
|
key: int
|
|
|
|
|
value: str
|
2021-07-25 18:15:26 +02:00
|
|
|
|
2021-10-31 18:15:43 +01:00
|
|
|
@classmethod
|
2022-06-04 14:10:44 +02:00
|
|
|
def from_lnd_grpc(cls, e) -> "CustomRecordsEntry":
|
2021-10-31 18:15:43 +01:00
|
|
|
return cls(
|
|
|
|
|
key=e.key,
|
|
|
|
|
value=e.value,
|
|
|
|
|
)
|
2021-07-25 18:15:26 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class InvoiceHTLC(BaseModel):
|
2022-06-04 14:10:44 +02:00
|
|
|
chan_id: int = Query(
|
|
|
|
|
..., description="The channel ID over which the HTLC was received."
|
|
|
|
|
)
|
2021-07-25 18:15:26 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
htlc_index: int = Query(..., description="The index of the HTLC on the channel.")
|
2021-07-25 18:15:26 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
amt_msat: int = Query(..., description="The amount of the HTLC in msat.")
|
2021-07-25 18:15:26 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
accept_height: int = Query(
|
|
|
|
|
..., description="The block height at which this HTLC was accepted."
|
|
|
|
|
)
|
2021-07-25 18:15:26 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
accept_time: int = Query(
|
|
|
|
|
..., description="The time at which this HTLC was accepted."
|
|
|
|
|
)
|
2021-07-25 18:15:26 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
resolve_time: int = Query(
|
|
|
|
|
..., description="The time at which this HTLC was resolved."
|
|
|
|
|
)
|
2021-07-25 18:15:26 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
expiry_height: int = Query(
|
|
|
|
|
..., description="The block height at which this HTLC expires."
|
|
|
|
|
)
|
2021-07-25 18:15:26 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
state: InvoiceHTLCState = Query(..., description="The state of the HTLC.")
|
2021-07-25 18:15:26 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
custom_records: List[CustomRecordsEntry] = Query(
|
|
|
|
|
[], description="Custom tlv records."
|
|
|
|
|
)
|
2021-07-25 18:15:26 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
mpp_total_amt_msat: int = Query(
|
|
|
|
|
..., description="The total amount of the mpp payment in msat."
|
|
|
|
|
)
|
2021-07-25 18:15:26 +02:00
|
|
|
|
2022-12-11 19:24:31 +01:00
|
|
|
amp: Amp = Query(
|
2022-06-04 14:10:44 +02:00
|
|
|
None,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"Details relevant to AMP HTLCs, only populated if this is an AMP HTLC."
|
|
|
|
|
),
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
2021-07-25 18:15:26 +02:00
|
|
|
|
2021-10-31 18:15:43 +01:00
|
|
|
@classmethod
|
2022-06-04 14:10:44 +02:00
|
|
|
def from_lnd_grpc(cls, h) -> "InvoiceHTLC":
|
2021-10-31 18:15:43 +01:00
|
|
|
def _crecords(recs):
|
2023-05-17 16:02:28 +02:00
|
|
|
record_list = []
|
2021-10-31 18:15:43 +01:00
|
|
|
for r in recs:
|
2023-05-17 16:02:28 +02:00
|
|
|
record_list.append(CustomRecordsEntry.from_lnd_grpc(r))
|
|
|
|
|
return record_list
|
2021-07-25 18:15:26 +02:00
|
|
|
|
2021-10-31 18:15:43 +01:00
|
|
|
return cls(
|
|
|
|
|
chan_id=h.chan_id,
|
|
|
|
|
htlc_index=h.htlc_index,
|
|
|
|
|
amt_msat=h.amt_msat,
|
|
|
|
|
accept_height=h.accept_height,
|
|
|
|
|
accept_time=h.accept_time,
|
|
|
|
|
resolve_time=h.resolve_time,
|
|
|
|
|
expiry_height=h.expiry_height,
|
2022-06-04 14:10:44 +02:00
|
|
|
state=InvoiceHTLCState.from_lnd_grpc(h.state),
|
2021-10-31 18:15:43 +01:00
|
|
|
custom_records=_crecords(h.custom_records),
|
|
|
|
|
mpp_total_amt_msat=h.mpp_total_amt_msat,
|
2022-12-11 19:24:31 +01:00
|
|
|
amp=Amp.from_lnd_grpc(h.amp),
|
2021-10-31 18:15:43 +01:00
|
|
|
)
|
2021-07-25 18:15:26 +02:00
|
|
|
|
|
|
|
|
|
2021-12-04 12:13:23 +01:00
|
|
|
class HopHint(BaseModel):
|
2022-06-04 14:10:44 +02:00
|
|
|
node_id: str = Query(
|
|
|
|
|
..., description="The public key of the node at the start of the channel."
|
|
|
|
|
)
|
2021-07-25 18:15:26 +02:00
|
|
|
|
2022-07-04 18:45:45 +02:00
|
|
|
chan_id: str = Query(..., description="The unique identifier of the channel.")
|
2021-07-25 18:15:26 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
fee_base_msat: int = Query(
|
|
|
|
|
..., description="The base fee of the channel denominated in msat."
|
|
|
|
|
)
|
2021-07-25 18:15:26 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
fee_proportional_millionths: int = Query(
|
|
|
|
|
...,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"The fee rate of the channel for sending one"
|
|
|
|
|
"satoshi across it denominated in msat"
|
|
|
|
|
),
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
2021-07-25 18:15:26 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
cltv_expiry_delta: int = Query(
|
|
|
|
|
..., description="The time-lock delta of the channel."
|
|
|
|
|
)
|
2021-07-25 18:15:26 +02:00
|
|
|
|
2021-09-20 17:00:36 +02:00
|
|
|
@classmethod
|
2022-06-04 14:10:44 +02:00
|
|
|
def from_lnd_grpc(cls, h) -> "HopHint":
|
2021-09-20 17:00:36 +02:00
|
|
|
return cls(
|
|
|
|
|
node_id=h.node_id,
|
|
|
|
|
chan_id=h.chan_id,
|
|
|
|
|
fee_base_msat=h.fee_base_msat,
|
|
|
|
|
fee_proportional_millionths=h.fee_proportional_millionths,
|
|
|
|
|
cltv_expiry_delta=h.cltv_expiry_delta,
|
|
|
|
|
)
|
2021-07-25 18:15:26 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
@classmethod
|
|
|
|
|
def from_cln_json(cls, h) -> "HopHint":
|
|
|
|
|
return cls(
|
|
|
|
|
node_id=h["pubkey"],
|
|
|
|
|
chan_id=h["short_channel_id"],
|
|
|
|
|
fee_base_msat=h["fee_base_msat"],
|
|
|
|
|
fee_proportional_millionths=h["fee_proportional_millionths"],
|
|
|
|
|
cltv_expiry_delta=h["cltv_expiry_delta"],
|
|
|
|
|
)
|
|
|
|
|
|
2021-07-25 18:15:26 +02:00
|
|
|
|
2021-12-04 12:13:23 +01:00
|
|
|
class RouteHint(BaseModel):
|
|
|
|
|
hop_hints: List[HopHint] = Query(
|
|
|
|
|
[],
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"A list of hop hints that when chained together can assist in "
|
|
|
|
|
"reaching a specific destination."
|
|
|
|
|
),
|
2021-12-04 12:13:23 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2022-06-04 14:10:44 +02:00
|
|
|
def from_lnd_grpc(cls, h) -> "RouteHint":
|
|
|
|
|
hop_hints = [HopHint.from_lnd_grpc(hh) for hh in h.hop_hints]
|
|
|
|
|
return cls(hop_hints=hop_hints)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def from_cln_json(cls, h) -> "RouteHint":
|
2022-07-04 18:45:45 +02:00
|
|
|
hop_hints = [HopHint.from_cln_json(hop_hint) for hop_hint in h]
|
2021-12-04 12:13:23 +01:00
|
|
|
return cls(hop_hints=hop_hints)
|
|
|
|
|
|
|
|
|
|
|
2022-05-09 19:45:18 +02:00
|
|
|
class Channel(BaseModel):
|
|
|
|
|
channel_id: Optional[str]
|
|
|
|
|
active: Optional[bool]
|
|
|
|
|
|
|
|
|
|
peer_publickey: Optional[str]
|
|
|
|
|
peer_alias: Optional[str]
|
|
|
|
|
|
|
|
|
|
balance_local: Optional[int]
|
|
|
|
|
balance_remote: Optional[int]
|
|
|
|
|
balance_capacity: Optional[int]
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2022-06-04 14:10:44 +02:00
|
|
|
def from_lnd_grpc(cls, c) -> "Channel":
|
2022-05-09 19:45:18 +02:00
|
|
|
return cls(
|
|
|
|
|
active=c.active,
|
2023-05-17 16:02:28 +02:00
|
|
|
# use channel point as id because thats needed
|
|
|
|
|
# for closing the channel with lnd
|
|
|
|
|
channel_id=c.channel_point,
|
2022-05-09 19:45:18 +02:00
|
|
|
peer_publickey=c.remote_pubkey,
|
|
|
|
|
peer_alias="n/a",
|
|
|
|
|
balance_local=c.local_balance,
|
|
|
|
|
balance_remote=c.remote_balance,
|
2022-05-18 18:20:13 +02:00
|
|
|
balance_capacity=c.capacity,
|
2022-05-09 19:45:18 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2022-06-04 14:10:44 +02:00
|
|
|
def from_lnd_grpc_pending(cls, c) -> "Channel":
|
2022-05-09 19:45:18 +02:00
|
|
|
return cls(
|
|
|
|
|
active=False,
|
2023-05-17 16:02:28 +02:00
|
|
|
# use channel point as id because thats needed
|
|
|
|
|
# for closing the channel with lnd
|
|
|
|
|
channel_id=c.channel_point,
|
2022-05-09 19:45:18 +02:00
|
|
|
peer_publickey=c.remote_node_pub,
|
|
|
|
|
peer_alias="n/a",
|
|
|
|
|
balance_local=-1,
|
|
|
|
|
balance_remote=-1,
|
2022-05-18 18:20:13 +02:00
|
|
|
balance_capacity=c.capacity,
|
2022-05-09 19:45:18 +02:00
|
|
|
)
|
|
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
@classmethod
|
2022-08-03 20:43:25 +02:00
|
|
|
def from_cln_grpc(cls, c, peer_alias="n/a") -> "Channel":
|
2022-06-04 14:10:44 +02:00
|
|
|
# TODO: get alias and balance of the channel
|
|
|
|
|
return cls(
|
2022-08-03 20:43:25 +02:00
|
|
|
active=c.connected,
|
2023-05-17 16:02:28 +02:00
|
|
|
# use channel point as id because thats needed
|
|
|
|
|
# for closing the channel with lnd
|
|
|
|
|
channel_id=c.short_channel_id,
|
2022-08-03 20:43:25 +02:00
|
|
|
peer_publickey=c.peer_id.hex(),
|
|
|
|
|
peer_alias=peer_alias,
|
|
|
|
|
balance_local=c.our_amount_msat.msat,
|
|
|
|
|
balance_remote=c.amount_msat.msat - c.our_amount_msat.msat,
|
2022-06-04 14:10:44 +02:00
|
|
|
balance_capacity=c.amount_msat.msat,
|
|
|
|
|
)
|
|
|
|
|
|
2023-03-26 20:53:50 +02:00
|
|
|
@classmethod
|
|
|
|
|
def from_cln_jrpc(cls, c, peer_alias="n/a") -> "Channel":
|
|
|
|
|
# TODO: get alias and balance of the channel
|
|
|
|
|
return cls(
|
|
|
|
|
active=c["connected"],
|
2023-04-07 20:59:44 +02:00
|
|
|
channel_id=c["short_channel_id"] if "short_channel_id" in c else None,
|
2023-03-26 20:53:50 +02:00
|
|
|
peer_publickey=c["peer_id"],
|
|
|
|
|
peer_alias=peer_alias,
|
|
|
|
|
balance_local=parse_cln_msat(c["our_amount_msat"]),
|
|
|
|
|
balance_remote=parse_cln_msat(c["amount_msat"])
|
|
|
|
|
- parse_cln_msat(c["our_amount_msat"]),
|
|
|
|
|
balance_capacity=parse_cln_msat(c["amount_msat"]),
|
|
|
|
|
)
|
|
|
|
|
|
2022-05-18 18:20:13 +02:00
|
|
|
|
2021-07-21 19:38:49 +02:00
|
|
|
class Invoice(BaseModel):
|
2024-02-24 15:30:32 +01:00
|
|
|
memo: str | None = Query(
|
2022-06-04 14:10:44 +02:00
|
|
|
None,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"Optional memo to attach along with the invoice. "
|
|
|
|
|
"Used for record keeping purposes for the invoice's creator, "
|
|
|
|
|
"and will also be set in the description field of the encoded payment "
|
|
|
|
|
"request if the description_hash field is not being used."
|
|
|
|
|
),
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
2021-07-25 18:15:26 +02:00
|
|
|
|
2024-02-24 15:30:32 +01:00
|
|
|
r_preimage: str | None = Query(
|
2022-06-04 14:10:44 +02:00
|
|
|
None,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"The hex-encoded preimage(32 byte) which will allow settling "
|
|
|
|
|
"an incoming HTLC payable to this preimage."
|
|
|
|
|
),
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
2021-07-25 18:15:26 +02:00
|
|
|
|
2024-02-24 15:30:32 +01:00
|
|
|
r_hash: str | None = Query(None, description="The hash of the preimage.")
|
2022-06-04 14:10:44 +02:00
|
|
|
|
|
|
|
|
value_msat: int = Query(
|
|
|
|
|
..., description="The value of this invoice in milli satoshis."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
settled: bool = Query(False, description="Whether this invoice has been fulfilled")
|
|
|
|
|
|
2024-02-24 15:30:32 +01:00
|
|
|
creation_date: int | None = Query(
|
2022-06-04 14:10:44 +02:00
|
|
|
None,
|
|
|
|
|
description="When this invoice was created. Not available with CLN.",
|
|
|
|
|
)
|
|
|
|
|
|
2024-02-24 15:30:32 +01:00
|
|
|
settle_date: int | None = Query(
|
2022-06-04 14:10:44 +02:00
|
|
|
None,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
2025-03-25 09:48:40 +01:00
|
|
|
"When this invoice was settled. Not available with pending invoices."
|
2023-05-17 16:02:28 +02:00
|
|
|
),
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
|
|
|
|
|
2024-02-24 15:30:32 +01:00
|
|
|
expiry_date: int | None = Query(
|
|
|
|
|
None, description="The time at which this invoice expires"
|
|
|
|
|
)
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2024-02-24 15:30:32 +01:00
|
|
|
payment_request: str | None = Query(
|
2022-06-04 14:10:44 +02:00
|
|
|
None,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"A bare-bones invoice for a payment within the "
|
|
|
|
|
"Lightning Network. With the details of the invoice, the sender "
|
|
|
|
|
"has all the data necessary to send a payment to the recipient."
|
|
|
|
|
),
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
|
|
|
|
|
2024-02-24 15:30:32 +01:00
|
|
|
description_hash: str | None = Query(
|
2022-06-04 14:10:44 +02:00
|
|
|
None,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"Hash(SHA-256) of a description of the payment. Used if the description of "
|
|
|
|
|
"payment(memo) is too long to naturally fit within the description field "
|
|
|
|
|
"of an encoded payment request."
|
|
|
|
|
),
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
|
|
|
|
|
2024-02-24 15:30:32 +01:00
|
|
|
expiry: int | None = Query(
|
2022-06-04 14:10:44 +02:00
|
|
|
None,
|
|
|
|
|
description="Payment request expiry time in seconds. Default is 3600 (1 hour).",
|
|
|
|
|
)
|
|
|
|
|
|
2024-02-24 15:30:32 +01:00
|
|
|
fallback_addr: str | None = Query(None, description="Fallback on-chain address.")
|
2022-06-04 14:10:44 +02:00
|
|
|
|
2024-02-24 15:30:32 +01:00
|
|
|
cltv_expiry: int | None = Query(
|
2022-06-04 14:10:44 +02:00
|
|
|
None,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"Delta to use for the time-lock of the CLTV extended to the final hop."
|
|
|
|
|
),
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
2021-07-25 18:15:26 +02:00
|
|
|
|
2024-02-24 15:30:32 +01:00
|
|
|
route_hints: List[RouteHint] | None = Query(
|
2022-06-04 14:10:44 +02:00
|
|
|
None,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"Route hints that can each be individually used to assist "
|
|
|
|
|
"in reaching the invoice's destination."
|
|
|
|
|
),
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
|
|
|
|
|
2024-02-24 15:30:32 +01:00
|
|
|
private: bool | None = Query(
|
2022-06-04 14:10:44 +02:00
|
|
|
None,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"Whether this invoice should include routing hints for private channels."
|
|
|
|
|
),
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
add_index: str = Query(
|
|
|
|
|
...,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"The index of this invoice. Each newly created invoice will increment this "
|
|
|
|
|
"index making it monotonically increasing. CLN and LND handle ids "
|
|
|
|
|
"differently. LND will generate an auto incremented integer id, while CLN "
|
|
|
|
|
"will use a user supplied string id. To unify both, we auto generate an id "
|
|
|
|
|
"for CLN and use the add_index for LND."
|
|
|
|
|
""
|
|
|
|
|
"For `LND` this will be an `integer` in string form. This is auto "
|
|
|
|
|
"generated by LND. "
|
|
|
|
|
""
|
|
|
|
|
"For `CLN` this will be a `string`. If the invoice was generated by "
|
|
|
|
|
"BlitzAPI, this will be a [Firebase-like PushID]"
|
|
|
|
|
"(https://firebase.blog/posts/2015/02/the-2120-ways-to-ensure-unique_68). "
|
|
|
|
|
"If generated by some other method, it'll be the string supplied by the "
|
|
|
|
|
"user at the time of creation of the invoice."
|
|
|
|
|
),
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
|
|
|
|
|
2024-02-24 15:30:32 +01:00
|
|
|
settle_index: int | None = Query(
|
2022-06-04 14:10:44 +02:00
|
|
|
None,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"The `settle` index of this invoice. Each newly settled invoice will "
|
|
|
|
|
"increment this index making it monotonically increasing. "
|
|
|
|
|
),
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
|
|
|
|
|
2024-02-24 15:30:32 +01:00
|
|
|
amt_paid_sat: int | None = Query(
|
2022-06-04 14:10:44 +02:00
|
|
|
None,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"The amount that was accepted for this invoice, in satoshis. This "
|
|
|
|
|
"will ONLY be set if this invoice has been settled. We provide "
|
|
|
|
|
"this field as if the invoice was created with a zero value, "
|
|
|
|
|
"then we need to record what amount was ultimately accepted. "
|
|
|
|
|
"Additionally, it's possible that the sender paid MORE that "
|
|
|
|
|
"was specified in the original invoice. So we'll record that here as well."
|
|
|
|
|
),
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
|
|
|
|
|
2024-02-24 15:30:32 +01:00
|
|
|
amt_paid_msat: int | None = Query(
|
2022-06-04 14:10:44 +02:00
|
|
|
None,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"The amount that was accepted for this invoice, in millisatoshis. "
|
|
|
|
|
"This will ONLY be set if this invoice has been settled. We "
|
|
|
|
|
"provide this field as if the invoice was created with a zero value, "
|
|
|
|
|
"then we need to record what amount was ultimately accepted. Additionally, "
|
|
|
|
|
"it's possible that the sender paid MORE that was specified in the "
|
|
|
|
|
"original invoice. So we'll record that here as well."
|
|
|
|
|
),
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
state: InvoiceState = Query(..., description="The state the invoice is in.")
|
|
|
|
|
|
2024-02-24 15:30:32 +01:00
|
|
|
htlcs: List[InvoiceHTLC] | None = Query(
|
2022-06-04 14:10:44 +02:00
|
|
|
None, description="List of HTLCs paying to this invoice[EXPERIMENTAL]."
|
|
|
|
|
)
|
|
|
|
|
|
2024-02-24 15:30:32 +01:00
|
|
|
features: List[FeaturesEntry] | None = Query(
|
2022-06-04 14:10:44 +02:00
|
|
|
None, description="List of features advertised on the invoice."
|
|
|
|
|
)
|
|
|
|
|
|
2024-02-24 15:30:32 +01:00
|
|
|
is_keysend: bool | None = Query(
|
2022-06-04 14:10:44 +02:00
|
|
|
None,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"[LND only] Indicates if this invoice was a spontaneous payment "
|
|
|
|
|
"that arrived via keysend[EXPERIMENTAL]."
|
|
|
|
|
),
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
|
|
|
|
|
2024-02-24 15:30:32 +01:00
|
|
|
payment_addr: str | None = Query(
|
2022-06-04 14:10:44 +02:00
|
|
|
None,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"The payment address of this invoice. This value will be used "
|
|
|
|
|
"in MPP payments, and also for newer invoices that always require the MPP "
|
|
|
|
|
"payload for added end-to-end security."
|
|
|
|
|
),
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
|
|
|
|
|
2024-02-24 15:30:32 +01:00
|
|
|
is_amp: bool | None = Query(
|
2022-06-04 14:10:44 +02:00
|
|
|
None, description="Signals whether or not this is an AMP invoice."
|
|
|
|
|
)
|
2021-07-25 18:15:26 +02:00
|
|
|
|
2021-10-31 18:15:43 +01:00
|
|
|
@classmethod
|
2022-06-04 14:10:44 +02:00
|
|
|
def from_lnd_grpc(cls, i) -> "Invoice":
|
2021-10-31 18:15:43 +01:00
|
|
|
def _route_hints(hints):
|
2023-05-17 16:02:28 +02:00
|
|
|
return [RouteHint.from_lnd_grpc(h) for h in hints]
|
2021-10-31 18:15:43 +01:00
|
|
|
|
|
|
|
|
def _htlcs(htlcs):
|
2023-05-17 16:02:28 +02:00
|
|
|
return [InvoiceHTLC.from_lnd_grpc(h) for h in htlcs]
|
2021-10-31 18:15:43 +01:00
|
|
|
|
|
|
|
|
def _features(features):
|
2023-05-17 16:02:28 +02:00
|
|
|
return [FeaturesEntry.from_lnd_grpc(k, features[k]) for k in features]
|
2021-07-25 18:15:26 +02:00
|
|
|
|
2021-10-31 18:15:43 +01:00
|
|
|
return cls(
|
|
|
|
|
memo=i.memo,
|
|
|
|
|
r_preimage=i.r_preimage.hex(),
|
|
|
|
|
r_hash=i.r_hash.hex(),
|
|
|
|
|
value_msat=i.value_msat,
|
|
|
|
|
settled=i.settled,
|
|
|
|
|
creation_date=i.creation_date,
|
2022-06-04 14:10:44 +02:00
|
|
|
expiry_date=i.creation_date + i.expiry,
|
2021-10-31 18:15:43 +01:00
|
|
|
settle_date=i.settle_date,
|
|
|
|
|
payment_request=i.payment_request,
|
|
|
|
|
description_hash=i.description_hash,
|
|
|
|
|
expiry=i.expiry,
|
|
|
|
|
fallback_addr=i.fallback_addr,
|
|
|
|
|
cltv_expiry=i.cltv_expiry,
|
|
|
|
|
route_hints=_route_hints(i.route_hints),
|
|
|
|
|
private=i.private,
|
2024-02-24 15:30:32 +01:00
|
|
|
add_index=str(i.add_index),
|
2021-10-31 18:15:43 +01:00
|
|
|
settle_index=i.settle_index,
|
|
|
|
|
amt_paid_sat=i.amt_paid_sat,
|
|
|
|
|
amt_paid_msat=i.amt_paid_msat,
|
2022-06-04 14:10:44 +02:00
|
|
|
state=InvoiceState.from_lnd_grpc(i.state),
|
2021-10-31 18:15:43 +01:00
|
|
|
htlcs=_htlcs(i.htlcs),
|
|
|
|
|
features=_features(i.features),
|
|
|
|
|
is_keysend=i.is_keysend,
|
|
|
|
|
payment_addr=i.payment_addr.hex(),
|
|
|
|
|
is_amp=i.is_amp,
|
|
|
|
|
)
|
2021-07-27 21:10:25 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
@classmethod
|
|
|
|
|
def from_cln_json(cls, i) -> "Invoice":
|
fix: handle missing fields in CLN invoice data gracefully
Fixes #129, #128, and addresses part of raspiblitz/raspiblitz#3182
BOLT12 offers, keysend payments, and certain CLN invoice types don't
always include all fields that the API expects, causing KeyError
exceptions that crash the web interface.
Changes:
- Modified Invoice.from_cln_json() to use .get() with safe defaults
for all potentially missing fields (bolt11, amount_msat, payment_hash,
description, label, status, etc.)
- Added fallback logic for amount_msat to use amount_received_msat
when the primary field is missing
- Enhanced InvoiceState.from_cln_json() to handle unknown/missing
statuses gracefully with logging instead of raising exceptions
This allows the web interface to display all CLN invoices including
BOLT12 payments from services like OCEAN mining pool, while preserving
all existing payment data for standard BOLT11 invoices.
Tested on RaspiBlitz v1.12.0 with Core Lightning and OCEAN mining
pool BOLT12 payouts.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-03 17:37:19 -06:00
|
|
|
# Handle missing amount_msat field (e.g., in BOLT12 offers or certain invoice types)
|
|
|
|
|
# Use amount_received_msat if amount_msat is not present and invoice is paid
|
|
|
|
|
amt = 0
|
|
|
|
|
if "amount_msat" in i:
|
|
|
|
|
amt = parse_cln_msat(i["amount_msat"])
|
|
|
|
|
elif "amount_received_msat" in i:
|
|
|
|
|
amt = parse_cln_msat(i["amount_received_msat"])
|
|
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
return cls(
|
fix: handle missing fields in CLN invoice data gracefully
Fixes #129, #128, and addresses part of raspiblitz/raspiblitz#3182
BOLT12 offers, keysend payments, and certain CLN invoice types don't
always include all fields that the API expects, causing KeyError
exceptions that crash the web interface.
Changes:
- Modified Invoice.from_cln_json() to use .get() with safe defaults
for all potentially missing fields (bolt11, amount_msat, payment_hash,
description, label, status, etc.)
- Added fallback logic for amount_msat to use amount_received_msat
when the primary field is missing
- Enhanced InvoiceState.from_cln_json() to handle unknown/missing
statuses gracefully with logging instead of raising exceptions
This allows the web interface to display all CLN invoices including
BOLT12 payments from services like OCEAN mining pool, while preserving
all existing payment data for standard BOLT11 invoices.
Tested on RaspiBlitz v1.12.0 with Core Lightning and OCEAN mining
pool BOLT12 payouts.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-03 17:37:19 -06:00
|
|
|
add_index=str(i.get("label", "unknown")),
|
|
|
|
|
memo=i.get("description", ""),
|
|
|
|
|
r_preimage=i.get("payment_preimage"),
|
|
|
|
|
r_hash=i.get("payment_hash"),
|
2023-04-02 12:58:04 +02:00
|
|
|
value_msat=amt,
|
fix: handle missing fields in CLN invoice data gracefully
Fixes #129, #128, and addresses part of raspiblitz/raspiblitz#3182
BOLT12 offers, keysend payments, and certain CLN invoice types don't
always include all fields that the API expects, causing KeyError
exceptions that crash the web interface.
Changes:
- Modified Invoice.from_cln_json() to use .get() with safe defaults
for all potentially missing fields (bolt11, amount_msat, payment_hash,
description, label, status, etc.)
- Added fallback logic for amount_msat to use amount_received_msat
when the primary field is missing
- Enhanced InvoiceState.from_cln_json() to handle unknown/missing
statuses gracefully with logging instead of raising exceptions
This allows the web interface to display all CLN invoices including
BOLT12 payments from services like OCEAN mining pool, while preserving
all existing payment data for standard BOLT11 invoices.
Tested on RaspiBlitz v1.12.0 with Core Lightning and OCEAN mining
pool BOLT12 payouts.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-03 17:37:19 -06:00
|
|
|
settled=True if i.get("status") == "paid" else False,
|
|
|
|
|
expiry_date=i.get("expires_at"),
|
|
|
|
|
settle_date=i.get("paid_at"),
|
|
|
|
|
# bolt11 field is not present in BOLT12 offers or keysend payments
|
|
|
|
|
payment_request=i.get("bolt11"),
|
|
|
|
|
settle_index=i.get("pay_index"),
|
2024-02-18 12:11:15 +01:00
|
|
|
amt_paid_sat=(
|
2024-02-24 15:30:32 +01:00
|
|
|
round(parse_cln_msat(i["amount_received_msat"]) / 1000)
|
2024-02-18 12:11:15 +01:00
|
|
|
if "amount_received_msat" in i
|
|
|
|
|
else None
|
|
|
|
|
),
|
|
|
|
|
amt_paid_msat=(
|
|
|
|
|
parse_cln_msat(i["amount_received_msat"])
|
|
|
|
|
if "amount_received_msat" in i
|
|
|
|
|
else None
|
|
|
|
|
),
|
fix: handle missing fields in CLN invoice data gracefully
Fixes #129, #128, and addresses part of raspiblitz/raspiblitz#3182
BOLT12 offers, keysend payments, and certain CLN invoice types don't
always include all fields that the API expects, causing KeyError
exceptions that crash the web interface.
Changes:
- Modified Invoice.from_cln_json() to use .get() with safe defaults
for all potentially missing fields (bolt11, amount_msat, payment_hash,
description, label, status, etc.)
- Added fallback logic for amount_msat to use amount_received_msat
when the primary field is missing
- Enhanced InvoiceState.from_cln_json() to handle unknown/missing
statuses gracefully with logging instead of raising exceptions
This allows the web interface to display all CLN invoices including
BOLT12 payments from services like OCEAN mining pool, while preserving
all existing payment data for standard BOLT11 invoices.
Tested on RaspiBlitz v1.12.0 with Core Lightning and OCEAN mining
pool BOLT12 payouts.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-03 17:37:19 -06:00
|
|
|
state=InvoiceState.from_cln_json(i.get("status", "unknown")),
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def from_cln_grpc(cls, i) -> "Invoice":
|
|
|
|
|
state = InvoiceState.from_cln_grpc(i)
|
|
|
|
|
return cls(
|
|
|
|
|
add_index=i.label,
|
|
|
|
|
memo=i.description,
|
|
|
|
|
r_preimage=i.payment_preimage.hex(),
|
|
|
|
|
r_hash=i.payment_hash.hex(),
|
|
|
|
|
value_msat=i.amount_msat.msat,
|
|
|
|
|
settled=True if state == InvoiceState.SETTLED else False,
|
|
|
|
|
expiry_date=i.expires_at,
|
|
|
|
|
settle_date=i.paid_at,
|
|
|
|
|
payment_request=i.bolt11,
|
|
|
|
|
settle_index=i.pay_index,
|
|
|
|
|
amt_paid_sat=i.amount_received_msat.msat / 1000,
|
|
|
|
|
amt_paid_msat=i.amount_received_msat.msat,
|
|
|
|
|
state=state,
|
|
|
|
|
)
|
|
|
|
|
|
2021-07-27 21:10:25 +02:00
|
|
|
|
|
|
|
|
class PaymentStatus(str, Enum):
|
|
|
|
|
UNKNOWN = "unknown"
|
|
|
|
|
IN_FLIGHT = "in_flight"
|
|
|
|
|
SUCCEEDED = "succeeded"
|
|
|
|
|
FAILED = "failed"
|
|
|
|
|
|
2021-10-31 18:15:43 +01:00
|
|
|
@classmethod
|
2022-06-04 14:10:44 +02:00
|
|
|
def from_lnd_grpc(cls, id) -> "PaymentStatus":
|
2021-10-31 18:15:43 +01:00
|
|
|
if id == 0:
|
|
|
|
|
return PaymentStatus.UNKNOWN
|
|
|
|
|
elif id == 1:
|
|
|
|
|
return PaymentStatus.IN_FLIGHT
|
|
|
|
|
elif id == 2:
|
|
|
|
|
return PaymentStatus.SUCCEEDED
|
|
|
|
|
elif id == 3:
|
|
|
|
|
return PaymentStatus.FAILED
|
|
|
|
|
else:
|
|
|
|
|
raise NotImplementedError(f"PaymentStatus {id} is not implemented")
|
2021-07-27 21:10:25 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
@classmethod
|
|
|
|
|
def from_cln_grpc(cls, id) -> "PaymentStatus":
|
|
|
|
|
if id == 0:
|
2022-07-04 21:06:11 +02:00
|
|
|
return PaymentStatus.SUCCEEDED
|
2022-06-04 14:10:44 +02:00
|
|
|
elif id == 1:
|
2022-07-04 21:06:11 +02:00
|
|
|
return PaymentStatus.IN_FLIGHT
|
2022-06-04 14:10:44 +02:00
|
|
|
elif id == 2:
|
2022-07-04 21:06:11 +02:00
|
|
|
return PaymentStatus.FAILED
|
2022-06-04 14:10:44 +02:00
|
|
|
else:
|
|
|
|
|
raise NotImplementedError(f"PaymentStatus {id} is not implemented")
|
|
|
|
|
|
2023-03-26 20:53:50 +02:00
|
|
|
@classmethod
|
|
|
|
|
def from_cln_jrpc(cls, id) -> "PaymentStatus":
|
|
|
|
|
if id == "complete":
|
|
|
|
|
return PaymentStatus.SUCCEEDED
|
|
|
|
|
elif id == "pending":
|
|
|
|
|
return PaymentStatus.IN_FLIGHT
|
|
|
|
|
elif id == "failed":
|
|
|
|
|
return PaymentStatus.FAILED
|
|
|
|
|
else:
|
|
|
|
|
raise NotImplementedError(f"PaymentStatus {id} is not implemented")
|
|
|
|
|
|
2021-07-27 21:10:25 +02:00
|
|
|
|
|
|
|
|
class PaymentFailureReason(str, Enum):
|
|
|
|
|
# Payment isn't failed(yet).
|
|
|
|
|
FAILURE_REASON_NONE = "FAILURE_REASON_NONE"
|
|
|
|
|
|
|
|
|
|
# There are more routes to try, but the payment timeout was exceeded.
|
|
|
|
|
FAILURE_REASON_TIMEOUT = "FAILURE_REASON_TIMEOUT"
|
|
|
|
|
|
|
|
|
|
# All possible routes were tried and failed permanently.
|
|
|
|
|
# Or were no routes to the destination at all.
|
|
|
|
|
FAILURE_REASON_NO_ROUTE = "FAILURE_REASON_NO_ROUTE"
|
|
|
|
|
|
|
|
|
|
# A non-recoverable error has occurred.
|
|
|
|
|
FAILURE_REASON_ERROR = "FAILURE_REASON_ERROR"
|
|
|
|
|
|
|
|
|
|
# Payment details incorrect(unknown hash, invalid amt or invalid final cltv delta)
|
2021-09-05 08:56:53 +02:00
|
|
|
FAILURE_REASON_INCORRECT_PAYMENT_DETAILS = (
|
|
|
|
|
"FAILURE_REASON_INCORRECT_PAYMENT_DETAILS"
|
|
|
|
|
)
|
2021-07-27 21:10:25 +02:00
|
|
|
|
|
|
|
|
# Insufficient local balance.
|
|
|
|
|
FAILURE_REASON_INSUFFICIENT_BALANCE = "FAILURE_REASON_INSUFFICIENT_BALANCE"
|
|
|
|
|
|
2021-10-31 18:15:43 +01:00
|
|
|
@classmethod
|
2022-06-04 14:10:44 +02:00
|
|
|
def from_lnd_grpc(cls, f) -> "PaymentFailureReason":
|
2021-10-31 18:15:43 +01:00
|
|
|
if f == 0:
|
|
|
|
|
return PaymentFailureReason.FAILURE_REASON_NONE
|
|
|
|
|
elif f == 1:
|
|
|
|
|
return PaymentFailureReason.FAILURE_REASON_TIMEOUT
|
|
|
|
|
elif f == 2:
|
|
|
|
|
return PaymentFailureReason.FAILURE_REASON_NO_ROUTE
|
|
|
|
|
elif f == 3:
|
|
|
|
|
return PaymentFailureReason.FAILURE_REASON_ERROR
|
|
|
|
|
elif f == 4:
|
|
|
|
|
return PaymentFailureReason.FAILURE_REASON_INCORRECT_PAYMENT_DETAILS
|
|
|
|
|
elif f == 5:
|
|
|
|
|
return PaymentFailureReason.FAILURE_REASON_INSUFFICIENT_BALANCE
|
|
|
|
|
else:
|
|
|
|
|
raise NotImplementedError(f"PaymentFailureReason {id} is not implemented")
|
2021-07-27 21:10:25 +02:00
|
|
|
|
2023-03-26 20:53:50 +02:00
|
|
|
# TODO: find a way to describe the failure reason. CLN currently doesn't
|
|
|
|
|
# seem to provide an API for this.
|
2022-06-04 14:10:44 +02:00
|
|
|
@classmethod
|
|
|
|
|
def from_cln_grpc(cls, p) -> "PaymentFailureReason":
|
|
|
|
|
if p.status == 0 or p.status == 2:
|
|
|
|
|
return PaymentFailureReason.FAILURE_REASON_NONE
|
|
|
|
|
|
2023-03-26 20:53:50 +02:00
|
|
|
return PaymentFailureReason.FAILURE_REASON_ERROR
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def from_cln_jrpc(cls, p) -> "PaymentFailureReason":
|
|
|
|
|
if p["status"] == "complete" or p["status"] == "pending":
|
|
|
|
|
return PaymentFailureReason.FAILURE_REASON_NONE
|
2022-06-04 14:10:44 +02:00
|
|
|
|
|
|
|
|
return PaymentFailureReason.FAILURE_REASON_ERROR
|
|
|
|
|
|
2021-07-27 21:10:25 +02:00
|
|
|
|
|
|
|
|
class ChannelUpdate(BaseModel):
|
2023-05-17 16:02:28 +02:00
|
|
|
# The signature that validates the announced data and proves the ownership
|
|
|
|
|
# of node id.
|
2021-07-27 21:10:25 +02:00
|
|
|
signature: str
|
|
|
|
|
|
|
|
|
|
# The target chain that this channel was opened within. This value should be the
|
|
|
|
|
# genesis hash of the target chain. Along with the short channel ID, this uniquely
|
|
|
|
|
# identifies the channel globally in a blockchain.
|
|
|
|
|
chain_hash: str
|
|
|
|
|
|
|
|
|
|
# The unique description of the funding transaction.
|
|
|
|
|
chan_id: int
|
|
|
|
|
|
|
|
|
|
# A timestamp that allows ordering in the case of
|
|
|
|
|
# multiple announcements. We should ignore the message if
|
|
|
|
|
# timestamp is not greater than the last-received.
|
|
|
|
|
timestamp: int
|
|
|
|
|
|
|
|
|
|
# The bitfield that describes whether optional fields are present in this update.
|
2023-05-17 16:02:28 +02:00
|
|
|
# Currently, the least-significant bit must be set to 1 if the optional
|
|
|
|
|
# field MaxHtlc is present.
|
2021-07-27 21:10:25 +02:00
|
|
|
message_flags: int
|
|
|
|
|
|
2023-05-17 16:02:28 +02:00
|
|
|
# The bitfield that describes additional meta-data concerning how the update is to
|
|
|
|
|
# be interpreted. Currently, the least-significant bit must be set to 0 if the
|
|
|
|
|
# creating node corresponds to the first node in the previously sent channel
|
|
|
|
|
# announcement and 1 otherwise. If the second bit is set, then the channel is set
|
|
|
|
|
# to be disabled.
|
2021-07-27 21:10:25 +02:00
|
|
|
channel_flags: int
|
|
|
|
|
|
2023-05-17 16:02:28 +02:00
|
|
|
# The minimum number of blocks this node requires to be added to the expiry
|
|
|
|
|
# of HTLCs. This is a security parameter determined by the node operator.
|
|
|
|
|
# This value represents the required gap between the time locks of the
|
|
|
|
|
# incoming and outgoing HTLC's set to this node.
|
2021-07-27 21:10:25 +02:00
|
|
|
time_lock_delta: int
|
|
|
|
|
|
|
|
|
|
# The minimum HTLC value which will be accepted.
|
|
|
|
|
htlc_minimum_msat: int
|
|
|
|
|
|
|
|
|
|
# The base fee that must be used for incoming HTLC's to this particular channel.
|
2023-05-17 16:02:28 +02:00
|
|
|
# This value will be tacked onto the required for a payment independent of the
|
|
|
|
|
# size of the payment.
|
2021-07-27 21:10:25 +02:00
|
|
|
base_fee: int
|
|
|
|
|
|
|
|
|
|
# The fee rate that will be charged per millionth of a satoshi.
|
|
|
|
|
fee_rate: int
|
|
|
|
|
|
|
|
|
|
# The maximum HTLC value which will be accepted.
|
|
|
|
|
htlc_maximum_msat: int
|
|
|
|
|
|
2023-05-17 16:02:28 +02:00
|
|
|
# The set of data that was appended to this message, some of which we may not
|
|
|
|
|
# actually know how to iterate or parse. By holding onto this data, we ensure that
|
|
|
|
|
# we're able to properly validate the set of signatures that cover these new fields,
|
|
|
|
|
# and ensure we're able to make upgrades to the network in a forwards compatible
|
|
|
|
|
# manner.
|
2021-07-27 21:10:25 +02:00
|
|
|
extra_opaque_data: str
|
|
|
|
|
|
2021-10-31 18:15:43 +01:00
|
|
|
@classmethod
|
2022-06-04 14:10:44 +02:00
|
|
|
def from_lnd_grpc(cls, u) -> "ChannelUpdate":
|
2021-10-31 18:15:43 +01:00
|
|
|
return cls(
|
|
|
|
|
signature=u.signature,
|
|
|
|
|
chain_hash=u.chain_hash,
|
|
|
|
|
chan_id=u.chan_id,
|
|
|
|
|
timestamp=u.timestamp,
|
|
|
|
|
message_flags=u.message_flags,
|
|
|
|
|
channel_flags=u.channel_flags,
|
|
|
|
|
time_lock_delta=u.time_lock_delta,
|
|
|
|
|
htlc_minimum_msat=u.htlc_minimum_msat,
|
|
|
|
|
base_fee=u.base_fee,
|
|
|
|
|
fee_rate=u.fee_rate,
|
|
|
|
|
htlc_maximum_msat=u.htlc_maximum_msat,
|
|
|
|
|
extra_opaque_data=u.extra_opaque_data,
|
|
|
|
|
)
|
2021-07-27 21:10:25 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class Hop(BaseModel):
|
|
|
|
|
# The unique channel ID for the channel. The first 3
|
|
|
|
|
# bytes are the block height, the next 3 the index within the
|
|
|
|
|
# block, and the last 2 bytes are the output index for the channel.
|
|
|
|
|
chan_id: int
|
|
|
|
|
|
|
|
|
|
chan_capacity: int
|
|
|
|
|
amt_to_forward: int
|
|
|
|
|
fee: int
|
|
|
|
|
expiry: int
|
|
|
|
|
amt_to_forward_msat: int
|
|
|
|
|
fee_msat: int
|
|
|
|
|
|
|
|
|
|
# An optional public key of the hop. If the public key is given,
|
|
|
|
|
# the payment can be executed without relying on a copy of the channel graph.
|
|
|
|
|
pub_key: str
|
|
|
|
|
|
2023-05-17 16:02:28 +02:00
|
|
|
# If set to true, then this hop will be encoded using the new variable length TLV
|
|
|
|
|
# format. Note that if any custom tlv_records below are specified, then this field
|
|
|
|
|
# MUST be set to true for them to be encoded properly.
|
2021-07-27 21:10:25 +02:00
|
|
|
tlv_payload: bool
|
|
|
|
|
|
2021-10-31 18:15:43 +01:00
|
|
|
@classmethod
|
2022-06-04 14:10:44 +02:00
|
|
|
def from_lnd_grpc(cls, h) -> "Hop":
|
2021-10-31 18:15:43 +01:00
|
|
|
return cls(
|
|
|
|
|
chan_id=h.chan_id,
|
|
|
|
|
chan_capacity=h.chan_capacity,
|
|
|
|
|
amt_to_forward=h.amt_to_forward,
|
|
|
|
|
fee=h.fee,
|
|
|
|
|
expiry=h.expiry,
|
|
|
|
|
amt_to_forward_msat=h.amt_to_forward_msat,
|
|
|
|
|
fee_msat=h.fee_msat,
|
|
|
|
|
pub_key=h.pub_key,
|
|
|
|
|
tlv_payload=h.tlv_payload,
|
|
|
|
|
)
|
2021-07-27 21:10:25 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class MPPRecord(BaseModel):
|
|
|
|
|
payment_addr: str
|
|
|
|
|
total_amt_msat: int
|
|
|
|
|
|
2021-10-31 18:15:43 +01:00
|
|
|
@classmethod
|
2022-06-04 14:10:44 +02:00
|
|
|
def from_lnd_grpc(cls, r) -> "MPPRecord":
|
2021-10-31 18:15:43 +01:00
|
|
|
return cls(
|
|
|
|
|
payment_addr=r.payment_addr,
|
|
|
|
|
total_amt_msat=r.total_amt_msat,
|
|
|
|
|
)
|
2021-07-27 21:10:25 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class AMPRecord(BaseModel):
|
|
|
|
|
root_share: str
|
|
|
|
|
set_id: str
|
|
|
|
|
child_index: int
|
|
|
|
|
|
2021-10-31 18:15:43 +01:00
|
|
|
@classmethod
|
2022-06-04 14:10:44 +02:00
|
|
|
def from_lnd_grpc(cls, r) -> "AMPRecord":
|
2021-10-31 18:15:43 +01:00
|
|
|
return cls(
|
|
|
|
|
root_share=r.root_share,
|
|
|
|
|
set_id=r.set_id,
|
|
|
|
|
child_index=r.child_index,
|
|
|
|
|
)
|
2021-07-27 21:10:25 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class Route(BaseModel):
|
|
|
|
|
total_time_lock: int
|
|
|
|
|
total_fees: int
|
|
|
|
|
total_amt: int
|
|
|
|
|
hops: List[Hop]
|
|
|
|
|
total_fees_msat: int
|
|
|
|
|
total_amt_msat: int
|
|
|
|
|
mpp_record: Union[MPPRecord, None]
|
|
|
|
|
amp_record: Union[AMPRecord, None]
|
2021-09-05 08:56:53 +02:00
|
|
|
custom_records: List[CustomRecordsEntry]
|
2021-07-27 21:10:25 +02:00
|
|
|
|
2021-10-31 18:15:43 +01:00
|
|
|
@classmethod
|
2022-06-04 14:10:44 +02:00
|
|
|
def from_lnd_grpc(cls, r):
|
2021-10-31 18:15:43 +01:00
|
|
|
def _crecords(recs):
|
2023-05-17 16:02:28 +02:00
|
|
|
return [CustomRecordsEntry.from_lnd_grpc(r) for r in recs]
|
2021-10-31 18:15:43 +01:00
|
|
|
|
|
|
|
|
def _get_hops(hops) -> List[Hop]:
|
2023-05-17 16:02:28 +02:00
|
|
|
return [Hop.from_lnd_grpc(h) for h in hops]
|
2021-10-31 18:15:43 +01:00
|
|
|
|
|
|
|
|
mpp = None
|
|
|
|
|
if hasattr(r, "mpp_record"):
|
2022-06-04 14:10:44 +02:00
|
|
|
mpp = MPPRecord.from_lnd_grpc(r.mpp_record)
|
2021-10-31 18:15:43 +01:00
|
|
|
|
|
|
|
|
amp = None
|
|
|
|
|
if hasattr(r, "amp_record"):
|
2022-06-04 14:10:44 +02:00
|
|
|
amp = AMPRecord.from_lnd_grpc(r.amp_record)
|
2021-10-31 18:15:43 +01:00
|
|
|
|
|
|
|
|
crecords = []
|
|
|
|
|
if hasattr(r, "custom_records"):
|
|
|
|
|
crecords = _crecords(r.custom_records)
|
2021-07-27 21:10:25 +02:00
|
|
|
|
2021-10-31 18:15:43 +01:00
|
|
|
return cls(
|
|
|
|
|
total_time_lock=r.total_time_lock,
|
|
|
|
|
total_fees=r.total_fees,
|
|
|
|
|
total_amt=r.total_amt,
|
|
|
|
|
hops=_get_hops(r.hops),
|
|
|
|
|
total_fees_msat=r.total_fees_msat,
|
|
|
|
|
total_amt_msat=r.total_amt_msat,
|
|
|
|
|
mpp_record=mpp,
|
|
|
|
|
amp_record=amp,
|
|
|
|
|
custom_records=crecords,
|
|
|
|
|
)
|
2021-07-27 21:10:25 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class HTLCAttemptFailure(BaseModel):
|
|
|
|
|
# Failure code as defined in the Lightning spec
|
|
|
|
|
code: int
|
|
|
|
|
|
|
|
|
|
# An optional channel update message.
|
|
|
|
|
channel_update: ChannelUpdate
|
|
|
|
|
|
|
|
|
|
# A failure type-dependent htlc value.
|
|
|
|
|
htlc_msat: int
|
|
|
|
|
|
|
|
|
|
# The sha256 sum of the onion payload.
|
|
|
|
|
onion_sha_256: str
|
|
|
|
|
|
|
|
|
|
# A failure type-dependent cltv expiry value.
|
|
|
|
|
cltv_expiry: int
|
|
|
|
|
|
|
|
|
|
# A failure type-dependent flags value.
|
|
|
|
|
flags: int
|
|
|
|
|
|
|
|
|
|
# The position in the path of the intermediate
|
|
|
|
|
# or final node that generated the failure message.
|
|
|
|
|
# Position zero is the sender node.
|
|
|
|
|
failure_source_index: int
|
|
|
|
|
|
|
|
|
|
# A failure type-dependent block height.
|
|
|
|
|
height: int
|
|
|
|
|
|
2021-10-31 18:15:43 +01:00
|
|
|
@classmethod
|
2022-06-04 14:10:44 +02:00
|
|
|
def from_lnd_grpc(cls, f) -> "HTLCAttemptFailure":
|
2021-10-31 18:15:43 +01:00
|
|
|
code = None
|
|
|
|
|
if hasattr(f, "code"):
|
|
|
|
|
code = f.code
|
2021-07-27 21:10:25 +02:00
|
|
|
|
2021-10-31 18:15:43 +01:00
|
|
|
htlc_msat = None
|
|
|
|
|
if hasattr(f, "htlc_msat"):
|
|
|
|
|
htlc_msat = f.htlc_msat
|
|
|
|
|
|
|
|
|
|
return cls(
|
|
|
|
|
code=code,
|
2022-06-04 14:10:44 +02:00
|
|
|
channel_update=ChannelUpdate.from_lnd_grpc(f.channel_update),
|
2021-10-31 18:15:43 +01:00
|
|
|
htlc_msat=htlc_msat,
|
|
|
|
|
onion_sha_256=f.onion_sha_256,
|
|
|
|
|
cltv_expiry=f.cltv_expiry,
|
|
|
|
|
flags=f.flags,
|
|
|
|
|
failure_source_index=f.failure_source_index,
|
|
|
|
|
height=f.height,
|
|
|
|
|
)
|
2021-07-27 21:10:25 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class HTLCStatus(str, Enum):
|
2021-10-31 18:15:43 +01:00
|
|
|
IN_FLIGHT = "in_flight"
|
|
|
|
|
SUCCEEDED = "succeeded"
|
|
|
|
|
FAILED = "failed"
|
2021-07-27 21:10:25 +02:00
|
|
|
|
2021-10-31 18:15:43 +01:00
|
|
|
@classmethod
|
2022-06-04 14:10:44 +02:00
|
|
|
def from_lnd_grpc(cls, s) -> "HTLCStatus":
|
2021-10-31 18:15:43 +01:00
|
|
|
if s == 0:
|
|
|
|
|
return HTLCStatus.IN_FLIGHT
|
|
|
|
|
elif s == 1:
|
|
|
|
|
return HTLCStatus.SUCCEEDED
|
|
|
|
|
elif s == 2:
|
|
|
|
|
return HTLCStatus.FAILED
|
|
|
|
|
else:
|
|
|
|
|
raise NotImplementedError(f"HTLCStatus {id} is not implemented")
|
2021-07-27 21:10:25 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class HTLCAttempt(BaseModel):
|
|
|
|
|
# The unique ID that is used for this attempt.
|
|
|
|
|
attempt_id: int
|
|
|
|
|
|
|
|
|
|
# The status of the HTLC.
|
|
|
|
|
status: HTLCStatus
|
|
|
|
|
|
|
|
|
|
# The route taken by this HTLC.
|
|
|
|
|
route: Route
|
|
|
|
|
|
|
|
|
|
# The time in UNIX nanoseconds at which this HTLC was sent.
|
|
|
|
|
attempt_time_ns: int
|
|
|
|
|
|
|
|
|
|
# The time in UNIX nanoseconds at which this HTLC was settled
|
|
|
|
|
# or failed. This value will not be set if the HTLC is still IN_FLIGHT.
|
|
|
|
|
resolve_time_ns: int
|
|
|
|
|
|
|
|
|
|
# Detailed htlc failure info.
|
|
|
|
|
failure: HTLCAttemptFailure
|
|
|
|
|
|
|
|
|
|
# The preimage that was used to settle the HTLC.
|
|
|
|
|
preimage: str
|
|
|
|
|
|
2021-10-31 18:15:43 +01:00
|
|
|
@classmethod
|
2022-06-04 14:10:44 +02:00
|
|
|
def from_lnd_grpc(cls, a) -> "HTLCAttempt":
|
2021-10-31 18:15:43 +01:00
|
|
|
return cls(
|
|
|
|
|
attempt_id=a.attempt_id,
|
2022-06-04 14:10:44 +02:00
|
|
|
status=HTLCStatus.from_lnd_grpc(a.status),
|
|
|
|
|
route=Route.from_lnd_grpc(a.route),
|
2021-10-31 18:15:43 +01:00
|
|
|
attempt_time_ns=a.attempt_time_ns,
|
|
|
|
|
resolve_time_ns=a.resolve_time_ns,
|
2022-06-04 14:10:44 +02:00
|
|
|
failure=HTLCAttemptFailure.from_lnd_grpc(a.failure),
|
2021-10-31 18:15:43 +01:00
|
|
|
preimage=a.preimage.hex(),
|
|
|
|
|
)
|
2021-07-27 21:10:25 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class Payment(BaseModel):
|
2022-06-04 14:10:44 +02:00
|
|
|
payment_hash: str = Query(..., description="The payment hash")
|
2021-07-27 21:10:25 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
payment_preimage: Optional[str] = Query(None, description="The payment preimage")
|
2021-07-27 21:10:25 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
value_msat: int = Query(
|
|
|
|
|
..., description="The value of the payment in milli-satoshis"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
payment_request: str = Query(
|
|
|
|
|
None, description="The optional payment request being fulfilled."
|
|
|
|
|
)
|
2021-07-27 21:10:25 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
status: PaymentStatus = Query(
|
|
|
|
|
PaymentStatus.UNKNOWN, description="The status of the payment."
|
|
|
|
|
)
|
2021-07-27 21:10:25 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
fee_msat: int = Query(..., description="The fee paid for this payment in msat")
|
2021-07-27 21:10:25 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
creation_time_ns: int = Query(
|
|
|
|
|
...,
|
|
|
|
|
description="The time in UNIX nanoseconds at which the payment was created.",
|
|
|
|
|
)
|
2021-07-27 21:10:25 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
htlcs: List[HTLCAttempt] = Query(
|
|
|
|
|
[], description="The HTLCs made in attempt to settle the payment."
|
|
|
|
|
)
|
2021-07-27 21:10:25 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
payment_index: int = Query(
|
|
|
|
|
0, description="The payment index. Only set with LND, 0 otherwise."
|
|
|
|
|
)
|
2021-07-27 21:10:25 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
label: str = Query(
|
|
|
|
|
"", description="The payment label. Only set with CLN, empty otherwise."
|
|
|
|
|
)
|
2021-07-27 21:10:25 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
failure_reason: PaymentFailureReason = Query(
|
|
|
|
|
PaymentFailureReason.FAILURE_REASON_NONE, description="The failure reason"
|
|
|
|
|
)
|
2021-07-27 21:10:25 +02:00
|
|
|
|
2021-10-31 18:15:43 +01:00
|
|
|
@classmethod
|
2022-06-04 14:10:44 +02:00
|
|
|
def from_lnd_grpc(cls, p) -> "Payment":
|
2021-10-31 18:15:43 +01:00
|
|
|
def _get_attempts(attempts):
|
2023-05-17 16:02:28 +02:00
|
|
|
return [HTLCAttempt.from_lnd_grpc(a) for a in attempts]
|
2021-07-27 21:10:25 +02:00
|
|
|
|
2021-10-31 18:15:43 +01:00
|
|
|
return cls(
|
|
|
|
|
payment_hash=p.payment_hash,
|
|
|
|
|
payment_preimage=p.payment_preimage,
|
|
|
|
|
value_msat=p.value_msat,
|
|
|
|
|
payment_request=p.payment_request,
|
2022-06-04 14:10:44 +02:00
|
|
|
status=PaymentStatus.from_lnd_grpc(p.status),
|
2021-10-31 18:15:43 +01:00
|
|
|
fee_msat=p.fee_msat,
|
|
|
|
|
creation_time_ns=p.creation_time_ns,
|
|
|
|
|
htlcs=_get_attempts(p.htlcs),
|
|
|
|
|
payment_index=p.payment_index,
|
2022-06-04 14:10:44 +02:00
|
|
|
failure_reason=PaymentFailureReason.from_lnd_grpc(p.failure_reason),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def from_cln_grpc(cls, p) -> "Payment":
|
|
|
|
|
return cls(
|
|
|
|
|
payment_hash=p.payment_hash.hex(),
|
|
|
|
|
payment_preimage="", # CLN currently doesn't return the preimage
|
|
|
|
|
value_msat=p.amount_sent_msat.msat,
|
2022-07-01 20:47:58 +02:00
|
|
|
payment_request="" if not hasattr(p, "bolt11") else p.bolt11,
|
2022-06-04 14:10:44 +02:00
|
|
|
status=PaymentStatus.from_cln_grpc(p.status),
|
|
|
|
|
fee_msat=p.amount_sent_msat.msat - p.amount_msat.msat,
|
|
|
|
|
creation_time_ns=p.created_at,
|
2022-07-01 20:47:58 +02:00
|
|
|
label="" if not hasattr(p, "label") else p.label,
|
2022-06-04 14:10:44 +02:00
|
|
|
failure_reason=PaymentFailureReason.from_cln_grpc(p),
|
2021-10-31 18:15:43 +01:00
|
|
|
)
|
2021-08-02 20:30:18 +02:00
|
|
|
|
2023-03-26 20:53:50 +02:00
|
|
|
@classmethod
|
|
|
|
|
def from_cln_jrpc(cls, p) -> "Payment":
|
|
|
|
|
value = parse_cln_msat(p["amount_msat"])
|
|
|
|
|
total_sent = parse_cln_msat(p["amount_sent_msat"])
|
2024-02-24 15:30:32 +01:00
|
|
|
ts = time.localtime(p["created_at"])
|
2023-03-26 20:53:50 +02:00
|
|
|
|
|
|
|
|
return cls(
|
|
|
|
|
payment_hash=p["payment_hash"],
|
2023-04-02 20:27:05 +02:00
|
|
|
payment_preimage=p["payment_preimage"] if "payment_preimage" in p else "",
|
2023-03-26 20:53:50 +02:00
|
|
|
value_msat=value,
|
2023-04-02 20:27:05 +02:00
|
|
|
payment_request=p["bolt11"] if "bolt11" in p else "",
|
2023-03-26 20:53:50 +02:00
|
|
|
status=PaymentStatus.from_cln_jrpc(p["status"]),
|
|
|
|
|
fee_msat=total_sent - value,
|
2024-02-24 15:30:32 +01:00
|
|
|
creation_time_ns=ts.tm_sec,
|
2023-04-02 20:27:05 +02:00
|
|
|
label=p["label"] if "label" in p else "",
|
2023-03-26 20:53:50 +02:00
|
|
|
failure_reason=PaymentFailureReason.from_cln_jrpc(p),
|
|
|
|
|
)
|
|
|
|
|
|
2021-08-02 20:30:18 +02:00
|
|
|
|
2021-11-22 21:54:42 +01:00
|
|
|
class NewAddressInput(BaseModel):
|
|
|
|
|
type: OnchainAddressType = Query(
|
|
|
|
|
...,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"Address-types has to be one of: "
|
|
|
|
|
"* p2wkh: Pay to witness key hash (bech32) "
|
|
|
|
|
"* np2wkh: Pay to nested witness key hash"
|
|
|
|
|
),
|
2021-11-22 21:54:42 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2021-11-24 11:12:10 +01:00
|
|
|
class UnlockWalletInput(BaseModel):
|
|
|
|
|
password: str = Query(..., description="The wallet password")
|
|
|
|
|
|
|
|
|
|
|
2021-10-03 20:55:28 +02:00
|
|
|
class SendCoinsInput(BaseModel):
|
|
|
|
|
address: str = Query(
|
|
|
|
|
...,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"The base58 or bech32 encoded bitcoin address to send coins to on-chain"
|
|
|
|
|
),
|
2021-10-03 20:55:28 +02:00
|
|
|
)
|
|
|
|
|
target_conf: int = Query(
|
2022-06-04 14:10:44 +02:00
|
|
|
None,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"The number of blocks that the transaction *should* confirm in, "
|
|
|
|
|
"will be used for fee estimation"
|
|
|
|
|
),
|
2021-10-03 20:55:28 +02:00
|
|
|
)
|
|
|
|
|
sat_per_vbyte: int = Query(
|
2022-06-04 14:10:44 +02:00
|
|
|
None,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"A manual fee expressed in sat/vbyte that should be used when "
|
|
|
|
|
"crafting the transaction (default: 0)"
|
|
|
|
|
),
|
2021-10-03 20:55:28 +02:00
|
|
|
)
|
|
|
|
|
min_confs: int = Query(
|
|
|
|
|
1,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"The minimum number of confirmations each one of your outputs "
|
|
|
|
|
"used for the transaction must satisfy"
|
|
|
|
|
),
|
2021-10-03 20:55:28 +02:00
|
|
|
)
|
2022-06-04 14:10:44 +02:00
|
|
|
label: str = Query(
|
|
|
|
|
"", description="A label for the transaction. Ignored by CLN backend."
|
|
|
|
|
)
|
2022-11-28 19:07:25 +01:00
|
|
|
send_all: bool = Query(
|
|
|
|
|
False,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"Send all available on-chain funds from the wallet. Will be "
|
|
|
|
|
"executed `amount` is **0**"
|
|
|
|
|
),
|
2022-11-28 19:07:25 +01:00
|
|
|
)
|
|
|
|
|
amount: conint(ge=0) = Query(
|
|
|
|
|
0,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"The number of bitcoin denominated in satoshis to send. Must not "
|
|
|
|
|
"be set when `send_all` is true."
|
|
|
|
|
),
|
2022-11-28 19:07:25 +01:00
|
|
|
)
|
|
|
|
|
|
2026-07-03 22:22:05 +02:00
|
|
|
@model_validator(mode="after")
|
|
|
|
|
def check_amount_or_send_all(self):
|
|
|
|
|
amount = self.amount if self.amount is not None else 0
|
|
|
|
|
send_all = self.send_all
|
2022-11-28 19:07:25 +01:00
|
|
|
|
|
|
|
|
if amount < 0:
|
|
|
|
|
raise ValueError("Amount must not be negative")
|
|
|
|
|
|
|
|
|
|
if amount == 0 and not send_all:
|
|
|
|
|
# neither amount nor send_all is set
|
|
|
|
|
raise ValueError(
|
2023-05-17 16:02:28 +02:00
|
|
|
(
|
|
|
|
|
"Either amount or send_all must be set. "
|
|
|
|
|
"Please review the documentation."
|
|
|
|
|
)
|
2022-11-28 19:07:25 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if amount > 0 and send_all:
|
|
|
|
|
# amount is set and send_all is true
|
|
|
|
|
raise ValueError(
|
2023-05-17 16:02:28 +02:00
|
|
|
(
|
|
|
|
|
"Amount and send_all must not be set at the same time. "
|
|
|
|
|
"Please review the documentation."
|
|
|
|
|
)
|
2022-11-28 19:07:25 +01:00
|
|
|
)
|
|
|
|
|
|
2026-07-03 22:22:05 +02:00
|
|
|
# valid: (amount > 0 and not send_all) or (amount == 0 and send_all)
|
|
|
|
|
return self
|
2021-10-03 20:55:28 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class SendCoinsResponse(BaseModel):
|
|
|
|
|
txid: str = Query(..., description="The transaction ID for this onchain payment")
|
|
|
|
|
address: str = Query(
|
|
|
|
|
...,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"The base58 or bech32 encoded bitcoin address where the onchain "
|
|
|
|
|
"funds where sent to"
|
|
|
|
|
),
|
2021-10-03 20:55:28 +02:00
|
|
|
)
|
2023-05-01 13:28:58 +02:00
|
|
|
amount: conint(ge=0) = Query(
|
2021-10-03 20:55:28 +02:00
|
|
|
...,
|
|
|
|
|
description="The number of bitcoin denominated in satoshis which where sent",
|
|
|
|
|
)
|
2022-11-28 19:07:25 +01:00
|
|
|
fees: conint(ge=0) = Query(
|
|
|
|
|
None,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"The number of bitcoin denominated in satoshis which where paid as fees"
|
|
|
|
|
),
|
2022-11-28 19:07:25 +01:00
|
|
|
)
|
2022-06-04 14:10:44 +02:00
|
|
|
label: str = Query(
|
|
|
|
|
"", description="The label used for the transaction. Ignored by CLN backend."
|
|
|
|
|
)
|
2023-05-01 13:28:58 +02:00
|
|
|
send_all: bool = Query(
|
|
|
|
|
False,
|
|
|
|
|
description="If this transaction was a `send_all` transaction.",
|
|
|
|
|
)
|
2022-06-04 14:10:44 +02:00
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def from_lnd_grpc(cls, r, input: SendCoinsInput):
|
2023-05-17 16:02:28 +02:00
|
|
|
amount = input.amount if input.send_all is False else r.amount
|
2022-06-04 14:10:44 +02:00
|
|
|
return cls(
|
2022-11-28 19:07:25 +01:00
|
|
|
txid=r.tx_hash,
|
2022-06-04 14:10:44 +02:00
|
|
|
address=input.address,
|
2022-11-28 19:07:25 +01:00
|
|
|
amount=abs(amount),
|
|
|
|
|
fees=r.total_fees,
|
2022-06-04 14:10:44 +02:00
|
|
|
label=input.label,
|
2023-05-01 13:28:58 +02:00
|
|
|
send_all=input.send_all,
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
2021-10-03 20:55:28 +02:00
|
|
|
|
|
|
|
|
@classmethod
|
2022-06-04 14:10:44 +02:00
|
|
|
def from_cln_grpc(cls, r, input: SendCoinsInput):
|
2021-10-03 20:55:28 +02:00
|
|
|
return cls(
|
2023-05-01 13:28:58 +02:00
|
|
|
txid=r.txid.hex(),
|
2021-10-03 20:55:28 +02:00
|
|
|
address=input.address,
|
|
|
|
|
amount=input.amount,
|
|
|
|
|
label=input.label,
|
2023-05-01 13:28:58 +02:00
|
|
|
send_all=input.send_all,
|
2021-10-03 20:55:28 +02:00
|
|
|
)
|
|
|
|
|
|
2023-03-26 20:53:50 +02:00
|
|
|
@classmethod
|
|
|
|
|
def from_cln_json(cls, r, input: SendCoinsInput):
|
|
|
|
|
return cls(
|
|
|
|
|
txid=r["txid"],
|
|
|
|
|
address=input.address,
|
|
|
|
|
amount=input.amount,
|
|
|
|
|
label=input.label,
|
2023-05-01 13:28:58 +02:00
|
|
|
send_all=input.send_all,
|
2023-03-26 20:53:50 +02:00
|
|
|
)
|
|
|
|
|
|
2021-10-03 20:55:28 +02:00
|
|
|
|
2021-08-02 20:30:18 +02:00
|
|
|
class Chain(BaseModel):
|
|
|
|
|
# The blockchain the node is on(eg bitcoin, litecoin)
|
|
|
|
|
chain: str
|
|
|
|
|
|
|
|
|
|
# The network the node is on(eg regtest, testnet, mainnet)
|
|
|
|
|
network: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LnInfo(BaseModel):
|
2021-11-18 19:50:04 +01:00
|
|
|
implementation: str = Query(
|
2022-06-04 14:10:44 +02:00
|
|
|
..., description="Lightning software implementation (LND, CLN)"
|
2021-11-18 19:50:04 +01:00
|
|
|
)
|
2021-08-02 20:30:18 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
version: str = Query(
|
|
|
|
|
..., description="The version of the software that the node is running."
|
|
|
|
|
)
|
2021-08-02 20:30:18 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
commit_hash: str = Query(
|
|
|
|
|
..., description="The SHA1 commit hash that the daemon is compiled with."
|
|
|
|
|
)
|
2022-05-18 00:14:35 +02:00
|
|
|
|
2023-05-20 14:13:24 +02:00
|
|
|
identity_pubkey: str = Query(
|
|
|
|
|
..., description="The identity pubkey of the current node."
|
|
|
|
|
)
|
2021-08-02 20:30:18 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
identity_uri: str = Query(
|
2023-05-20 14:13:24 +02:00
|
|
|
...,
|
|
|
|
|
description="The complete URI (pubkey@physicaladdress:port) the current node.",
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
2021-08-02 20:30:18 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
alias: str = Query(..., description="The alias of the node.")
|
2021-08-02 20:30:18 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
color: str = Query(
|
|
|
|
|
..., description="The color of the current node in hex code format."
|
|
|
|
|
)
|
2021-08-02 20:30:18 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
num_pending_channels: int = Query(..., description="Number of pending channels.")
|
2021-08-02 20:30:18 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
num_active_channels: int = Query(..., description="Number of active channels.")
|
2021-08-02 20:30:18 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
num_inactive_channels: int = Query(..., description="Number of inactive channels.")
|
2021-08-02 20:30:18 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
num_peers: int = Query(..., description="Number of peers.")
|
2021-08-02 20:30:18 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
block_height: int = Query(
|
|
|
|
|
...,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"The node's current view of the height of the best block. "
|
|
|
|
|
"Only available with LND."
|
|
|
|
|
),
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
2021-08-02 20:30:18 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
block_hash: str = Query(
|
|
|
|
|
"",
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"The node's current view of the hash of the best block. "
|
|
|
|
|
"Only available with LND."
|
|
|
|
|
),
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
2021-08-02 20:30:18 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
best_header_timestamp: int = Query(
|
|
|
|
|
None,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
2025-03-25 09:48:40 +01:00
|
|
|
"Timestamp of the block best known to the wallet. Only available with LND."
|
2023-05-17 16:02:28 +02:00
|
|
|
),
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
2021-08-02 20:30:18 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
synced_to_chain: bool = Query(
|
|
|
|
|
None,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"Whether the wallet's view is synced to the main chain. "
|
|
|
|
|
"Only available with LND."
|
|
|
|
|
),
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
synced_to_graph: bool = Query(
|
|
|
|
|
None,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"Whether we consider ourselves synced with the public channel "
|
|
|
|
|
"graph. Only available with LND."
|
|
|
|
|
),
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
2021-08-02 20:30:18 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
chains: List[Chain] = Query(
|
|
|
|
|
[], description="A list of active chains the node is connected to"
|
|
|
|
|
)
|
2021-08-02 20:30:18 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
uris: List[str] = Query([], description="The URIs of the current node.")
|
2021-08-02 20:30:18 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
features: List[FeaturesEntry] = Query(
|
|
|
|
|
[],
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"Features that our node has advertised in our init message node "
|
|
|
|
|
"announcements and invoices. Not yet implemented with CLN"
|
|
|
|
|
),
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
2021-08-02 20:30:18 +02:00
|
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
|
if isinstance(other, self.__class__):
|
|
|
|
|
diff = DeepDiff(self, other, ignore_order=True)
|
|
|
|
|
return len(diff) == 0
|
|
|
|
|
else:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
def __ne__(self, other):
|
|
|
|
|
return not self.__eq__(other)
|
|
|
|
|
|
2021-10-31 18:15:43 +01:00
|
|
|
@classmethod
|
2022-06-04 14:10:44 +02:00
|
|
|
def from_lnd_grpc(cls, implementation, i) -> "LnInfo":
|
2021-10-31 18:15:43 +01:00
|
|
|
_chains = []
|
|
|
|
|
for c in i.chains:
|
|
|
|
|
_chains.append(Chain(chain=c.chain, network=c.network))
|
|
|
|
|
|
|
|
|
|
_features = []
|
|
|
|
|
for f in i.features:
|
2022-06-04 14:10:44 +02:00
|
|
|
_features.append(FeaturesEntry.from_lnd_grpc(f, i.features[f]))
|
2021-10-31 18:15:43 +01:00
|
|
|
|
|
|
|
|
_uris = [u for u in i.uris]
|
2022-12-20 07:43:37 +01:00
|
|
|
uri = ""
|
|
|
|
|
if len(_uris) > 0:
|
|
|
|
|
uri = _uris[0]
|
2021-10-31 18:15:43 +01:00
|
|
|
|
|
|
|
|
return LnInfo(
|
2021-11-18 19:50:04 +01:00
|
|
|
implementation=implementation,
|
2021-10-31 18:15:43 +01:00
|
|
|
version=i.version,
|
|
|
|
|
commit_hash=i.commit_hash,
|
|
|
|
|
identity_pubkey=i.identity_pubkey,
|
2022-12-20 07:43:37 +01:00
|
|
|
identity_uri=uri,
|
2021-10-31 18:15:43 +01:00
|
|
|
alias=i.alias,
|
|
|
|
|
color=i.color,
|
|
|
|
|
num_pending_channels=i.num_pending_channels,
|
|
|
|
|
num_active_channels=i.num_active_channels,
|
|
|
|
|
num_inactive_channels=i.num_inactive_channels,
|
|
|
|
|
num_peers=i.num_peers,
|
|
|
|
|
block_height=i.block_height,
|
|
|
|
|
block_hash=i.block_hash,
|
|
|
|
|
best_header_timestamp=i.best_header_timestamp,
|
|
|
|
|
synced_to_chain=i.synced_to_chain,
|
|
|
|
|
synced_to_graph=i.synced_to_graph,
|
|
|
|
|
chains=_chains,
|
|
|
|
|
uris=_uris,
|
|
|
|
|
features=_features,
|
|
|
|
|
)
|
2021-08-03 21:20:24 +02:00
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
@classmethod
|
2023-03-26 20:53:50 +02:00
|
|
|
def from_cln_jrpc(cls, implementation, i) -> "LnInfo":
|
2022-06-04 14:10:44 +02:00
|
|
|
_chains = [Chain(chain="bitcoin", network=i["network"])]
|
|
|
|
|
|
|
|
|
|
_features = []
|
|
|
|
|
# TODO: Map CLN's feature advertisements to LND's
|
|
|
|
|
# for k in i["our_features"].keys():
|
|
|
|
|
# _features.append(FeaturesEntry.from_cln_json(i["our_features"][k], k))
|
|
|
|
|
|
|
|
|
|
_uris = []
|
2023-05-20 14:13:24 +02:00
|
|
|
pubkey = i["id"]
|
|
|
|
|
if "binding" in i:
|
|
|
|
|
for b in i["binding"]:
|
|
|
|
|
_uris.append(f"{pubkey}@{b['address']}:{b['port']}")
|
|
|
|
|
|
|
|
|
|
if "address" in i:
|
|
|
|
|
for b in i["address"]:
|
|
|
|
|
_uris.append(f"{pubkey}@{b['address']}:{b['port']}")
|
|
|
|
|
|
|
|
|
|
uri = ""
|
|
|
|
|
if len(_uris) > 0:
|
|
|
|
|
uri = _uris[0]
|
2022-06-04 14:10:44 +02:00
|
|
|
|
|
|
|
|
return LnInfo(
|
|
|
|
|
implementation=implementation,
|
|
|
|
|
version=i["version"],
|
|
|
|
|
commit_hash=i["version"].split("-")[-1],
|
2023-05-20 14:13:24 +02:00
|
|
|
identity_pubkey=pubkey,
|
|
|
|
|
identity_uri=uri,
|
2022-06-04 14:10:44 +02:00
|
|
|
alias=i["alias"],
|
|
|
|
|
color=i["color"],
|
|
|
|
|
num_pending_channels=i["num_pending_channels"],
|
|
|
|
|
num_active_channels=i["num_active_channels"],
|
|
|
|
|
num_inactive_channels=i["num_inactive_channels"],
|
|
|
|
|
num_peers=i["num_peers"],
|
|
|
|
|
block_height=i["blockheight"],
|
|
|
|
|
chains=_chains,
|
|
|
|
|
uris=_uris,
|
|
|
|
|
features=_features,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def from_cln_grpc(cls, implementation, i) -> "LnInfo":
|
|
|
|
|
_chains = [Chain(chain="bitcoin", network=i.network)]
|
|
|
|
|
|
|
|
|
|
_features = []
|
|
|
|
|
# TODO: Map CLN's feature advertisements to LND's
|
|
|
|
|
# for k in i["our_features"].keys():
|
|
|
|
|
# _features.append(FeaturesEntry.from_cln_json(i["our_features"][k], k))
|
|
|
|
|
|
2022-12-20 07:43:37 +01:00
|
|
|
pubkey = i.id.hex()
|
|
|
|
|
uri = ""
|
2022-06-04 14:10:44 +02:00
|
|
|
_uris = []
|
2022-12-20 07:43:37 +01:00
|
|
|
for b in i.address:
|
|
|
|
|
_uris.append(f"{pubkey}@{b.address}:{b.port}")
|
2022-06-04 14:10:44 +02:00
|
|
|
for b in i.binding:
|
2022-12-20 07:43:37 +01:00
|
|
|
_uris.append(f"{pubkey}@{b.address}:{b.port}")
|
|
|
|
|
if len(_uris) > 0:
|
|
|
|
|
uri = _uris[0]
|
2022-06-04 14:10:44 +02:00
|
|
|
|
|
|
|
|
return LnInfo(
|
|
|
|
|
implementation=implementation,
|
|
|
|
|
version=i.version,
|
|
|
|
|
commit_hash=i.version.split("-")[-1],
|
2022-12-20 07:43:37 +01:00
|
|
|
identity_pubkey=pubkey,
|
|
|
|
|
identity_uri=uri,
|
2022-06-04 14:10:44 +02:00
|
|
|
alias=i.alias,
|
|
|
|
|
color=i.color.hex(),
|
|
|
|
|
num_pending_channels=i.num_pending_channels,
|
|
|
|
|
num_active_channels=i.num_active_channels,
|
|
|
|
|
num_inactive_channels=i.num_inactive_channels,
|
|
|
|
|
num_peers=i.num_peers,
|
|
|
|
|
block_height=i.blockheight,
|
|
|
|
|
chains=_chains,
|
|
|
|
|
uris=_uris,
|
|
|
|
|
features=_features,
|
|
|
|
|
)
|
|
|
|
|
|
2021-08-03 21:20:24 +02:00
|
|
|
|
|
|
|
|
class WalletBalance(BaseModel):
|
2021-09-05 19:19:53 +02:00
|
|
|
onchain_confirmed_balance: int = Query(
|
|
|
|
|
...,
|
2023-05-17 16:02:28 +02:00
|
|
|
description="Confirmed onchain balance (more than 3 confirmations) in sat",
|
2021-09-05 19:19:53 +02:00
|
|
|
)
|
|
|
|
|
onchain_total_balance: int = Query(
|
|
|
|
|
..., description="Total combined onchain balance in sat"
|
|
|
|
|
)
|
|
|
|
|
onchain_unconfirmed_balance: int = Query(
|
|
|
|
|
...,
|
2023-05-17 16:02:28 +02:00
|
|
|
description="Unconfirmed onchain balance (less than 3 confirmations) in sat",
|
2021-08-03 21:20:24 +02:00
|
|
|
)
|
2021-09-05 19:19:53 +02:00
|
|
|
channel_local_balance: int = Query(
|
|
|
|
|
..., description="Sum of channels local balances in msat"
|
|
|
|
|
)
|
|
|
|
|
channel_remote_balance: int = Query(
|
|
|
|
|
..., description="Sum of channels remote balances in msat."
|
|
|
|
|
)
|
|
|
|
|
channel_unsettled_local_balance: int = Query(
|
|
|
|
|
..., description="Sum of channels local unsettled balances in msat."
|
|
|
|
|
)
|
|
|
|
|
channel_unsettled_remote_balance: int = Query(
|
|
|
|
|
..., description="Sum of channels remote unsettled balances in msat."
|
|
|
|
|
)
|
|
|
|
|
channel_pending_open_local_balance: int = Query(
|
|
|
|
|
..., description="Sum of channels pending local balances in msat."
|
|
|
|
|
)
|
|
|
|
|
channel_pending_open_remote_balance: int = Query(
|
|
|
|
|
..., description="Sum of channels pending remote balances in msat."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2022-06-04 14:10:44 +02:00
|
|
|
def from_lnd_grpc(cls, onchain, channel) -> "WalletBalance":
|
2021-09-05 19:19:53 +02:00
|
|
|
return cls(
|
|
|
|
|
onchain_confirmed_balance=onchain.confirmed_balance,
|
|
|
|
|
onchain_total_balance=onchain.total_balance,
|
|
|
|
|
onchain_unconfirmed_balance=onchain.unconfirmed_balance,
|
|
|
|
|
channel_local_balance=channel.local_balance.msat,
|
|
|
|
|
channel_remote_balance=channel.remote_balance.msat,
|
|
|
|
|
channel_unsettled_local_balance=channel.unsettled_local_balance.msat,
|
|
|
|
|
channel_unsettled_remote_balance=channel.unsettled_remote_balance.msat,
|
|
|
|
|
channel_pending_open_local_balance=channel.pending_open_local_balance.msat,
|
|
|
|
|
channel_pending_open_remote_balance=channel.pending_open_remote_balance.msat,
|
|
|
|
|
)
|
2021-09-20 17:00:36 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class PaymentRequest(BaseModel):
|
|
|
|
|
destination: str
|
|
|
|
|
payment_hash: str
|
2022-06-04 14:10:44 +02:00
|
|
|
num_satoshis: int = Query(
|
|
|
|
|
None, description="Deprecated. User num_msat instead", deprecated=True
|
|
|
|
|
)
|
2021-09-20 17:00:36 +02:00
|
|
|
timestamp: int
|
|
|
|
|
expiry: int
|
|
|
|
|
description: str
|
2022-06-04 14:10:44 +02:00
|
|
|
description_hash: Optional[str]
|
2021-09-20 17:00:36 +02:00
|
|
|
fallback_addr: Optional[str]
|
|
|
|
|
cltv_expiry: int
|
2021-12-04 12:13:23 +01:00
|
|
|
route_hints: List[RouteHint] = Query(
|
|
|
|
|
[], description="A list of [HopHint] for the RouteHint"
|
|
|
|
|
)
|
2022-06-04 14:10:44 +02:00
|
|
|
payment_addr: str = Query("", description="The payment address in hex format")
|
|
|
|
|
num_msat: Optional[int]
|
2021-09-20 17:00:36 +02:00
|
|
|
features: List[FeaturesEntry] = Query([])
|
2022-07-01 20:12:36 +02:00
|
|
|
currency: Optional[str] = Query(
|
|
|
|
|
"", description="Optional requested currency of the payment. "
|
|
|
|
|
)
|
2021-09-20 17:00:36 +02:00
|
|
|
|
|
|
|
|
@classmethod
|
2022-06-04 14:10:44 +02:00
|
|
|
def from_lnd_grpc(cls, r):
|
2021-09-20 17:00:36 +02:00
|
|
|
return cls(
|
|
|
|
|
destination=r.destination,
|
|
|
|
|
payment_hash=r.payment_hash,
|
|
|
|
|
num_satoshis=r.num_satoshis,
|
|
|
|
|
timestamp=r.timestamp,
|
|
|
|
|
expiry=r.expiry,
|
|
|
|
|
description=r.description,
|
|
|
|
|
description_hash=r.description_hash,
|
|
|
|
|
fallback_addr=r.fallback_addr,
|
|
|
|
|
cltv_expiry=r.cltv_expiry,
|
2022-06-04 14:10:44 +02:00
|
|
|
route_hints=[RouteHint.from_lnd_grpc(rh) for rh in r.route_hints],
|
2021-09-20 17:00:36 +02:00
|
|
|
payment_addr=r.payment_addr.hex(),
|
|
|
|
|
num_msat=r.num_msat,
|
2022-06-04 14:10:44 +02:00
|
|
|
features=[
|
|
|
|
|
FeaturesEntry.from_lnd_grpc(k, r.features[k]) for k in r.features
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def from_cln_json(cls, r):
|
|
|
|
|
routes = []
|
|
|
|
|
if "routes" in r.keys():
|
|
|
|
|
routes = [RouteHint.from_cln_json(rh) for rh in r["routes"]]
|
|
|
|
|
|
|
|
|
|
msat = 0
|
|
|
|
|
if "msatoshi" in r:
|
|
|
|
|
msat = r["msatoshi"]
|
|
|
|
|
|
|
|
|
|
features = []
|
|
|
|
|
# TODO: Map CLN's feature advertisements to LND's
|
|
|
|
|
|
|
|
|
|
return cls(
|
2022-07-01 20:12:36 +02:00
|
|
|
currency="" if "currency" not in r else r["currency"],
|
2022-06-04 14:10:44 +02:00
|
|
|
destination=r["payee"],
|
|
|
|
|
payment_hash=r["payment_hash"],
|
|
|
|
|
num_satoshis=msat / 1000,
|
|
|
|
|
timestamp=r["created_at"],
|
2022-07-23 16:36:55 +02:00
|
|
|
expiry=0 if "expiry" not in r else r["expiry"],
|
|
|
|
|
description="" if "description" not in r else r["description"],
|
2024-02-18 12:11:15 +01:00
|
|
|
description_hash=(
|
|
|
|
|
"" if "description_hash" not in r else r["description_hash"]
|
|
|
|
|
),
|
2022-06-04 14:10:44 +02:00
|
|
|
fallback_addr="" if "fallbacks" not in r else r["fallbacks"][0],
|
|
|
|
|
cltv_expiry=r["min_final_cltv_expiry"],
|
|
|
|
|
route_hints=routes,
|
|
|
|
|
num_msat=msat,
|
|
|
|
|
payment_addr=r["payment_secret"],
|
|
|
|
|
features=features,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def from_cln_grpc(cls, r):
|
|
|
|
|
routes = []
|
|
|
|
|
if "routes" in r.keys():
|
|
|
|
|
routes = [RouteHint.from_cln_json(rh) for rh in r["routes"]]
|
|
|
|
|
|
|
|
|
|
msat = 0
|
|
|
|
|
if "amount_msat" in r:
|
|
|
|
|
msat = r["amount_msat"]
|
|
|
|
|
|
|
|
|
|
features = []
|
|
|
|
|
# TODO: Map CLN's feature advertisements to LND's
|
|
|
|
|
|
|
|
|
|
dhash = ""
|
|
|
|
|
if hasattr(r, "payment_hash"):
|
|
|
|
|
dhash = r.payment_hash.hex()
|
|
|
|
|
|
|
|
|
|
fback = []
|
|
|
|
|
if hasattr(r, "fallbacks"):
|
|
|
|
|
fback = r["fallbacks"][0]
|
|
|
|
|
|
|
|
|
|
return cls(
|
|
|
|
|
destination=r.payee,
|
|
|
|
|
payment_hash=r.payment_hash.hex(),
|
|
|
|
|
num_satoshis=msat / 1000,
|
|
|
|
|
timestamp=r.created_at,
|
|
|
|
|
expiry=r.expiry,
|
|
|
|
|
description=r.description,
|
|
|
|
|
description_hash=dhash,
|
|
|
|
|
fallback_addr=fback,
|
|
|
|
|
cltv_expiry=r.min_final_cltv_expiry,
|
|
|
|
|
route_hints=routes,
|
|
|
|
|
num_msat=msat,
|
|
|
|
|
payment_addr=r.payment_secret,
|
|
|
|
|
features=features,
|
2021-09-20 17:00:36 +02:00
|
|
|
)
|
2021-10-31 16:49:15 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class OnChainTransaction(BaseModel):
|
|
|
|
|
tx_hash: str = Query(..., description="The transaction hash")
|
|
|
|
|
amount: int = Query(
|
|
|
|
|
..., description="The transaction amount, denominated in satoshis"
|
|
|
|
|
)
|
|
|
|
|
num_confirmations: int = Query(..., description="The number of confirmations")
|
|
|
|
|
block_height: int = Query(
|
|
|
|
|
..., description="The height of the block this transaction was included in"
|
|
|
|
|
)
|
|
|
|
|
time_stamp: int = Query(..., description="Timestamp of this transaction")
|
|
|
|
|
total_fees: int = Query(..., description="Fees paid for this transaction")
|
|
|
|
|
dest_addresses: List[str] = Query(
|
|
|
|
|
[], description="Addresses that received funds for this transaction"
|
|
|
|
|
)
|
|
|
|
|
label: str = Query(
|
|
|
|
|
"", description="An optional label that was set on transaction broadcast."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2022-06-04 14:10:44 +02:00
|
|
|
def from_lnd_grpc(cls, t):
|
2021-10-31 16:49:15 +01:00
|
|
|
addrs = [a for a in t.dest_addresses]
|
|
|
|
|
return cls(
|
|
|
|
|
tx_hash=t.tx_hash,
|
|
|
|
|
amount=t.amount,
|
|
|
|
|
num_confirmations=t.num_confirmations,
|
|
|
|
|
block_height=t.block_height,
|
|
|
|
|
time_stamp=t.time_stamp,
|
|
|
|
|
total_fees=t.total_fees,
|
|
|
|
|
dest_addresses=addrs,
|
|
|
|
|
label=t.label,
|
|
|
|
|
)
|
2021-11-02 19:02:08 +01:00
|
|
|
|
2022-11-05 22:35:01 +01:00
|
|
|
@classmethod
|
|
|
|
|
def from_cln_bkpr(cls, t):
|
|
|
|
|
amount = None
|
|
|
|
|
if t["tag"] == "deposit":
|
|
|
|
|
amount = parse_cln_msat(t["credit_msat"]) / 1000
|
|
|
|
|
elif t["tag"] == "withdrawal":
|
|
|
|
|
amount = -parse_cln_msat(t["debit_msat"]) / 1000
|
|
|
|
|
|
|
|
|
|
return cls(
|
|
|
|
|
tx_hash=t["outpoint"].split(":")[0],
|
|
|
|
|
amount=amount,
|
|
|
|
|
num_confirmations=0, # block_height - t["blockheight"],
|
|
|
|
|
block_height=0, # Block height must be set later in CLN ...
|
|
|
|
|
time_stamp=t["timestamp"],
|
|
|
|
|
total_fees=0, # Fees are an extra event in CLN, must be set later
|
|
|
|
|
dest_addresses=[],
|
|
|
|
|
)
|
|
|
|
|
|
2021-11-02 19:02:08 +01:00
|
|
|
|
|
|
|
|
class TxCategory(str, Enum):
|
|
|
|
|
ONCHAIN = "onchain"
|
|
|
|
|
LIGHTNING = "ln"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TxType(str, Enum):
|
|
|
|
|
UNKNOWN = "unknown"
|
|
|
|
|
SEND = "send"
|
|
|
|
|
RECEIVE = "receive"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TxStatus(str, Enum):
|
|
|
|
|
UNKNOWN = "unknown"
|
|
|
|
|
IN_FLIGHT = "in_flight"
|
|
|
|
|
SUCCEEDED = "succeeded"
|
|
|
|
|
FAILED = "failed"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class GenericTx(BaseModel):
|
|
|
|
|
index: int = Query(0, description="The index of the transaction.")
|
|
|
|
|
id: str = Query(..., description=docs.tx_id_desc)
|
|
|
|
|
category: TxCategory = Query(
|
|
|
|
|
...,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"Whether this is an onchain (**onchain**) or lightning (**ln**) "
|
|
|
|
|
"transaction."
|
|
|
|
|
),
|
2021-11-02 19:02:08 +01:00
|
|
|
)
|
|
|
|
|
type: TxType = Query(
|
|
|
|
|
...,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"Whether this is an outgoing (**send**) transaction or an "
|
|
|
|
|
"incoming(**receive**) transaction."
|
|
|
|
|
),
|
2021-11-02 19:02:08 +01:00
|
|
|
)
|
|
|
|
|
amount: int = Query(..., description=docs.tx_amount_desc)
|
|
|
|
|
time_stamp: int = Query(..., description=docs.tx_time_stamp_desc)
|
|
|
|
|
comment: str = Query("", description="Optional comment for this transaction")
|
|
|
|
|
status: TxStatus = Query(..., description=docs.tx_status_desc)
|
|
|
|
|
block_height: int = Query(
|
|
|
|
|
None,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"Block height, if included in a block. Only applicable for "
|
|
|
|
|
"category **onchain**."
|
|
|
|
|
),
|
2021-11-02 19:02:08 +01:00
|
|
|
)
|
2024-02-24 15:30:32 +01:00
|
|
|
num_confs: int | None = Query(
|
|
|
|
|
None,
|
2022-07-31 18:01:02 +02:00
|
|
|
ge=0,
|
2023-05-17 16:02:28 +02:00
|
|
|
description=(
|
|
|
|
|
"Number of confirmations. Only applicable for category **onchain**."
|
|
|
|
|
),
|
2021-11-02 19:02:08 +01:00
|
|
|
)
|
|
|
|
|
total_fees: int = Query(None, description="Total fees paid for this transaction")
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2023-03-26 20:53:50 +02:00
|
|
|
def from_invoice(cls, i: Invoice) -> "GenericTx":
|
2021-11-02 19:02:08 +01:00
|
|
|
status = TxStatus.UNKNOWN
|
2023-03-26 20:53:50 +02:00
|
|
|
time_stamp = i.expiry_date
|
2023-04-02 20:27:05 +02:00
|
|
|
amount = None
|
2023-03-26 20:53:50 +02:00
|
|
|
if i.state == InvoiceState.SETTLED:
|
2021-11-02 19:02:08 +01:00
|
|
|
status = TxStatus.SUCCEEDED
|
|
|
|
|
time_stamp = i.settle_date
|
2023-04-02 20:27:05 +02:00
|
|
|
amount = i.amt_paid_msat
|
2023-03-26 20:53:50 +02:00
|
|
|
elif i.state == InvoiceState.OPEN:
|
2021-11-02 19:02:08 +01:00
|
|
|
status = TxStatus.IN_FLIGHT
|
2023-04-02 20:27:05 +02:00
|
|
|
amount = i.value_msat
|
2023-03-26 20:53:50 +02:00
|
|
|
elif i.state == InvoiceState.CANCELED:
|
2021-11-02 19:02:08 +01:00
|
|
|
status = TxStatus.FAILED
|
2023-04-02 20:27:05 +02:00
|
|
|
amount = i.value_msat
|
2021-11-02 19:02:08 +01:00
|
|
|
|
|
|
|
|
return cls(
|
2024-02-24 15:30:32 +01:00
|
|
|
id=i.payment_request or "",
|
2021-11-02 19:02:08 +01:00
|
|
|
category=TxCategory.LIGHTNING,
|
|
|
|
|
type=TxType.RECEIVE,
|
2023-04-02 20:27:05 +02:00
|
|
|
amount=amount,
|
2021-11-02 19:02:08 +01:00
|
|
|
time_stamp=time_stamp,
|
|
|
|
|
comment=i.memo,
|
|
|
|
|
status=status,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2023-03-26 20:53:50 +02:00
|
|
|
def from_onchain_tx(
|
|
|
|
|
cls, tx: OnChainTransaction, current_block_height: int
|
|
|
|
|
) -> "GenericTx":
|
|
|
|
|
confs = current_block_height - tx.block_height
|
|
|
|
|
if confs < 0:
|
|
|
|
|
confs = 0
|
2022-08-01 19:21:29 +02:00
|
|
|
logging.warning(
|
2023-05-17 16:02:28 +02:00
|
|
|
f"""Got negative confirmation count of for {tx.tx_hash}\n
|
|
|
|
|
Calc:{current_block_height} - {tx.block_height} = {confs}
|
|
|
|
|
"""
|
2022-08-01 19:21:29 +02:00
|
|
|
)
|
|
|
|
|
|
2023-03-26 20:53:50 +02:00
|
|
|
s = TxStatus.SUCCEEDED if confs > 0 else TxStatus.IN_FLIGHT
|
2021-11-02 19:02:08 +01:00
|
|
|
|
2023-03-26 20:53:50 +02:00
|
|
|
t = TxType.SEND
|
|
|
|
|
if tx.total_fees == 0:
|
2021-11-02 19:02:08 +01:00
|
|
|
t = TxType.RECEIVE
|
|
|
|
|
|
|
|
|
|
return cls(
|
|
|
|
|
id=tx.tx_hash,
|
|
|
|
|
category=TxCategory.ONCHAIN,
|
|
|
|
|
type=t,
|
|
|
|
|
amount=tx.amount,
|
2023-03-26 20:53:50 +02:00
|
|
|
time_stamp=0,
|
2021-11-02 19:02:08 +01:00
|
|
|
status=s,
|
2023-03-26 20:53:50 +02:00
|
|
|
comment="",
|
2021-11-02 19:02:08 +01:00
|
|
|
block_height=tx.block_height,
|
2023-03-26 20:53:50 +02:00
|
|
|
num_confs=confs,
|
2021-11-02 19:02:08 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2023-03-26 20:53:50 +02:00
|
|
|
def from_payment(cls, payment: Payment, comment: str = "") -> "GenericTx":
|
2021-11-02 19:02:08 +01:00
|
|
|
return cls(
|
|
|
|
|
id=payment.payment_request,
|
|
|
|
|
category=TxCategory.LIGHTNING,
|
|
|
|
|
type=TxType.SEND,
|
2023-03-26 20:53:50 +02:00
|
|
|
time_stamp=payment.creation_time_ns,
|
2021-11-02 19:02:08 +01:00
|
|
|
amount=-payment.value_msat,
|
2023-03-26 20:53:50 +02:00
|
|
|
status=payment.status,
|
2021-11-02 19:02:08 +01:00
|
|
|
total_fees=payment.fee_msat,
|
|
|
|
|
comment=comment,
|
|
|
|
|
)
|
2022-06-04 14:10:44 +02:00
|
|
|
|
|
|
|
|
@classmethod
|
2023-03-26 20:53:50 +02:00
|
|
|
def from_lnd_grpc_invoice(cls, i) -> "GenericTx":
|
2022-06-04 14:10:44 +02:00
|
|
|
status = TxStatus.UNKNOWN
|
2023-03-26 20:53:50 +02:00
|
|
|
time_stamp = i.creation_date
|
|
|
|
|
amount = i.value_msat
|
|
|
|
|
if i.settled:
|
2022-06-04 14:10:44 +02:00
|
|
|
status = TxStatus.SUCCEEDED
|
2023-03-26 20:53:50 +02:00
|
|
|
time_stamp = i.settle_date
|
|
|
|
|
amount = i.amt_paid_msat
|
|
|
|
|
elif i.state == 0 or i.state == 3: # state is OPEN or ACCEPTED
|
2022-06-04 14:10:44 +02:00
|
|
|
status = TxStatus.IN_FLIGHT
|
2023-03-26 20:53:50 +02:00
|
|
|
elif i.state == 2: # state is CANCELED
|
2022-06-04 14:10:44 +02:00
|
|
|
status = TxStatus.FAILED
|
|
|
|
|
|
|
|
|
|
return cls(
|
2023-03-26 20:53:50 +02:00
|
|
|
id=i.payment_request,
|
2022-06-04 14:10:44 +02:00
|
|
|
category=TxCategory.LIGHTNING,
|
|
|
|
|
type=TxType.RECEIVE,
|
|
|
|
|
amount=amount,
|
|
|
|
|
time_stamp=time_stamp,
|
2023-03-26 20:53:50 +02:00
|
|
|
comment=i.memo,
|
2022-06-04 14:10:44 +02:00
|
|
|
status=status,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2023-03-26 20:53:50 +02:00
|
|
|
def from_lnd_grpc_onchain_tx(cls, tx) -> "GenericTx":
|
|
|
|
|
if tx.num_confirmations < 0:
|
2022-08-01 19:21:29 +02:00
|
|
|
logging.warning(
|
2023-03-26 20:53:50 +02:00
|
|
|
f"Got negative confirmation count of from LND {tx.num_confirmations}"
|
2022-07-31 18:01:02 +02:00
|
|
|
)
|
|
|
|
|
|
2023-03-26 20:53:50 +02:00
|
|
|
s = TxStatus.SUCCEEDED if tx.num_confirmations > 0 else TxStatus.IN_FLIGHT
|
2022-06-04 14:10:44 +02:00
|
|
|
|
|
|
|
|
t = TxType.UNKNOWN
|
2023-03-26 20:53:50 +02:00
|
|
|
if tx.amount > 0:
|
2022-06-04 14:10:44 +02:00
|
|
|
t = TxType.RECEIVE
|
2023-03-26 20:53:50 +02:00
|
|
|
elif tx.amount < 0:
|
2022-06-04 14:10:44 +02:00
|
|
|
t = TxType.SEND
|
2023-03-26 20:53:50 +02:00
|
|
|
# else == 0 => unknown
|
2022-06-04 14:10:44 +02:00
|
|
|
|
|
|
|
|
return cls(
|
2023-03-26 20:53:50 +02:00
|
|
|
id=tx.tx_hash,
|
2022-06-04 14:10:44 +02:00
|
|
|
category=TxCategory.ONCHAIN,
|
|
|
|
|
type=t,
|
2023-03-26 20:53:50 +02:00
|
|
|
amount=tx.amount,
|
|
|
|
|
time_stamp=tx.time_stamp,
|
2022-06-04 14:10:44 +02:00
|
|
|
status=s,
|
2023-03-26 20:53:50 +02:00
|
|
|
comment=tx.label,
|
|
|
|
|
block_height=tx.block_height,
|
|
|
|
|
num_confs=tx.num_confirmations,
|
2022-06-04 14:10:44 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2023-03-26 20:53:50 +02:00
|
|
|
def from_lnd_grpc_payment(cls, payment, comment: str = "") -> "GenericTx":
|
|
|
|
|
status = TxStatus.UNKNOWN
|
|
|
|
|
if payment.status == 1:
|
2022-06-04 14:10:44 +02:00
|
|
|
status = TxStatus.IN_FLIGHT
|
2023-03-26 20:53:50 +02:00
|
|
|
elif payment.status == 2:
|
2022-06-04 14:10:44 +02:00
|
|
|
status = TxStatus.SUCCEEDED
|
2023-03-26 20:53:50 +02:00
|
|
|
elif payment.status == 3:
|
2022-06-04 14:10:44 +02:00
|
|
|
status = TxStatus.FAILED
|
|
|
|
|
|
|
|
|
|
return cls(
|
2023-03-26 20:53:50 +02:00
|
|
|
id=payment.payment_request,
|
2022-06-04 14:10:44 +02:00
|
|
|
category=TxCategory.LIGHTNING,
|
|
|
|
|
type=TxType.SEND,
|
2023-03-26 20:53:50 +02:00
|
|
|
time_stamp=payment.creation_date,
|
|
|
|
|
amount=-payment.value_msat,
|
2022-06-04 14:10:44 +02:00
|
|
|
status=status,
|
2023-03-26 20:53:50 +02:00
|
|
|
total_fees=payment.fee_msat,
|
2022-06-04 14:10:44 +02:00
|
|
|
comment=comment,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def from_cln_grpc_invoice(cls, i) -> "GenericTx":
|
|
|
|
|
status = TxStatus.UNKNOWN
|
|
|
|
|
time_stamp = i.expires_at
|
|
|
|
|
amount = i.amount_msat.msat
|
|
|
|
|
if i.status == 0: # unpaid
|
|
|
|
|
status = TxStatus.IN_FLIGHT
|
|
|
|
|
elif i.status == 1: # paid
|
|
|
|
|
status = TxStatus.SUCCEEDED
|
|
|
|
|
time_stamp = i.paid_at
|
|
|
|
|
amount = i.amount_received_msat.msat
|
|
|
|
|
elif i.status == 2: # expired
|
|
|
|
|
status = TxStatus.FAILED
|
|
|
|
|
|
|
|
|
|
return cls(
|
|
|
|
|
id=i.bolt11,
|
|
|
|
|
category=TxCategory.LIGHTNING,
|
|
|
|
|
type=TxType.RECEIVE,
|
|
|
|
|
amount=amount,
|
|
|
|
|
time_stamp=time_stamp,
|
|
|
|
|
comment=i.description,
|
|
|
|
|
status=status,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2023-04-02 20:27:05 +02:00
|
|
|
def from_cln_grpc_payment(
|
|
|
|
|
cls, payment, comment: str = "", amount: Union[None, int] = None
|
|
|
|
|
) -> "GenericTx":
|
2022-06-04 14:10:44 +02:00
|
|
|
status = TxStatus.UNKNOWN
|
2023-04-02 20:27:05 +02:00
|
|
|
fees = 0
|
|
|
|
|
|
2022-06-04 14:10:44 +02:00
|
|
|
if payment.status == 0: # pending
|
|
|
|
|
status = TxStatus.IN_FLIGHT
|
|
|
|
|
elif payment.status == 1: # failed
|
|
|
|
|
status = TxStatus.FAILED
|
|
|
|
|
elif payment.status == 2: # complete
|
|
|
|
|
status = TxStatus.SUCCEEDED
|
2023-04-02 20:27:05 +02:00
|
|
|
fees = payment.amount_sent_msat.msat - payment.amount_msat.msat
|
2022-06-04 14:10:44 +02:00
|
|
|
|
|
|
|
|
return cls(
|
|
|
|
|
id=payment.bolt11,
|
|
|
|
|
category=TxCategory.LIGHTNING,
|
|
|
|
|
type=TxType.SEND,
|
|
|
|
|
time_stamp=payment.created_at,
|
2023-04-02 20:27:05 +02:00
|
|
|
amount=-payment.amount_msat.msat if amount is None else -amount,
|
2022-06-04 14:10:44 +02:00
|
|
|
status=status,
|
2024-02-18 12:11:15 +01:00
|
|
|
total_fees=(
|
|
|
|
|
payment.amount_sent_msat.msat - payment.amount_msat.msat
|
|
|
|
|
if fees is None
|
|
|
|
|
else fees
|
|
|
|
|
),
|
2022-06-04 14:10:44 +02:00
|
|
|
comment=comment,
|
|
|
|
|
)
|