class CLightningWallet(Wallet): def __init__(self): self.l1 = LightningRpc(getenv("CLIGHTNING_RPC")) def create_invoice(self, amount: int, memo: str = "") -> InvoiceResponse: label = "lbl{}".format(random.random()) r = self.l1.invoice(amount*1000, label, memo, exposeprivatechannels=True) ok, checking_id, payment_request, error_message = True, r["payment_hash"], r["bolt11"], None return InvoiceResponse(ok, checking_id, payment_request, error_message) def pay_invoice(self, bolt11: str) -> PaymentResponse: r = self.l1.pay(bolt11) ok, checking_id, fee_msat, error_message = True, None, None, None return PaymentResponse(ok, checking_id, fee_msat, error_message) def get_invoice_status(self, checking_id: str) -> PaymentStatus: r = self.l1.listinvoices(checking_id) if r['invoices'][0]['status'] == 'unpaid': return PaymentStatus(False) return PaymentStatus(True) def get_payment_status(self, checking_id: str) -> PaymentStatus: r = self.l1.listsendpays(checking_id) if not r.ok: return PaymentStatus(r, None) payments = [p for p in r.json()["payments"] if p["payment_hash"] == payment_hash] payment = payments[0] if payments else None statuses = {"UNKNOWN": None, "IN_FLIGHT": None, "SUCCEEDED": True, "FAILED": False} return PaymentStatus(statuses[payment["status"]] if payment else None)
class CLightningWallet(Wallet): def __init__(self): if LightningRpc is None: # pragma: nocover raise ImportError( "The `pylightning` library must be installed to use `CLightningWallet`." ) self.l1 = LightningRpc(getenv("CLIGHTNING_RPC")) def create_invoice(self, amount: int, memo: str = "", description_hash: bytes = b"") -> InvoiceResponse: if description_hash: raise Unsupported("description_hash") label = "lbl{}".format(random.random()) r = self.l1.invoice(amount * 1000, label, memo, exposeprivatechannels=True) ok, checking_id, payment_request, error_message = True, r[ "payment_hash"], r["bolt11"], None return InvoiceResponse(ok, checking_id, payment_request, error_message) def pay_invoice(self, bolt11: str) -> PaymentResponse: r = self.l1.pay(bolt11) ok, checking_id, fee_msat, error_message = True, r[ "payment_hash"], r["msatoshi_sent"] - r["msatoshi"], None return PaymentResponse(ok, checking_id, fee_msat, error_message) def get_invoice_status(self, checking_id: str) -> PaymentStatus: r = self.l1.listinvoices(checking_id) if not r["invoices"]: return PaymentStatus(False) if r["invoices"][0]["label"] == checking_id: return PaymentStatus(r["pays"][0]["status"] == "paid") raise KeyError("supplied an invalid checking_id") def get_payment_status(self, checking_id: str) -> PaymentStatus: r = self.l1.listpays(payment_hash=checking_id) if not r["pays"]: return PaymentStatus(False) if r["pays"][0]["payment_hash"] == checking_id: status = r["pays"][0]["status"] if status == "complete": return PaymentStatus(True) elif status == "failed": return PaymentStatus(False) return PaymentStatus(None) raise KeyError("supplied an invalid checking_id")
class LightningNode(object): displayName = 'lightning' def __init__(self, lightning_dir, lightning_port, btc, executor=None, node_id=0): self.bitcoin = btc self.executor = executor self.daemon = LightningD(lightning_dir, self.bitcoin, port=lightning_port) socket_path = os.path.join(lightning_dir, "lightning-rpc").format(node_id) self.invoice_count = 0 self.logger = logging.getLogger( 'lightning-node({})'.format(lightning_port)) self.rpc = LightningRpc(socket_path, self.executor) orig_call = self.rpc._call def rpc_call(method, args): self.logger.debug("Calling {} with arguments {}".format( method, json.dumps(args, indent=4, sort_keys=True))) r = orig_call(method, args) self.logger.debug("Call returned {}".format( json.dumps(r, indent=4, sort_keys=True))) return r self.rpc._call = rpc_call self.myid = None def peers(self): return [p['id'] for p in self.rpc.listpeers()['peers']] def getinfo(self): if not self.info: self.info = self.rpc.getinfo() return self.info def id(self): if not self.myid: self.myid = self.rpc.getinfo()['id'] return self.myid def openchannel(self, node_id, host, port, satoshis): # Make sure we have a connection already if node_id not in self.peers(): raise ValueError("Must connect to node before opening a channel") return self.rpc.fundchannel(node_id, satoshis) def getaddress(self): return self.rpc.newaddr()['address'] def addfunds(self, bitcoind, satoshis): addr = self.getaddress() txid = bitcoind.rpc.sendtoaddress(addr, float(satoshis) / 10**8) bitcoind.rpc.getrawtransaction(txid) while len(self.rpc.listfunds()['outputs']) == 0: time.sleep(1) bitcoind.rpc.generate(1) def ping(self): """ Simple liveness test to see if the node is up and running Returns true if the node is reachable via RPC, false otherwise. """ try: self.rpc.help() return True except: return False def check_channel(self, remote, require_both=False): """Make sure that we have an active channel with remote `require_both` must be False unless the other side supports sending a `channel_announcement` and `channel_update` on `funding_locked`. This doesn't work for eclair for example. """ remote_id = remote.id() self_id = self.id() peer = None for p in self.rpc.listpeers()['peers']: if remote.id() == p['id']: peer = p break if not peer: self.logger.debug('Peer {} not found in listpeers'.format(remote)) return False if len(peer['channels']) < 1: self.logger.debug( 'Peer {} has no channel open with us'.format(remote)) return False state = p['channels'][0]['state'] self.logger.debug("Channel {} -> {} state: {}".format( self_id, remote_id, state)) if state != 'CHANNELD_NORMAL' or not p['connected']: self.logger.debug( 'Channel with peer {} is not in state normal ({}) or peer is not connected ({})' .format(remote_id, state, p['connected'])) return False # Make sure that gossipd sees a local channel_update for routing scid = p['channels'][0]['short_channel_id'] channels = self.rpc.listchannels(scid)['channels'] if not require_both and len(channels) >= 1: return channels[0]['active'] if len(channels) != 2: self.logger.debug( 'Waiting for both channel directions to be announced: 2 != {}'. format(len(channels))) return False return channels[0]['active'] and channels[1]['active'] def getchannels(self): result = [] for c in self.rpc.listchannels()['channels']: result.append((c['source'], c['destination'])) return set(result) def getnodes(self): return set([n['nodeid'] for n in self.rpc.listnodes()['nodes']]) def invoice(self, amount): invoice = self.rpc.invoice(amount, "invoice%d" % (self.invoice_count), "description") self.invoice_count += 1 return invoice['bolt11'] def send(self, req): result = self.rpc.pay(req) return result['payment_preimage'] def connect(self, host, port, node_id): return self.rpc.connect(node_id, host, port) def info(self): r = self.rpc.getinfo() return { 'id': r['id'], 'blockheight': r['blockheight'], } def block_sync(self, blockhash): time.sleep(1) def restart(self): self.daemon.stop() time.sleep(5) self.daemon.start() time.sleep(1) def stop(self): self.daemon.stop() def start(self): self.daemon.start() def check_route(self, node_id, amount): try: r = self.rpc.getroute(node_id, amount, 1.0) except ValueError as e: if (str(e).find("Could not find a route") > 0): return False raise return True
class CLightningWallet(Wallet): def __init__(self): if LightningRpc is None: # pragma: nocover raise ImportError( "The `pylightning` library must be installed to use `CLightningWallet`." ) self.rpc = getenv("CLIGHTNING_RPC") self.ln = LightningRpc(self.rpc) # check description_hash support (could be provided by a plugin) self.supports_description_hash = False try: answer = self.ln.help("invoicewithdescriptionhash") if answer["help"][0]["command"].startswith( "invoicewithdescriptionhash msatoshi label description_hash", ): self.supports_description_hash = True except: pass # check last payindex so we can listen from that point on self.last_pay_index = 0 invoices = self.ln.listinvoices() for inv in invoices["invoices"][::-1]: if "pay_index" in inv: self.last_pay_index = inv["pay_index"] break def create_invoice( self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None) -> InvoiceResponse: label = "lbl{}".format(random.random()) msat = amount * 1000 try: if description_hash: if not self.supports_description_hash: raise Unsupported("description_hash") params = [msat, label, description_hash.hex()] r = self.ln.call("invoicewithdescriptionhash", params) return InvoiceResponse(True, label, r["bolt11"], "") else: r = self.ln.invoice(msat, label, memo, exposeprivatechannels=True) return InvoiceResponse(True, label, r["bolt11"], "") except RpcError as exc: error_message = f"lightningd '{exc.method}' failed with '{exc.error}'." return InvoiceResponse(False, label, None, error_message) def pay_invoice(self, bolt11: str) -> PaymentResponse: r = self.ln.pay(bolt11) return PaymentResponse(True, r["payment_hash"], r["msatoshi_sent"] - r["msatoshi"], None) def get_invoice_status(self, checking_id: str) -> PaymentStatus: r = self.ln.listinvoices(checking_id) if not r["invoices"]: return PaymentStatus(False) if r["invoices"][0]["label"] == checking_id: return PaymentStatus(r["invoices"][0]["status"] == "paid") raise KeyError("supplied an invalid checking_id") def get_payment_status(self, checking_id: str) -> PaymentStatus: r = self.ln.call("listpays", {"payment_hash": checking_id}) if not r["pays"]: return PaymentStatus(False) if r["pays"][0]["payment_hash"] == checking_id: status = r["pays"][0]["status"] if status == "complete": return PaymentStatus(True) elif status == "failed": return PaymentStatus(False) return PaymentStatus(None) raise KeyError("supplied an invalid checking_id") async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: stream = await trio.open_unix_socket(self.rpc) i = 0 while True: call = json.dumps({ "method": "waitanyinvoice", "id": 0, "params": [self.last_pay_index], }) await stream.send_all(call.encode("utf-8")) data = await stream.receive_some() paid = json.loads(data.decode("ascii")) paid = self.ln.waitanyinvoice(self.last_pay_index) self.last_pay_index = paid["pay_index"] yield paid["label"] i += 1
class LightningNode(object): def __init__(self, lightning_dir, lightning_port, btc, executor=None, node_id=0): self.bitcoin = btc self.executor = executor self.daemon = LightningD(lightning_dir, btc.bitcoin_dir, port=lightning_port) socket_path = os.path.join(lightning_dir, "lightning-rpc").format(node_id) self.invoice_count = 0 self.rpc = LightningRpc(socket_path, self.executor) self.logger = logging.getLogger( 'lightning-node({})'.format(lightning_port)) def peers(self): return [p['peerid'] for p in self.rpc.getpeers()['peers']] def id(self): return self.rpc.getinfo()['id'] def openchannel(self, node_id, host, port, satoshis): # Make sure we have a connection already if node_id not in self.peers(): raise ValueError("Must connect to node before opening a channel") return self.rpc.fundchannel(node_id, satoshis) def getaddress(self): return self.rpc.newaddr()['address'] def addfunds(self, bitcoind, satoshis): addr = self.getaddress() txid = bitcoind.rpc.sendtoaddress(addr, float(satoshis) / 10**8) tx = bitcoind.rpc.getrawtransaction(txid) self.rpc.addfunds(tx) def ping(self): """ Simple liveness test to see if the node is up and running Returns true if the node is reachable via RPC, false otherwise. """ try: self.rpc.help() return True except: return False def check_channel(self, remote): """ Make sure that we have an active channel with remote """ remote_id = remote.id() self_id = self.id() for p in self.rpc.getpeers()['peers']: if remote.id() == p['peerid']: self.logger.debug("Channel {} -> {} state: {}".format( self_id, remote_id, p['state'])) return p['state'] == 'CHANNELD_NORMAL' self.logger.warning("Channel {} -> {} not found".format( self_id, remote_id)) return False def getchannels(self): result = [] for c in self.rpc.getchannels()['channels']: result.append((c['source'], c['destination'])) return set(result) def getnodes(self): return set([n['nodeid'] for n in self.rpc.getnodes()['nodes']]) def invoice(self, amount): invoice = self.rpc.invoice(amount, "invoice%d" % (self.invoice_count), "description") self.invoice_count += 1 print(invoice) return invoice['bolt11'] def send(self, req): result = self.rpc.pay(req) return result['preimage'] def connect(self, host, port, node_id): return self.rpc.connect(node_id, "{}:{}".format(host, port)) def info(self): r = self.rpc.getinfo() return { 'id': r['id'], 'blockheight': r['blockheight'], }
class CLightning(LnNode): def __init__(self): super().__init__() self.lnrpc = '' self.rpc_file = '/tmp/lightningrpc' def setup(self, ipaddr='127.0.0.1', port=9735, argv=None): self.ipaddr = ipaddr self.port = port if argv is not None: self.rpc_file = argv self.lnrpc = LightningRpc(self.rpc_file) def get_name(self): return 'c-lightning' ''' enum channel_state { /* In channeld, still waiting for lockin. */ CHANNELD_AWAITING_LOCKIN = 2, /* Normal operating state. */ CHANNELD_NORMAL, /* We are closing, pending HTLC resolution. */ CHANNELD_SHUTTING_DOWN, /* Exchanging signatures on closing tx. */ CLOSINGD_SIGEXCHANGE, /* Waiting for onchain event. */ CLOSINGD_COMPLETE, /* Waiting for unilateral close to hit blockchain. */ AWAITING_UNILATERAL, /* We've seen the funding spent, we're waiting for onchaind. */ FUNDING_SPEND_SEEN, /* On chain */ ONCHAIN }; ''' def get_status(self, peer): channel_sat = 0 try: result = self.lnrpc.listpeers() if ('peers' not in result) or (len(result['peers']) == 0): print('(status=none)') return LnNode.Status.NONE, 0 peer_status = '' current_ch = None for p in result['peers']: if p['id'] == peer: for ch in p['channels']: if ch['state'] != 'ONCHAIN': # onchainなものは「済」と判断して無視する current_ch = ch peer_status = ch['state'] break print('(status=', peer_status + ')') if peer_status == 'CHANNELD_NORMAL': status = LnNode.Status.NORMAL channel_sat = current_ch['msatoshi_to_us'] elif peer_status == 'CHANNELD_AWAITING_LOCKIN': status = LnNode.Status.FUNDING elif peer_status == 'CHANNELD_SHUTTING_DOWN' or\ peer_status == 'CLOSINGD_SIGEXCHANGE' or\ peer_status == 'CLOSINGD_COMPLETE' or\ peer_status == 'AWAITING_UNILATERAL' or\ peer_status == 'FUNDING_SPEND_SEEN': status = LnNode.Status.CLOSING else: status = LnNode.Status.NONE except: print('traceback.format_exc():\n%s' % traceback.format_exc()) os.kill(os.getpid(), signal.SIGKILL) return status, channel_sat def get_nodeid(self): try: info = self.lnrpc.getinfo() return info['id'] except: print('traceback.format_exc():\n%s' % traceback.format_exc()) os.kill(os.getpid(), signal.SIGKILL) # result[1] = "OK" or "NG" def connect(self, node_id, ipaddr, port): try: res = self.lnrpc.connect(node_id, ipaddr, port) print('connect=', res) res = '{"result": ["connect","OK","' + node_id + '"]}' except RpcError as e: print('traceback.format_exc():\n%s' % traceback.format_exc()) print('fail connect') res = '{"result": ["connect","NG","' + node_id + '","' + e.error[ 'message'] + '"]}' except: print('traceback.format_exc():\n%s' % traceback.format_exc()) print('fail connect') res = '{"result": ["connect","NG","' + node_id + '"]}' return res # result[1] = "OK" or "NG" def disconnect(self, node_id): print('disconnect: ' + node_id) try: res = self.lnrpc.disconnect(node_id, force=True) print('disconnect=', res) res = '{"result": ["disconnect","OK","' + node_id + '"]}' except RpcError as e: print('traceback.format_exc():\n%s' % traceback.format_exc()) print('fail disconnect') res = '{"result": ["disconnect","NG","' + node_id + '","' + e.error[ 'message'] + '"]}' except: print('traceback.format_exc():\n%s' % traceback.format_exc()) print('fail disconnect') res = '{"result": ["disconnect","NG","' + node_id + '"]}' return res # result[1] = "OK" or "NG" def open_channel(self, node_id, amount): try: res = self.lnrpc.fundchannel(node_id, amount) print('open_channel=', res) res = '{"result": ["openchannel","OK","' + node_id + '"]}' except RpcError as e: print('traceback.format_exc():\n%s' % traceback.format_exc()) print('fail openchannel') res = '{"result": ["openchannel","NG","' + node_id + '","' + e.error[ 'message'] + '"]}' except: print('traceback.format_exc():\n%s' % traceback.format_exc()) print('fail open_channel') res = '{"result": ["openchannel","NG","' + node_id + '"]}' return res # result[1] = BOLT11 or "NG" def get_invoice(self, amount_msat, label=''): try: res = self.lnrpc.invoice(amount_msat, "lbl{}".format(random.random()), "testpayment") print('invoice=', res) res = '{"result": ["invoice","' + res[ 'bolt11'] + '","' + label + '"]}' except RpcError as e: print('traceback.format_exc():\n%s' % traceback.format_exc()) print('fail invoice') res = '{"result": ["invoice","NG","' + label + '","' + e.error[ 'message'] + '"]}' except: print('traceback.format_exc():\n%s' % traceback.format_exc()) print('fail invoice') res = '{"result": ["invoice","NG","' + label + '"]}' return res # result[1] = "OK" or "NG" def pay(self, invoice): try: res = self.lnrpc.pay(invoice, riskfactor=100) print('pay=', res) res = '{"result": ["pay","OK","' + invoice + '"]}' except RpcError as e: print('traceback.format_exc():\n%s' % traceback.format_exc()) print('fail pay: ' + invoice) res = '{"result": ["pay","NG","' + invoice + '","' + e.error[ 'message'] + '"]}' except: print('traceback.format_exc():\n%s' % traceback.format_exc()) print('fail pay: ' + invoice) res = '{"result": ["pay","NG","' + invoice + '"]}' return res # result[1] = "OK" or "NG" def close_mutual(self, node_id): print('close_mutual') return self._close(node_id, False) # result[1] = "OK" or "NG" def close_force(self, node_id): print('close_force') self.disconnect(node_id) return self._close(node_id, True) # result[1] = "OK" or "NG" def _close(self, node_id, force): str_force = 'force' if force else 'mutual' try: res = self.lnrpc.close(node_id, force=force) print('close=', res) res = '{"result": ["closechannel","OK","' + node_id + '","' + str_force + '"]}' except RpcError as e: print('traceback.format_exc():\n%s' % traceback.format_exc()) print('fail closechannel') res = '{"result": ["closechannel","NG","' + node_id + '","' + e.error[ 'message'] + '"]}' except: print('traceback.format_exc():\n%s' % traceback.format_exc()) print('fail closechannel') res = '{"result": ["closechannel","NG","' + node_id + '","' + str_force + '"]}' return res