diff --git a/app/lightning/docs.py b/app/lightning/docs.py index a0be4b9..3438a4f 100644 --- a/app/lightning/docs.py +++ b/app/lightning/docs.py @@ -99,6 +99,17 @@ If neither __target_conf__, or __sat_per_vbyte__ are set, then the internal wall * If __sat_per_vbyte__ is set then __target_conf__ is ignored and vbytes (sipabytes) will be used. > 👉 See [https://lightning.readthedocs.io/lightning-txprepare.7.html](https://lightning.readthedocs.io/lightning-txprepare.7.html) + +### Sending all onchain funds +> â„šī¸ Keep the following points in mind when sending all onchain funds: + +* If __send_all__ is set to __true__, the __amount__ field must be set to __0__. +* If the __amount__ field is greater than __0__, the __send_all__ field must be __false__. + * The API will return an error if neither or both conditions are met at the same time. +* If __send_all__ is set to __true__ the amount of satoshis to send will be calculated by subtracting the fee from the wallet balance. +* If the wallet balance is not sufficient to cover the fee, the call will fail. +* The call will __not__ close any channels. +* The implementation may keep a reserve of funds if there are still open channels. """ send_payment_desc = """ diff --git a/app/lightning/impl/cln_grpc.py b/app/lightning/impl/cln_grpc.py index 56db620..6310633 100644 --- a/app/lightning/impl/cln_grpc.py +++ b/app/lightning/impl/cln_grpc.py @@ -16,6 +16,7 @@ import app.lightning.impl.protos.cln.node_pb2_grpc as clnrpc import app.lightning.impl.protos.cln.primitives_pb2 as lnp from app.api.utils import config_get_hex_str, next_push_id from app.bitcoind.utils import bitcoin_rpc_async +from app.api.utils import SSE, broadcast_sse_msg from app.lightning.impl.ln_base import LightningNodeBase from app.lightning.models import ( Channel, @@ -352,7 +353,7 @@ class LnNodeCLNgRPC(LightningNodeBase): # TODO: Improve this once CLN reports the block height in bkpr-listincome # see https://github.com/ElementsProject/lightning/issues/5694 - + # now get the block height for each tx ... res = await _make_local_call("bkpr-listaccountevents") if not res: @@ -567,7 +568,7 @@ class LnNodeCLNgRPC(LightningNodeBase): utxos.append(lnp.Outpoint(txid=o.txid, outnum=o.output)) max_amt += o.amount_msat.msat - if max_amt <= input.amount: + if not input.send_all and max_amt <= input.amount: raise HTTPException( status.HTTP_412_PRECONDITION_FAILED, detail=f"Could not afford {input.amount}sat. Not enough funds available", @@ -576,14 +577,17 @@ class LnNodeCLNgRPC(LightningNodeBase): req = ln.WithdrawRequest( destination=input.address, satoshi=lnp.AmountOrAll( - amount=lnp.Amount(msat=input.amount), all=False + amount=lnp.Amount(msat=input.amount), + all=input.send_all, ), minconf=input.min_confs, feerate=fee_rate, utxos=utxos, ) - res = await self._cln_stub.Withdraw(req) - return SendCoinsResponse.from_cln_grpc(res, input) + response = await self._cln_stub.Withdraw(req) + r = SendCoinsResponse.from_cln_grpc(response, input) + await broadcast_sse_msg(SSE.LN_ONCHAIN_PAYMENT_STATUS, r.dict()) + return r except grpc.aio._call.AioRpcError as error: details = error.details() if details and details.find("Could not parse destination address") > -1: diff --git a/app/lightning/impl/lnd_grpc.py b/app/lightning/impl/lnd_grpc.py index 759d074..afc23b8 100644 --- a/app/lightning/impl/lnd_grpc.py +++ b/app/lightning/impl/lnd_grpc.py @@ -8,6 +8,7 @@ from decouple import config as dconfig from fastapi.exceptions import HTTPException from starlette import status +import app.bitcoind.service as btc import app.lightning.impl.protos.lnd.lightning_pb2 as ln import app.lightning.impl.protos.lnd.lightning_pb2_grpc as lnrpc import app.lightning.impl.protos.lnd.router_pb2 as router @@ -503,14 +504,26 @@ This will show more debug information. sat_per_vbyte=input.sat_per_vbyte, min_confs=input.min_confs, label=input.label, + send_all=input.send_all, ) - response = await self._lnd_stub.SendCoins(r) - r = SendCoinsResponse.from_lnd_grpc(response, input) + bi = await btc.get_blockchain_info() + sendResponse = await self._lnd_stub.SendCoins(r) + txResponse = await self._lnd_stub.GetTransactions( + ln.GetTransactionsRequest(start_height=-1, end_height=bi.blocks) + ) + + tx = None + for t in txResponse.transactions: + if t.tx_hash == sendResponse.txid: + tx = t + break + + r = SendCoinsResponse.from_lnd_grpc(tx, input) await broadcast_sse_msg(SSE.LN_ONCHAIN_PAYMENT_STATUS, r.dict()) return r except grpc.aio._call.AioRpcError as error: - _check_if_locked() + _check_if_locked(error) details = error.details() if details and details.find("invalid bech32 string") > -1: raise HTTPException( diff --git a/app/lightning/models.py b/app/lightning/models.py index 1ebcab9..05e0c86 100644 --- a/app/lightning/models.py +++ b/app/lightning/models.py @@ -4,7 +4,7 @@ from typing import List, Optional, Union from deepdiff import DeepDiff from fastapi.param_functions import Query -from pydantic import BaseModel +from pydantic import BaseModel, validator from pydantic.types import conint import app.lightning.docs as docs @@ -1136,10 +1136,6 @@ class SendCoinsInput(BaseModel): ..., description="The base58 or bech32 encoded bitcoin address to send coins to on-chain", ) - amount: conint(gt=0) = Query( - ..., - description="The number of bitcoin denominated in satoshis to send", - ) target_conf: int = Query( None, description="The number of blocks that the transaction *should* confirm in, will be used for fee estimation", @@ -1155,6 +1151,47 @@ class SendCoinsInput(BaseModel): label: str = Query( "", description="A label for the transaction. Ignored by CLN backend." ) + send_all: bool = Query( + False, + description="Send all available on-chain funds from the wallet. Will be executed `amount` is **0**", + ) + amount: conint(ge=0) = Query( + 0, + description="The number of bitcoin denominated in satoshis to send. Must not be set when `send_all` is true.", + ) + + @validator("amount", pre=True, always=True) + def check_amount_or_send_all(cls, amount, values): + if amount == None: + amount = 0 + + send_all = values.get("send_all") if "send_all" in values else False + + 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( + "Either amount or send_all must be set. Please review the documentation." + ) + + if amount > 0 and not send_all: + # amount is set and send_all is false + return amount + + if amount > 0 and send_all: + # amount is set and send_all is true + raise ValueError( + "Amount and send_all must not be set at the same time. Please review the documentation." + ) + + if amount == 0 and send_all: + # amount is not set and send_all is true + return amount + + # normally this should never be reached + raise ValueError(f"Unknown input.") class SendCoinsResponse(BaseModel): @@ -1167,16 +1204,22 @@ class SendCoinsResponse(BaseModel): ..., description="The number of bitcoin denominated in satoshis which where sent", ) + fees: conint(ge=0) = Query( + None, + description="The number of bitcoin denominated in satoshis which where paid as fees", + ) label: str = Query( "", description="The label used for the transaction. Ignored by CLN backend." ) @classmethod def from_lnd_grpc(cls, r, input: SendCoinsInput): + amount = input.amount if input.send_all == False else r.amount return cls( - txid=r.txid, + txid=r.tx_hash, address=input.address, - amount=input.amount, + amount=abs(amount), + fees=r.total_fees, label=input.label, )