mirror of
https://github.com/ElementsProject/lightning.git
synced 2025-02-23 15:00:34 +01:00
Having the grpc bindings in the pyln-testing package was always a bit strange, however it came with additional issues. Due to the way that protos are handled by protobuf, any name clash, independently of where we import the protos, would cause an import error. This usually happens in diamond-pattern dependencies, and so pull out the generated files into their own package that everyone else can rely on. Changelog-None
287 lines
9.8 KiB
Python
287 lines
9.8 KiB
Python
"""A drop-in replacement for the JSON-RPC LightningRpc
|
|
"""
|
|
|
|
import logging
|
|
from binascii import unhexlify
|
|
from typing import List, Optional, Tuple
|
|
|
|
import grpc
|
|
from pyln.testing import grpc2py
|
|
|
|
from pyln import grpc as clnpb
|
|
|
|
DUMMY_CA_PEM = b"""-----BEGIN CERTIFICATE-----
|
|
MIIBcTCCARigAwIBAgIJAJhah1bqO05cMAoGCCqGSM49BAMCMBYxFDASBgNVBAMM
|
|
C2NsbiBSb290IENBMCAXDTc1MDEwMTAwMDAwMFoYDzQwOTYwMTAxMDAwMDAwWjAW
|
|
MRQwEgYDVQQDDAtjbG4gUm9vdCBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA
|
|
BPF4JrGsOsksgsYM1NNdUdLESwOxkzyD75Rnj/g7sFEVYXewcmyB3MRGCBx2a3/7
|
|
ft2Xu2ED6WigajaHlnSvfUyjTTBLMBkGA1UdEQQSMBCCA2NsboIJbG9jYWxob3N0
|
|
MB0GA1UdDgQWBBRcTjvqVodamGirO6sX1rOR02LwXzAPBgNVHRMBAf8EBTADAQH/
|
|
MAoGCCqGSM49BAMCA0cAMEQCICDvV5iFw/nmJdl6rlEEGAdBdZqjxD0tV6U/FvuL
|
|
7PycAiASEMtsFtpfiUvxveBkOGt7AN32GP/Z75l+GhYXh7L1ig==
|
|
-----END CERTIFICATE-----"""
|
|
|
|
|
|
DUMMY_CA_KEY_PEM = b"""-----BEGIN PRIVATE KEY-----
|
|
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgqbU7LQsRcvmI5vE5
|
|
MBBNK3imhIU2jmAczgvLuBi/Ys+hRANCAATxeCaxrDrJLILGDNTTXVHSxEsDsZM8
|
|
g++UZ4/4O7BRFWF3sHJsgdzERggcdmt/+37dl7thA+looGo2h5Z0r31M
|
|
-----END PRIVATE KEY-----"""
|
|
|
|
|
|
DUMMY_CLIENT_KEY_PEM = b"""-----BEGIN PRIVATE KEY-----
|
|
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgIEdQyKso8PaD1kiz
|
|
xxFEcKiTvTg+bej4Nc/GqnXipcGhRANCAARGoUNSnWx1qgt4RiVG8tOMX1vpKvhr
|
|
OLcUJ92T++kIFZchZvcTXwnlNiTAQg3ukL+RYyG5Q1PaYrYRVlOtl1T0
|
|
-----END PRIVATE KEY-----"""
|
|
|
|
|
|
DUMMY_CLIENT_PEM = b"""-----BEGIN CERTIFICATE-----
|
|
MIIBRDCB7KADAgECAgkA8SsXq7IZfi8wCgYIKoZIzj0EAwIwFjEUMBIGA1UEAwwL
|
|
Y2xuIFJvb3QgQ0EwIBcNNzUwMTAxMDAwMDAwWhgPNDA5NjAxMDEwMDAwMDBaMBox
|
|
GDAWBgNVBAMMD2NsbiBncnBjIFNlcnZlcjBZMBMGByqGSM49AgEGCCqGSM49AwEH
|
|
A0IABEahQ1KdbHWqC3hGJUby04xfW+kq+Gs4txQn3ZP76QgVlyFm9xNfCeU2JMBC
|
|
De6Qv5FjIblDU9pithFWU62XVPSjHTAbMBkGA1UdEQQSMBCCA2NsboIJbG9jYWxo
|
|
b3N0MAoGCCqGSM49BAMCA0cAMEQCICTU/YAs35cb6DRdZNzO1YbEt77uEjcqMRca
|
|
Hh6kK99RAiAKOQOkGnoAICjBmBJeC/iC4/+hhhkWZtFgbC3Jg5JD0w==
|
|
-----END CERTIFICATE-----"""
|
|
|
|
|
|
def int2msat(amount: int) -> clnpb.Amount:
|
|
return clnpb.Amount(msat=amount)
|
|
|
|
|
|
def int2amount_or_all(amount: Tuple[int, str]) -> clnpb.AmountOrAll:
|
|
if amount == "all":
|
|
return clnpb.AmountOrAll(all=True)
|
|
else:
|
|
assert isinstance(amount, int)
|
|
return clnpb.AmountOrAll(amount=int2msat(amount))
|
|
|
|
|
|
def int2amount_or_any(amount: Tuple[int, str]) -> clnpb.AmountOrAny:
|
|
if amount == "any":
|
|
return clnpb.AmountOrAny(any=True)
|
|
else:
|
|
assert isinstance(amount, int)
|
|
return clnpb.AmountOrAny(amount=int2msat(amount))
|
|
|
|
|
|
class LightningGrpc(object):
|
|
def __init__(
|
|
self,
|
|
host: str,
|
|
port: int,
|
|
root_certificates: bytes = DUMMY_CA_PEM,
|
|
private_key: bytes = DUMMY_CLIENT_KEY_PEM,
|
|
certificate_chain: bytes = DUMMY_CLIENT_PEM,
|
|
):
|
|
self.logger = logging.getLogger("LightningGrpc")
|
|
self.credentials = grpc.ssl_channel_credentials(
|
|
root_certificates=root_certificates,
|
|
private_key=private_key,
|
|
certificate_chain=certificate_chain,
|
|
)
|
|
self.logger.debug(f"Connecting to grpc interface at {host}:{port}")
|
|
self.channel = grpc.secure_channel(
|
|
f"{host}:{port}",
|
|
self.credentials,
|
|
options=(("grpc.ssl_target_name_override", "cln"),),
|
|
)
|
|
self.stub = clnpb.NodeStub(self.channel)
|
|
|
|
def getinfo(self):
|
|
return grpc2py.getinfo2py(self.stub.Getinfo(clnpb.GetinfoRequest()))
|
|
|
|
def connect(self, peer_id, host=None, port=None):
|
|
"""
|
|
Connect to {peer_id} at {host} and {port}.
|
|
"""
|
|
payload = clnpb.ConnectRequest(id=peer_id, host=host, port=port)
|
|
return grpc2py.connect2py(self.stub.ConnectPeer(payload))
|
|
|
|
def listpeers(self, peerid=None, level=None):
|
|
payload = clnpb.ListpeersRequest(
|
|
id=unhexlify(peerid) if peerid is not None else None,
|
|
level=level,
|
|
)
|
|
return grpc2py.listpeers2py(self.stub.ListPeers(payload))
|
|
|
|
def getpeer(self, peer_id, level=None):
|
|
"""
|
|
Show peer with {peer_id}, if {level} is set, include {log}s.
|
|
"""
|
|
res = self.listpeers(peer_id, level)
|
|
return res.get("peers") and res["peers"][0] or None
|
|
|
|
def newaddr(self, addresstype=None):
|
|
"""Get a new address of type {addresstype} of the internal wallet."""
|
|
enum = {
|
|
None: 0,
|
|
"BECH32": 0,
|
|
"P2SH_SEGWIT": 1,
|
|
"P2SH-SEGWIT": 1,
|
|
"ALL": 2
|
|
}
|
|
if addresstype is not None:
|
|
addresstype = addresstype.upper()
|
|
atype = enum.get(addresstype, None)
|
|
if atype is None:
|
|
raise ValueError(
|
|
f"Unknown addresstype {addresstype}, known values are {enum.values()}"
|
|
)
|
|
|
|
payload = clnpb.NewaddrRequest(addresstype=atype)
|
|
res = grpc2py.newaddr2py(self.stub.NewAddr(payload))
|
|
|
|
# Need to remap the bloody spelling of p2sh-segwit to match
|
|
# addresstype.
|
|
if 'p2sh_segwit' in res:
|
|
res['p2sh-segwit'] = res['p2sh_segwit']
|
|
del res['p2sh_segwit']
|
|
return res
|
|
|
|
def listfunds(self, spent=None):
|
|
payload = clnpb.ListfundsRequest(spent=spent)
|
|
return grpc2py.listfunds2py(self.stub.ListFunds(payload))
|
|
|
|
def fundchannel(
|
|
self,
|
|
node_id: str,
|
|
amount: int,
|
|
# TODO map the following arguments
|
|
# feerate=None,
|
|
announce: Optional[bool] = True,
|
|
minconf: Optional[int] = None,
|
|
# utxos=None,
|
|
# push_msat=None,
|
|
close_to: Optional[str] = None,
|
|
# request_amt=None,
|
|
compact_lease: Optional[str] = None,
|
|
):
|
|
payload = clnpb.FundchannelRequest(
|
|
id=unhexlify(node_id),
|
|
amount=int2amount_or_all(amount * 1000), # This is satoshis after all
|
|
# TODO Parse and insert `feerate`
|
|
announce=announce,
|
|
utxos=None,
|
|
minconf=minconf,
|
|
close_to=close_to,
|
|
compact_lease=compact_lease,
|
|
)
|
|
return grpc2py.fundchannel2py(self.stub.FundChannel(payload))
|
|
|
|
def listchannels(self, short_channel_id=None, source=None, destination=None):
|
|
payload = clnpb.ListchannelsRequest(
|
|
short_channel_id=short_channel_id,
|
|
source=unhexlify(source) if source else None,
|
|
destination=unhexlify(destination) if destination else None,
|
|
)
|
|
return grpc2py.listchannels2py(self.stub.ListChannels(payload))
|
|
|
|
def pay(
|
|
self,
|
|
bolt11: str,
|
|
amount_msat: Optional[int] = None,
|
|
label: Optional[str] = None,
|
|
riskfactor: Optional[float] = None,
|
|
maxfeepercent: Optional[float] = None,
|
|
retry_for: Optional[int] = None,
|
|
maxdelay: Optional[int] = None,
|
|
exemptfee: Optional[int] = None,
|
|
localofferid: Optional[str] = None,
|
|
# TODO map the following arguments
|
|
# exclude: Optional[List[str]] = None,
|
|
# maxfee=None,
|
|
description: Optional[str] = None,
|
|
msatoshi: Optional[int] = None,
|
|
):
|
|
payload = clnpb.PayRequest(
|
|
bolt11=bolt11,
|
|
amount_msat=int2msat(amount_msat),
|
|
label=label,
|
|
riskfactor=riskfactor,
|
|
maxfeepercent=maxfeepercent,
|
|
retry_for=retry_for,
|
|
maxdelay=maxdelay,
|
|
exemptfee=exemptfee,
|
|
localofferid=localofferid,
|
|
# Needs conversion
|
|
# exclude=exclude,
|
|
# maxfee=maxfee
|
|
description=description,
|
|
)
|
|
return grpc2py.pay2py(self.stub.Pay(payload))
|
|
|
|
def invoice(
|
|
self,
|
|
amount_msat: Optional[int] = None,
|
|
label: str = None,
|
|
description: str = None,
|
|
expiry: Optional[int] = None,
|
|
fallbacks: Optional[List[str]] = None,
|
|
preimage: Optional[str] = None,
|
|
exposeprivatechannels: Optional[bool] = None,
|
|
cltv: Optional[int] = None,
|
|
deschashonly: Optional[bool] = None,
|
|
# msatoshi=None
|
|
):
|
|
payload = clnpb.InvoiceRequest(
|
|
amount_msat=int2amount_or_any(amount_msat),
|
|
label=label,
|
|
description=description,
|
|
expiry=expiry,
|
|
fallbacks=fallbacks,
|
|
preimage=unhexlify(preimage) if preimage else None,
|
|
exposeprivatechannels=exposeprivatechannels,
|
|
cltv=cltv,
|
|
deschashonly=deschashonly,
|
|
)
|
|
return grpc2py.invoice2py(self.stub.Invoice(payload))
|
|
|
|
def stop(self):
|
|
payload = clnpb.StopRequest()
|
|
try:
|
|
self.stub.Stop(payload)
|
|
except Exception:
|
|
pass
|
|
|
|
def listnodes(self, node_id=None):
|
|
payload = clnpb.ListnodesRequest(id=unhexlify(node_id) if node_id else None)
|
|
return grpc2py.listnodes2py(self.stub.ListNodes(payload))
|
|
|
|
def close(
|
|
self,
|
|
peer_id: str,
|
|
unilateraltimeout: Optional[int] = None,
|
|
destination: Optional[str] = None,
|
|
fee_negotiation_step: Optional[str] = None,
|
|
force_lease_closed: Optional[bool] = None,
|
|
# TODO: not mapped yet
|
|
# feerange: Optional[List[str]]=None
|
|
):
|
|
payload = clnpb.CloseRequest(
|
|
id=peer_id,
|
|
unilateraltimeout=unilateraltimeout,
|
|
destination=destination,
|
|
fee_negotiation_step=fee_negotiation_step,
|
|
# wrong_funding,
|
|
force_lease_closed=force_lease_closed,
|
|
# feerange,
|
|
)
|
|
return grpc2py.close2py(self.stub.Close(payload))
|
|
|
|
def listinvoices(
|
|
self,
|
|
label=None,
|
|
payment_hash=None,
|
|
invstring=None,
|
|
offer_id=None
|
|
):
|
|
payload = clnpb.ListinvoicesRequest(
|
|
label=label,
|
|
invstring=invstring,
|
|
payment_hash=unhexlify(payment_hash) if payment_hash else None,
|
|
offer_id=offer_id,
|
|
)
|
|
return grpc2py.listinvoices2py(self.stub.ListInvoices(payload))
|