Skip to content

Commit

Permalink
feat: implement send all onchain funds flag
Browse files Browse the repository at this point in the history
  • Loading branch information
fusion44 committed Nov 28, 2022
1 parent b9f4577 commit 86442dd
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 15 deletions.
11 changes: 11 additions & 0 deletions app/lightning/docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,17 @@
* 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 = """
Expand Down
14 changes: 9 additions & 5 deletions app/lightning/impl/cln_grpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
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,
Expand Down Expand Up @@ -352,7 +353,7 @@ async def list_on_chain_tx(self) -> List[OnChainTransaction]:

# 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:
Expand Down Expand Up @@ -567,7 +568,7 @@ async def send_coins(self, input: SendCoinsInput) -> SendCoinsResponse:
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",
Expand All @@ -576,14 +577,17 @@ async def send_coins(self, input: SendCoinsInput) -> SendCoinsResponse:
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:
Expand Down
19 changes: 16 additions & 3 deletions app/lightning/impl/lnd_grpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
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
Expand Down Expand Up @@ -503,14 +504,26 @@ async def send_coins(self, input: SendCoinsInput) -> SendCoinsResponse:
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(
Expand Down
57 changes: 50 additions & 7 deletions app/lightning/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

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
Expand Down Expand Up @@ -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",
Expand All @@ -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):
Expand All @@ -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,
)

Expand Down

0 comments on commit 86442dd

Please sign in to comment.