rgb-cln/contrib/pyln-testing/pyln/testing/grpc.py

289 lines
9.9 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.testing import node_pb2 as pb
from pyln.testing import node_pb2_grpc as pbgrpc
from pyln.testing import primitives_pb2 as primpb
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) -> primpb.Amount:
return primpb.Amount(msat=amount)
def int2amount_or_all(amount: Tuple[int, str]) -> primpb.AmountOrAll:
if amount == "all":
return primpb.AmountOrAll(all=True)
else:
assert isinstance(amount, int)
return primpb.AmountOrAll(amount=int2msat(amount))
def int2amount_or_any(amount: Tuple[int, str]) -> primpb.AmountOrAny:
if amount == "any":
return primpb.AmountOrAny(any=True)
else:
assert isinstance(amount, int)
return primpb.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 = pbgrpc.NodeStub(self.channel)
def getinfo(self):
return grpc2py.getinfo2py(self.stub.Getinfo(pb.GetinfoRequest()))
def connect(self, peer_id, host=None, port=None):
"""
Connect to {peer_id} at {host} and {port}.
"""
payload = pb.ConnectRequest(id=peer_id, host=host, port=port)
return grpc2py.connect2py(self.stub.ConnectPeer(payload))
def listpeers(self, peerid=None, level=None):
payload = pb.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 = pb.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 = pb.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 = pb.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 = pb.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 = pb.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 = pb.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 = pb.StopRequest()
try:
self.stub.Stop(payload)
except Exception:
pass
def listnodes(self, node_id=None):
payload = pb.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 = pb.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 = pb.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))