mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-23 17:47:31 +00:00
lightning: march 2018 rebase, without integration
This commit is contained in:
parent
9c454726f4
commit
ad5aac1383
8 changed files with 1230 additions and 0 deletions
|
@ -449,6 +449,12 @@ BoxLayout:
|
||||||
ActionOvrButton:
|
ActionOvrButton:
|
||||||
name: 'network'
|
name: 'network'
|
||||||
text: _('Network')
|
text: _('Network')
|
||||||
|
ActionOvrButton:
|
||||||
|
name: 'lightning_payer_dialog'
|
||||||
|
text: _('Pay Lightning Invoice')
|
||||||
|
ActionOvrButton:
|
||||||
|
name: 'lightning_channels_dialog'
|
||||||
|
text: _('Lightning Channels')
|
||||||
ActionOvrButton:
|
ActionOvrButton:
|
||||||
name: 'settings'
|
name: 'settings'
|
||||||
text: _('Settings')
|
text: _('Settings')
|
||||||
|
|
|
@ -75,6 +75,8 @@ from electrum.util import (base_units, NoDynamicFeeEstimates, decimal_point_to_b
|
||||||
base_unit_name_to_decimal_point, NotEnoughFunds, UnknownBaseUnit,
|
base_unit_name_to_decimal_point, NotEnoughFunds, UnknownBaseUnit,
|
||||||
DECIMAL_POINT_DEFAULT)
|
DECIMAL_POINT_DEFAULT)
|
||||||
|
|
||||||
|
from .uix.dialogs.lightning_payer import LightningPayerDialog
|
||||||
|
from .uix.dialogs.lightning_channels import LightningChannelsDialog
|
||||||
|
|
||||||
class ElectrumWindow(App):
|
class ElectrumWindow(App):
|
||||||
|
|
||||||
|
@ -635,6 +637,14 @@ class ElectrumWindow(App):
|
||||||
self._settings_dialog.update()
|
self._settings_dialog.update()
|
||||||
self._settings_dialog.open()
|
self._settings_dialog.open()
|
||||||
|
|
||||||
|
def lightning_payer_dialog(self):
|
||||||
|
d = LightningPayerDialog(self)
|
||||||
|
d.open()
|
||||||
|
|
||||||
|
def lightning_channels_dialog(self):
|
||||||
|
d = LightningChannelsDialog(self)
|
||||||
|
d.open()
|
||||||
|
|
||||||
def popup_dialog(self, name):
|
def popup_dialog(self, name):
|
||||||
if name == 'settings':
|
if name == 'settings':
|
||||||
self.settings_dialog()
|
self.settings_dialog()
|
||||||
|
@ -652,6 +662,8 @@ class ElectrumWindow(App):
|
||||||
ref.data = xpub
|
ref.data = xpub
|
||||||
master_public_keys_layout.add_widget(ref)
|
master_public_keys_layout.add_widget(ref)
|
||||||
popup.open()
|
popup.open()
|
||||||
|
elif name.endswith("_dialog"):
|
||||||
|
getattr(self, name)()
|
||||||
else:
|
else:
|
||||||
popup = Builder.load_file('electrum/gui/kivy/uix/ui_screens/'+name+'.kv')
|
popup = Builder.load_file('electrum/gui/kivy/uix/ui_screens/'+name+'.kv')
|
||||||
popup.open()
|
popup.open()
|
||||||
|
|
49
gui/kivy/uix/dialogs/lightning_channels.py
Normal file
49
gui/kivy/uix/dialogs/lightning_channels.py
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
from kivy.lang import Builder
|
||||||
|
from kivy.factory import Factory
|
||||||
|
|
||||||
|
Builder.load_string('''
|
||||||
|
<LightningChannelItem@CardItem>
|
||||||
|
channelId: '<channelId not set>'
|
||||||
|
Label:
|
||||||
|
text: root.channelId
|
||||||
|
|
||||||
|
<LightningChannelsDialog@Popup>:
|
||||||
|
name: 'lightning_channels'
|
||||||
|
BoxLayout:
|
||||||
|
orientation: 'vertical'
|
||||||
|
spacing: '1dp'
|
||||||
|
ScrollView:
|
||||||
|
GridLayout:
|
||||||
|
cols: 1
|
||||||
|
id: lightning_channels_container
|
||||||
|
size_hint: 1, None
|
||||||
|
height: self.minimum_height
|
||||||
|
spacing: '2dp'
|
||||||
|
padding: '12dp'
|
||||||
|
''')
|
||||||
|
|
||||||
|
class LightningChannelsDialog(Factory.Popup):
|
||||||
|
def __init__(self, app):
|
||||||
|
super(LightningChannelsDialog, self).__init__()
|
||||||
|
self.clocks = []
|
||||||
|
self.app = app
|
||||||
|
def open(self, *args, **kwargs):
|
||||||
|
super(LightningChannelsDialog, self).open(*args, **kwargs)
|
||||||
|
for i in self.clocks: i.cancel()
|
||||||
|
self.clocks.append(Clock.schedule_interval(self.fetch_channels, 10))
|
||||||
|
self.app.wallet.lightning.subscribe(self.rpc_result_handler)
|
||||||
|
def dismiss(self, *args, **kwargs):
|
||||||
|
super(LightningChannelsDialog, self).dismiss(*args, **kwargs)
|
||||||
|
self.app.wallet.lightning.clearSubscribers()
|
||||||
|
def fetch_channels(self, dw):
|
||||||
|
lightning.lightningCall(self.app.wallet.lightning, "listchannels")()
|
||||||
|
def rpc_result_handler(self, res):
|
||||||
|
if isinstance(res, Exception):
|
||||||
|
raise res
|
||||||
|
channel_cards = self.ids.lightning_channels_container
|
||||||
|
channels_cards.clear_widgets()
|
||||||
|
for i in res["channels"]:
|
||||||
|
item = Factory.LightningChannelItem()
|
||||||
|
item.screen = self
|
||||||
|
item.channelId = i.channelId
|
||||||
|
channel_cards.add_widget(item)
|
68
gui/kivy/uix/dialogs/lightning_payer.py
Normal file
68
gui/kivy/uix/dialogs/lightning_payer.py
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
from kivy.lang import Builder
|
||||||
|
from kivy.factory import Factory
|
||||||
|
from electrum_gui.kivy.i18n import _
|
||||||
|
|
||||||
|
Builder.load_string('''
|
||||||
|
<LightningPayerDialog@Popup>
|
||||||
|
id: s
|
||||||
|
name: 'lightning_payer'
|
||||||
|
invoice_data: ''
|
||||||
|
BoxLayout:
|
||||||
|
orientation: "vertical"
|
||||||
|
BlueButton:
|
||||||
|
text: s.invoice_data if s.invoice_data else _('Lightning invoice')
|
||||||
|
shorten: True
|
||||||
|
on_release: Clock.schedule_once(lambda dt: app.show_info(_('Copy and paste the lightning invoice using the Paste button, or use the camera to scan a QR code.')))
|
||||||
|
GridLayout:
|
||||||
|
cols: 4
|
||||||
|
size_hint: 1, None
|
||||||
|
height: '48dp'
|
||||||
|
IconButton:
|
||||||
|
id: qr
|
||||||
|
on_release: Clock.schedule_once(lambda dt: app.scan_qr(on_complete=s.on_lightning_qr))
|
||||||
|
icon: 'atlas://gui/kivy/theming/light/camera'
|
||||||
|
Button:
|
||||||
|
text: _('Paste')
|
||||||
|
on_release: s.do_paste()
|
||||||
|
Button:
|
||||||
|
text: _('Paste sample')
|
||||||
|
on_release: s.do_paste_sample()
|
||||||
|
Button:
|
||||||
|
text: _('Clear')
|
||||||
|
on_release: s.do_clear()
|
||||||
|
Button:
|
||||||
|
size_hint: 1, None
|
||||||
|
height: '48dp'
|
||||||
|
text: _('Pay pasted/scanned invoice')
|
||||||
|
on_release: s.do_pay()
|
||||||
|
''')
|
||||||
|
|
||||||
|
class LightningPayerDialog(Factory.Popup):
|
||||||
|
def __init__(self, app):
|
||||||
|
super(LightningPayerDialog, self).__init__()
|
||||||
|
self.app = app
|
||||||
|
def open(self, *args, **kwargs):
|
||||||
|
super(LightningPayerDialog, self).open(*args, **kwargs)
|
||||||
|
class FakeQtSignal:
|
||||||
|
def emit(self2, data):
|
||||||
|
self.app.show_info(data)
|
||||||
|
class MyConsole:
|
||||||
|
newResult = FakeQtSignal()
|
||||||
|
self.app.wallet.lightning.setConsole(MyConsole())
|
||||||
|
def dismiss(self, *args, **kwargs):
|
||||||
|
super(LightningPayerDialog, self).dismiss(*args, **kwargs)
|
||||||
|
self.app.wallet.lightning.setConsole(None)
|
||||||
|
def do_paste_sample(self):
|
||||||
|
self.invoice_data = "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d73gafnh3cax9rn449d9p5uxz9ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ecky03ylcqca784w"
|
||||||
|
def do_paste(self):
|
||||||
|
contents = self.app._clipboard.paste()
|
||||||
|
if not contents:
|
||||||
|
self.app.show_info(_("Clipboard is empty"))
|
||||||
|
return
|
||||||
|
self.invoice_data = contents
|
||||||
|
def do_clear(self):
|
||||||
|
self.invoice_data = ""
|
||||||
|
def do_pay(self):
|
||||||
|
lightning.lightningCall(self.app.wallet.lightning, "sendpayment")("--pay_req=" + self.invoice_data)
|
||||||
|
def on_lightning_qr(self):
|
||||||
|
self.app.show_info("Lightning Invoice QR scanning not implemented") #TODO
|
147
gui/qt/lightning_invoice_list.py
Normal file
147
gui/qt/lightning_invoice_list.py
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import base64
|
||||||
|
import binascii
|
||||||
|
from PyQt5 import QtCore, QtWidgets
|
||||||
|
from collections import OrderedDict
|
||||||
|
import logging
|
||||||
|
from electrum.lightning import lightningCall
|
||||||
|
|
||||||
|
mapping = {0: "r_hash", 1: "pay_req", 2: "settled"}
|
||||||
|
revMapp = {"r_hash": 0, "pay_req": 1, "settled": 2}
|
||||||
|
datatable = OrderedDict([])
|
||||||
|
idx = 0
|
||||||
|
|
||||||
|
class MyTableRow(QtWidgets.QTreeWidgetItem):
|
||||||
|
def __init__(self, di):
|
||||||
|
if "settled" not in di:
|
||||||
|
di["settled"] = False
|
||||||
|
strs = [str(di[mapping[key]]) for key in range(len(mapping))]
|
||||||
|
print(strs)
|
||||||
|
super(MyTableRow, self).__init__(strs)
|
||||||
|
assert isinstance(di, dict)
|
||||||
|
self.di = di
|
||||||
|
def __getitem__(self, idx):
|
||||||
|
return self.di[idx]
|
||||||
|
def __setitem__(self, idx, val):
|
||||||
|
self.di[idx] = val
|
||||||
|
try:
|
||||||
|
self.setData(revMapp[idx], QtCore.Qt.DisplayRole, '{0}'.format(val))
|
||||||
|
except KeyError:
|
||||||
|
logging.warning("Lightning Invoice field %s unknown", idx)
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.di)
|
||||||
|
|
||||||
|
def addInvoiceRow(new):
|
||||||
|
made = MyTableRow(new)
|
||||||
|
datatable[new["r_hash"]] = made
|
||||||
|
datatable.move_to_end(new["r_hash"], last=False)
|
||||||
|
return made
|
||||||
|
|
||||||
|
def clickHandler(numInput, treeView, lightningRpc):
|
||||||
|
amt = numInput.value()
|
||||||
|
if amt < 1:
|
||||||
|
print("value too small")
|
||||||
|
return
|
||||||
|
print("creating invoice with value {}".format(amt))
|
||||||
|
global idx
|
||||||
|
#obj = {
|
||||||
|
# "r_hash": binascii.hexlify((int.from_bytes(bytearray.fromhex("9500edb0994b7bc23349193486b25c82097045db641f35fa988c0e849acdec29"), "big")+idx).to_bytes(byteorder="big", length=32)).decode("ascii"),
|
||||||
|
# "pay_req": "lntb81920n1pdf258s" + str(idx),
|
||||||
|
# "settled": False
|
||||||
|
#}
|
||||||
|
#treeView.insertTopLevelItem(0, addInvoiceRow(obj))
|
||||||
|
idx += 1
|
||||||
|
lightningCall(lightningRpc, "addinvoice")("--amt=" + str(amt))
|
||||||
|
|
||||||
|
class LightningInvoiceList(QtWidgets.QWidget):
|
||||||
|
def create_menu(self, position):
|
||||||
|
menu = QtWidgets.QMenu()
|
||||||
|
pay_req = self._tv.currentItem()["pay_req"]
|
||||||
|
cb = QtWidgets.QApplication.instance().clipboard()
|
||||||
|
def copy():
|
||||||
|
print(pay_req)
|
||||||
|
cb.setText(pay_req)
|
||||||
|
menu.addAction("Copy payment request", copy)
|
||||||
|
menu.exec_(self._tv.viewport().mapToGlobal(position))
|
||||||
|
def lightningWorkerHandler(self, sourceClassName, obj):
|
||||||
|
new = {}
|
||||||
|
for k, v in obj.items():
|
||||||
|
try:
|
||||||
|
v = binascii.hexlify(base64.b64decode(v)).decode("ascii")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
new[k] = v
|
||||||
|
try:
|
||||||
|
obj = datatable[new["r_hash"]]
|
||||||
|
except KeyError:
|
||||||
|
print("lightning payment invoice r_hash {} unknown!".format(new["r_hash"]))
|
||||||
|
else:
|
||||||
|
for k, v in new.items():
|
||||||
|
try:
|
||||||
|
if obj[k] != v: obj[k] = v
|
||||||
|
except KeyError:
|
||||||
|
obj[k] = v
|
||||||
|
def lightningRpcHandler(self, methodName, obj):
|
||||||
|
if methodName != "addinvoice":
|
||||||
|
print("ignoring reply {} to {}".format(obj, methodName))
|
||||||
|
return
|
||||||
|
self._tv.insertTopLevelItem(0, addInvoiceRow(obj))
|
||||||
|
|
||||||
|
def __init__(self, parent, lightningWorker, lightningRpc):
|
||||||
|
QtWidgets.QWidget.__init__(self, parent)
|
||||||
|
|
||||||
|
lightningWorker.subscribe(self.lightningWorkerHandler)
|
||||||
|
lightningRpc.subscribe(self.lightningRpcHandler)
|
||||||
|
|
||||||
|
self._tv=QtWidgets.QTreeWidget(self)
|
||||||
|
self._tv.setHeaderLabels([mapping[i] for i in range(len(mapping))])
|
||||||
|
self._tv.setColumnCount(len(mapping))
|
||||||
|
self._tv.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||||
|
self._tv.customContextMenuRequested.connect(self.create_menu)
|
||||||
|
|
||||||
|
class SatoshiCountSpinBox(QtWidgets.QSpinBox):
|
||||||
|
def keyPressEvent(self2, e):
|
||||||
|
super(SatoshiCountSpinBox, self2).keyPressEvent(e)
|
||||||
|
if QtCore.Qt.Key_Return == e.key():
|
||||||
|
clickHandler(self2, self._tv, lightningRpc)
|
||||||
|
|
||||||
|
numInput = SatoshiCountSpinBox(self)
|
||||||
|
|
||||||
|
button = QtWidgets.QPushButton('Add invoice', self)
|
||||||
|
button.clicked.connect(lambda: clickHandler(numInput, self._tv, lightningRpc))
|
||||||
|
|
||||||
|
l=QtWidgets.QVBoxLayout(self)
|
||||||
|
h=QtWidgets.QGridLayout(self)
|
||||||
|
h.addWidget(numInput, 0, 0)
|
||||||
|
h.addWidget(button, 0, 1)
|
||||||
|
#h.addItem(QtWidgets.QSpacerItem(100, 200, QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred), 0, 2)
|
||||||
|
#h.setSizePolicy(
|
||||||
|
h.setColumnStretch(0, 1)
|
||||||
|
h.setColumnStretch(1, 1)
|
||||||
|
h.setColumnStretch(2, 2)
|
||||||
|
l.addLayout(h)
|
||||||
|
l.addWidget(self._tv)
|
||||||
|
|
||||||
|
self.resize(2500,1000)
|
||||||
|
|
||||||
|
def tick():
|
||||||
|
key = "9500edb0994b7bc23349193486b25c82097045db641f35fa988c0e849acdec29"
|
||||||
|
if not key in datatable:
|
||||||
|
return
|
||||||
|
row = datatable[key]
|
||||||
|
row["settled"] = not row["settled"]
|
||||||
|
print("data changed")
|
||||||
|
|
||||||
|
if __name__=="__main__":
|
||||||
|
from sys import argv, exit
|
||||||
|
|
||||||
|
a=QtWidgets.QApplication(argv)
|
||||||
|
|
||||||
|
w=LightningInvoiceList()
|
||||||
|
w.show()
|
||||||
|
w.raise_()
|
||||||
|
|
||||||
|
timer = QtCore.QTimer()
|
||||||
|
timer.timeout.connect(tick)
|
||||||
|
timer.start(1000)
|
||||||
|
exit(a.exec_())
|
912
lib/lightning.py
Normal file
912
lib/lightning.py
Normal file
|
@ -0,0 +1,912 @@
|
||||||
|
import functools
|
||||||
|
import sys
|
||||||
|
import struct
|
||||||
|
import traceback
|
||||||
|
sys.path.insert(0, "lib/ln")
|
||||||
|
from .ln import rpc_pb2
|
||||||
|
|
||||||
|
from jsonrpclib import Server
|
||||||
|
from google.protobuf import json_format
|
||||||
|
import binascii
|
||||||
|
import ecdsa.util
|
||||||
|
import hashlib
|
||||||
|
from .bitcoin import EC_KEY, MySigningKey
|
||||||
|
from ecdsa.curves import SECP256k1
|
||||||
|
from . import bitcoin
|
||||||
|
from . import transaction
|
||||||
|
from . import keystore
|
||||||
|
|
||||||
|
import queue
|
||||||
|
|
||||||
|
from .util import ForeverCoroutineJob
|
||||||
|
|
||||||
|
import threading
|
||||||
|
import json
|
||||||
|
import base64
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from concurrent.futures import TimeoutError
|
||||||
|
|
||||||
|
WALLET = None
|
||||||
|
NETWORK = None
|
||||||
|
CONFIG = None
|
||||||
|
locked = set()
|
||||||
|
|
||||||
|
machine = "148.251.87.112"
|
||||||
|
#machine = "127.0.0.1"
|
||||||
|
|
||||||
|
def WriteDb(json):
|
||||||
|
req = rpc_pb2.WriteDbRequest()
|
||||||
|
json_format.Parse(json, req)
|
||||||
|
print("writedb unimplemented", req.dbData)
|
||||||
|
m = rpc_pb2.WriteDbResponse()
|
||||||
|
msg = json_format.MessageToJson(m)
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
def ConfirmedBalance(json):
|
||||||
|
request = rpc_pb2.ConfirmedBalanceRequest()
|
||||||
|
json_format.Parse(json, request)
|
||||||
|
m = rpc_pb2.ConfirmedBalanceResponse()
|
||||||
|
confs = request.confirmations
|
||||||
|
#witness = request.witness # bool
|
||||||
|
|
||||||
|
m.amount = sum(WALLET.get_balance())
|
||||||
|
msg = json_format.MessageToJson(m)
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
def NewAddress(json):
|
||||||
|
request = rpc_pb2.NewAddressRequest()
|
||||||
|
json_format.Parse(json, request)
|
||||||
|
m = rpc_pb2.NewAddressResponse()
|
||||||
|
if request.type == rpc_pb2.WITNESS_PUBKEY_HASH:
|
||||||
|
m.address = WALLET.get_unused_address()
|
||||||
|
elif request.type == rpc_pb2.NESTED_PUBKEY_HASH:
|
||||||
|
assert False, "cannot handle nested-pubkey-hash address type generation yet"
|
||||||
|
elif request.type == rpc_pb2.PUBKEY_HASH:
|
||||||
|
assert False, "cannot handle pubkey_hash generation yet"
|
||||||
|
else:
|
||||||
|
assert False, "unknown address type"
|
||||||
|
msg = json_format.MessageToJson(m)
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
#def FetchRootKey(json):
|
||||||
|
# request = rpc_pb2.FetchRootKeyRequest()
|
||||||
|
# json_format.Parse(json, request)
|
||||||
|
# m = rpc_pb2.FetchRootKeyResponse()
|
||||||
|
# m.rootKey = WALLET.keystore.get_private_key([151,151,151,151], None)[0]
|
||||||
|
# msg = json_format.MessageToJson(m)
|
||||||
|
# return msg
|
||||||
|
|
||||||
|
|
||||||
|
cl = rpc_pb2.ListUnspentWitnessRequest
|
||||||
|
|
||||||
|
assert rpc_pb2.WITNESS_PUBKEY_HASH is not None
|
||||||
|
|
||||||
|
|
||||||
|
def ListUnspentWitness(json):
|
||||||
|
req = cl()
|
||||||
|
json_format.Parse(json, req)
|
||||||
|
confs = req.minConfirmations #TODO regard this
|
||||||
|
|
||||||
|
unspent = WALLET.get_utxos()
|
||||||
|
m = rpc_pb2.ListUnspentWitnessResponse()
|
||||||
|
for utxo in unspent:
|
||||||
|
# print(utxo)
|
||||||
|
# example:
|
||||||
|
# {'prevout_n': 0,
|
||||||
|
# 'address': 'sb1qt52ccplvtpehz7qvvqft2udf2eaqvfsal08xre',
|
||||||
|
# 'prevout_hash': '0d4caccd6e8a906c8ca22badf597c4dedc6dd7839f3cac3137f8f29212099882',
|
||||||
|
# 'coinbase': False,
|
||||||
|
# 'height': 326,
|
||||||
|
# 'value': 400000000}
|
||||||
|
|
||||||
|
global locked
|
||||||
|
if (utxo["prevout_hash"], utxo["prevout_n"]) in locked:
|
||||||
|
print("SKIPPING LOCKED OUTPOINT", utxo["prevout_hash"])
|
||||||
|
continue
|
||||||
|
towire = m.utxos.add()
|
||||||
|
towire.addressType = rpc_pb2.WITNESS_PUBKEY_HASH
|
||||||
|
towire.redeemScript = b""
|
||||||
|
towire.pkScript = b""
|
||||||
|
towire.witnessScript = bytes(bytearray.fromhex(
|
||||||
|
bitcoin.address_to_script(utxo["address"])))
|
||||||
|
towire.value = utxo["value"]
|
||||||
|
towire.outPoint.hash = utxo["prevout_hash"]
|
||||||
|
towire.outPoint.index = utxo["prevout_n"]
|
||||||
|
return json_format.MessageToJson(m)
|
||||||
|
|
||||||
|
def LockOutpoint(json):
|
||||||
|
req = rpc_pb2.LockOutpointRequest()
|
||||||
|
json_format.Parse(json, req)
|
||||||
|
global locked
|
||||||
|
locked.add((req.outpoint.hash, req.outpoint.index))
|
||||||
|
|
||||||
|
|
||||||
|
def UnlockOutpoint(json):
|
||||||
|
req = rpc_pb2.UnlockOutpointRequest()
|
||||||
|
json_format.Parse(json, req)
|
||||||
|
global locked
|
||||||
|
# throws KeyError if not existing. Use .discard() if we do not care
|
||||||
|
locked.remove((req.outpoint.hash, req.outpoint.index))
|
||||||
|
|
||||||
|
def ListTransactionDetails(json):
|
||||||
|
global WALLET
|
||||||
|
global NETWORK
|
||||||
|
m = rpc_pb2.ListTransactionDetailsResponse()
|
||||||
|
for tx_hash, height, conf, timestamp, delta, balance in WALLET.get_history():
|
||||||
|
if height == 0:
|
||||||
|
print("WARNING", tx_hash, "has zero height!")
|
||||||
|
detail = m.details.add()
|
||||||
|
detail.hash = tx_hash
|
||||||
|
detail.value = delta
|
||||||
|
detail.numConfirmations = conf
|
||||||
|
detail.blockHash = NETWORK.blockchain().get_hash(height)
|
||||||
|
detail.blockHeight = height
|
||||||
|
detail.timestamp = timestamp
|
||||||
|
detail.totalFees = 1337 # TODO
|
||||||
|
return json_format.MessageToJson(m)
|
||||||
|
|
||||||
|
def FetchInputInfo(json):
|
||||||
|
req = rpc_pb2.FetchInputInfoRequest()
|
||||||
|
json_format.Parse(json, req)
|
||||||
|
has = req.outPoint.hash
|
||||||
|
idx = req.outPoint.index
|
||||||
|
txoinfo = WALLET.txo.get(has, {})
|
||||||
|
m = rpc_pb2.FetchInputInfoResponse()
|
||||||
|
if has in WALLET.transactions:
|
||||||
|
tx = WALLET.transactions[has]
|
||||||
|
m.mine = True
|
||||||
|
else:
|
||||||
|
tx = WALLET.get_input_tx(has)
|
||||||
|
print("did not find tx with hash", has)
|
||||||
|
print("tx", tx)
|
||||||
|
|
||||||
|
m.mine = False
|
||||||
|
return json_format.MessageToJson(m)
|
||||||
|
outputs = tx.outputs()
|
||||||
|
assert {bitcoin.TYPE_SCRIPT: "SCRIPT", bitcoin.TYPE_ADDRESS: "ADDRESS",
|
||||||
|
bitcoin.TYPE_PUBKEY: "PUBKEY"}[outputs[idx][0]] == "ADDRESS"
|
||||||
|
scr = transaction.Transaction.pay_script(outputs[idx][0], outputs[idx][1])
|
||||||
|
m.txOut.value = outputs[idx][2] # type, addr, val
|
||||||
|
m.txOut.pkScript = bytes(bytearray.fromhex(scr))
|
||||||
|
msg = json_format.MessageToJson(m)
|
||||||
|
return msg
|
||||||
|
|
||||||
|
def SendOutputs(json):
|
||||||
|
global NETWORK, WALLET, CONFIG
|
||||||
|
|
||||||
|
req = rpc_pb2.SendOutputsRequest()
|
||||||
|
json_format.Parse(json, req)
|
||||||
|
|
||||||
|
m = rpc_pb2.SendOutputsResponse()
|
||||||
|
|
||||||
|
elecOutputs = [(bitcoin.TYPE_SCRIPT, binascii.hexlify(txout.pkScript).decode("utf-8"), txout.value) for txout in req.outputs]
|
||||||
|
|
||||||
|
print("ignoring feeSatPerByte", req.feeSatPerByte) # TODO
|
||||||
|
|
||||||
|
tx = None
|
||||||
|
try:
|
||||||
|
# outputs, password, config, fee
|
||||||
|
tx = WALLET.mktx(elecOutputs, None, CONFIG, 1000)
|
||||||
|
except Exception as e:
|
||||||
|
m.success = False
|
||||||
|
m.error = str(e)
|
||||||
|
m.resultHash = ""
|
||||||
|
return json_format.MessageToJson(m)
|
||||||
|
|
||||||
|
suc, has = NETWORK.broadcast(tx)
|
||||||
|
if not suc:
|
||||||
|
m.success = False
|
||||||
|
m.error = "electrum/lightning/SendOutputs: Could not broadcast: " + str(has)
|
||||||
|
m.resultHash = ""
|
||||||
|
return json_format.MessageToJson(m)
|
||||||
|
m.success = True
|
||||||
|
m.error = ""
|
||||||
|
m.resultHash = tx.txid()
|
||||||
|
return json_format.MessageToJson(m)
|
||||||
|
|
||||||
|
def isSynced():
|
||||||
|
global NETWORK
|
||||||
|
local_height, server_height = NETWORK.get_status_value("updated")
|
||||||
|
synced = server_height != 0 and NETWORK.is_up_to_date() and local_height >= server_height
|
||||||
|
return synced, local_height, server_height
|
||||||
|
|
||||||
|
def IsSynced(json):
|
||||||
|
m = rpc_pb2.IsSyncedResponse()
|
||||||
|
m.synced, localHeight, _ = isSynced()
|
||||||
|
block = NETWORK.blockchain().read_header(localHeight)
|
||||||
|
m.lastBlockTimeStamp = block["timestamp"]
|
||||||
|
return json_format.MessageToJson(m)
|
||||||
|
|
||||||
|
def SignMessage(json):
|
||||||
|
req = rpc_pb2.SignMessageRequest()
|
||||||
|
json_format.Parse(json, req)
|
||||||
|
m = rpc_pb2.SignMessageResponse()
|
||||||
|
|
||||||
|
pri = privKeyForPubKey(req.pubKey)
|
||||||
|
|
||||||
|
m.signature = pri.sign(bitcoin.Hash(req.messageToBeSigned), ecdsa.util.sigencode_der)
|
||||||
|
m.error = ""
|
||||||
|
m.success = True
|
||||||
|
return json_format.MessageToJson(m)
|
||||||
|
|
||||||
|
def LEtobytes(x, l):
|
||||||
|
if l == 2:
|
||||||
|
fmt = "<H"
|
||||||
|
elif l == 4:
|
||||||
|
fmt = "<I"
|
||||||
|
elif l == 8:
|
||||||
|
fmt = "<Q"
|
||||||
|
else:
|
||||||
|
assert False, "invalid format for LEtobytes"
|
||||||
|
return struct.pack(fmt, x)
|
||||||
|
|
||||||
|
|
||||||
|
def toint(x):
|
||||||
|
if len(x) == 1:
|
||||||
|
return ord(x)
|
||||||
|
elif len(x) == 2:
|
||||||
|
fmt = ">H"
|
||||||
|
elif len(x) == 4:
|
||||||
|
fmt = ">I"
|
||||||
|
elif len(x) == 8:
|
||||||
|
fmt = ">Q"
|
||||||
|
else:
|
||||||
|
assert False, "invalid length for toint(): " + str(len(x))
|
||||||
|
return struct.unpack(fmt, x)[0]
|
||||||
|
|
||||||
|
class TxSigHashes(object):
|
||||||
|
def __init__(self, hashOutputs=None, hashSequence=None, hashPrevOuts=None):
|
||||||
|
self.hashOutputs = hashOutputs
|
||||||
|
self.hashSequence = hashSequence
|
||||||
|
self.hashPrevOuts = hashPrevOuts
|
||||||
|
|
||||||
|
|
||||||
|
class Output(object):
|
||||||
|
def __init__(self, value=None, pkScript=None):
|
||||||
|
assert value is not None and pkScript is not None
|
||||||
|
self.value = value
|
||||||
|
self.pkScript = pkScript
|
||||||
|
|
||||||
|
|
||||||
|
class InputScript(object):
|
||||||
|
def __init__(self, scriptSig, witness):
|
||||||
|
assert witness is None or type(witness[0]) is type(bytes([]))
|
||||||
|
assert type(scriptSig) is type(bytes([]))
|
||||||
|
self.scriptSig = scriptSig
|
||||||
|
self.witness = witness
|
||||||
|
|
||||||
|
|
||||||
|
def tweakPrivKey(basePriv, commitTweak):
|
||||||
|
tweakInt = int.from_bytes(commitTweak, byteorder="big")
|
||||||
|
tweakInt += basePriv.secret # D is secret
|
||||||
|
tweakInt %= SECP256k1.generator.order()
|
||||||
|
return EC_KEY(tweakInt.to_bytes(32, 'big'))
|
||||||
|
|
||||||
|
def singleTweakBytes(commitPoint, basePoint):
|
||||||
|
m = hashlib.sha256()
|
||||||
|
m.update(bytearray.fromhex(commitPoint))
|
||||||
|
m.update(bytearray.fromhex(basePoint))
|
||||||
|
return m.digest()
|
||||||
|
|
||||||
|
def deriveRevocationPrivKey(revokeBasePriv, commitSecret):
|
||||||
|
revokeTweakBytes = singleTweakBytes(revokeBasePriv.get_public_key(True),
|
||||||
|
commitSecret.get_public_key(True))
|
||||||
|
revokeTweakInt = int.from_bytes(revokeTweakBytes, byteorder="big")
|
||||||
|
|
||||||
|
commitTweakBytes = singleTweakBytes(commitSecret.get_public_key(True),
|
||||||
|
revokeBasePriv.get_public_key(True))
|
||||||
|
commitTweakInt = int.from_bytes(commitTweakBytes, byteorder="big")
|
||||||
|
|
||||||
|
revokeHalfPriv = revokeTweakInt * revokeBasePriv.secret # D is secret
|
||||||
|
commitHalfPriv = commitTweakInt * commitSecret.secret
|
||||||
|
|
||||||
|
revocationPriv = revokeHalfPriv + commitHalfPriv
|
||||||
|
revocationPriv %= SECP256k1.generator.order()
|
||||||
|
|
||||||
|
return EC_KEY(revocationPriv.to_bytes(32, byteorder="big"))
|
||||||
|
|
||||||
|
|
||||||
|
def maybeTweakPrivKey(signdesc, pri):
|
||||||
|
if len(signdesc.singleTweak) > 0:
|
||||||
|
pri2 = tweakPrivKey(pri, signdesc.singleTweak)
|
||||||
|
elif len(signdesc.doubleTweak) > 0:
|
||||||
|
pri2 = deriveRevocationPrivKey(pri, EC_KEY(signdesc.doubleTweak))
|
||||||
|
else:
|
||||||
|
pri2 = pri
|
||||||
|
|
||||||
|
if pri2 != pri:
|
||||||
|
have_keys = WALLET.storage.get("lightning_extra_keys", [])
|
||||||
|
if pri2.secret not in have_keys:
|
||||||
|
WALLET.storage.put("lightning_extra_keys", have_keys + [pri2.secret])
|
||||||
|
WALLET.storage.write()
|
||||||
|
print("saved new tweaked key", pri2.secret)
|
||||||
|
|
||||||
|
return pri2
|
||||||
|
|
||||||
|
|
||||||
|
def isWitnessPubKeyHash(script):
|
||||||
|
if len(script) != 2:
|
||||||
|
return False
|
||||||
|
haveop0 = (transaction.opcodes.OP_0 == script[0][0])
|
||||||
|
haveopdata20 = (20 == script[1][0])
|
||||||
|
return haveop0 and haveopdata20
|
||||||
|
|
||||||
|
#// calcWitnessSignatureHash computes the sighash digest of a transaction's
|
||||||
|
#// segwit input using the new, optimized digest calculation algorithm defined
|
||||||
|
#// in BIP0143: https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki.
|
||||||
|
#// This function makes use of pre-calculated sighash fragments stored within
|
||||||
|
#// the passed HashCache to eliminate duplicate hashing computations when
|
||||||
|
#// calculating the final digest, reducing the complexity from O(N^2) to O(N).
|
||||||
|
#// Additionally, signatures now cover the input value of the referenced unspent
|
||||||
|
#// output. This allows offline, or hardware wallets to compute the exact amount
|
||||||
|
#// being spent, in addition to the final transaction fee. In the case the
|
||||||
|
#// wallet if fed an invalid input amount, the real sighash will differ causing
|
||||||
|
#// the produced signature to be invalid.
|
||||||
|
|
||||||
|
|
||||||
|
def calcWitnessSignatureHash(original, sigHashes, hashType, tx, idx, amt):
|
||||||
|
assert len(original) != 0
|
||||||
|
decoded = transaction.deserialize(binascii.hexlify(tx).decode("utf-8"))
|
||||||
|
if idx > len(decoded["inputs"]) - 1:
|
||||||
|
raise Exception("invalid inputIndex")
|
||||||
|
txin = decoded["inputs"][idx]
|
||||||
|
#tohash = transaction.Transaction.serialize_witness(txin)
|
||||||
|
sigHash = LEtobytes(decoded["version"], 4)
|
||||||
|
if toint(hashType) & toint(sigHashAnyOneCanPay) == 0:
|
||||||
|
sigHash += bytes(bytearray.fromhex(sigHashes.hashPrevOuts))[::-1]
|
||||||
|
else:
|
||||||
|
sigHash += b"\x00" * 32
|
||||||
|
|
||||||
|
if toint(hashType) & toint(sigHashAnyOneCanPay) == 0 and toint(hashType) & toint(sigHashMask) != toint(sigHashSingle) and toint(hashType) & toint(sigHashMask) != toint(sigHashNone):
|
||||||
|
sigHash += bytes(bytearray.fromhex(sigHashes.hashSequence))[::-1]
|
||||||
|
else:
|
||||||
|
sigHash += b"\x00" * 32
|
||||||
|
|
||||||
|
sigHash += bytes(bytearray.fromhex(txin["prevout_hash"]))[::-1]
|
||||||
|
sigHash += LEtobytes(txin["prevout_n"], 4)
|
||||||
|
# byte 72
|
||||||
|
|
||||||
|
subscript = list(transaction.script_GetOp(original))
|
||||||
|
if isWitnessPubKeyHash(subscript):
|
||||||
|
sigHash += b"\x19"
|
||||||
|
sigHash += bytes([transaction.opcodes.OP_DUP])
|
||||||
|
sigHash += bytes([transaction.opcodes.OP_HASH160])
|
||||||
|
sigHash += b"\x14" # 20 bytes
|
||||||
|
assert len(subscript) == 2, subscript
|
||||||
|
opcode, data, length = subscript[1]
|
||||||
|
sigHash += data
|
||||||
|
sigHash += bytes([transaction.opcodes.OP_EQUALVERIFY])
|
||||||
|
sigHash += bytes([transaction.opcodes.OP_CHECKSIG])
|
||||||
|
else:
|
||||||
|
# For p2wsh outputs, and future outputs, the script code is
|
||||||
|
# the original script, with all code separators removed,
|
||||||
|
# serialized with a var int length prefix.
|
||||||
|
|
||||||
|
assert len(sigHash) == 104, len(sigHash)
|
||||||
|
sigHash += bytes(bytearray.fromhex(bitcoin.var_int(len(original))))
|
||||||
|
assert len(sigHash) == 105, len(sigHash)
|
||||||
|
|
||||||
|
sigHash += original
|
||||||
|
|
||||||
|
sigHash += LEtobytes(amt, 8)
|
||||||
|
sigHash += LEtobytes(txin["sequence"], 4)
|
||||||
|
|
||||||
|
if toint(hashType) & toint(sigHashSingle) != toint(sigHashSingle) and toint(hashType) & toint(sigHashNone) != toint(sigHashNone):
|
||||||
|
sigHash += bytes(bytearray.fromhex(sigHashes.hashOutputs))[::-1]
|
||||||
|
elif toint(hashtype) & toint(sigHashMask) == toint(sigHashSingle) and idx < len(decoded["outputs"]):
|
||||||
|
raise Exception("TODO 1")
|
||||||
|
else:
|
||||||
|
raise Exception("TODO 2")
|
||||||
|
|
||||||
|
sigHash += LEtobytes(decoded["lockTime"], 4)
|
||||||
|
sigHash += LEtobytes(toint(hashType), 4)
|
||||||
|
|
||||||
|
return transaction.Hash(sigHash)
|
||||||
|
|
||||||
|
#// RawTxInWitnessSignature returns the serialized ECDA signature for the input
|
||||||
|
#// idx of the given transaction, with the hashType appended to it. This
|
||||||
|
#// function is identical to RawTxInSignature, however the signature generated
|
||||||
|
#// signs a new sighash digest defined in BIP0143.
|
||||||
|
# func RawTxInWitnessSignature(tx *MsgTx, sigHashes *TxSigHashes, idx int,
|
||||||
|
# amt int64, subScript []byte, hashType SigHashType,
|
||||||
|
# key *btcec.PrivateKey) ([]byte, error) {
|
||||||
|
|
||||||
|
|
||||||
|
def rawTxInWitnessSignature(tx, sigHashes, idx, amt, subscript, hashType, key):
|
||||||
|
digest = calcWitnessSignatureHash(
|
||||||
|
subscript, sigHashes, hashType, tx, idx, amt)
|
||||||
|
return key.sign(digest, sigencode=ecdsa.util.sigencode_der) + hashType
|
||||||
|
|
||||||
|
# WitnessSignature creates an input witness stack for tx to spend BTC sent
|
||||||
|
# from a previous output to the owner of privKey using the p2wkh script
|
||||||
|
# template. The passed transaction must contain all the inputs and outputs as
|
||||||
|
# dictated by the passed hashType. The signature generated observes the new
|
||||||
|
# transaction digest algorithm defined within BIP0143.
|
||||||
|
def witnessSignature(tx, sigHashes, idx, amt, subscript, hashType, privKey, compress):
|
||||||
|
sig = rawTxInWitnessSignature(
|
||||||
|
tx, sigHashes, idx, amt, subscript, hashType, privKey)
|
||||||
|
|
||||||
|
pkData = bytes(bytearray.fromhex(
|
||||||
|
privKey.get_public_key(compressed=compress)))
|
||||||
|
|
||||||
|
return sig, pkData
|
||||||
|
|
||||||
|
|
||||||
|
sigHashMask = b"\x1f"
|
||||||
|
|
||||||
|
sigHashAll = b"\x01"
|
||||||
|
sigHashNone = b"\x02"
|
||||||
|
sigHashSingle = b"\x03"
|
||||||
|
sigHashAnyOneCanPay = b"\x80"
|
||||||
|
|
||||||
|
test = rpc_pb2.ComputeInputScriptResponse()
|
||||||
|
|
||||||
|
test.witnessScript.append(b"\x01")
|
||||||
|
test.witnessScript.append(b"\x02")
|
||||||
|
|
||||||
|
|
||||||
|
def SignOutputRaw(json):
|
||||||
|
req = rpc_pb2.SignOutputRawRequest()
|
||||||
|
json_format.Parse(json, req)
|
||||||
|
|
||||||
|
#assert len(req.signDesc.pubKey) in [33, 0]
|
||||||
|
assert len(req.signDesc.doubleTweak) in [32, 0]
|
||||||
|
assert len(req.signDesc.sigHashes.hashPrevOuts) == 64
|
||||||
|
assert len(req.signDesc.sigHashes.hashSequence) == 64
|
||||||
|
assert len(req.signDesc.sigHashes.hashOutputs) == 64
|
||||||
|
|
||||||
|
m = rpc_pb2.SignOutputRawResponse()
|
||||||
|
|
||||||
|
m.signature = signOutputRaw(req.tx, req.signDesc)
|
||||||
|
|
||||||
|
msg = json_format.MessageToJson(m)
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
def signOutputRaw(tx, signDesc):
|
||||||
|
pri = derivePrivKey(signDesc.keyDescriptor)
|
||||||
|
assert pri is not None
|
||||||
|
pri2 = maybeTweakPrivKey(signDesc, pri)
|
||||||
|
sig = rawTxInWitnessSignature(tx, signDesc.sigHashes, signDesc.inputIndex,
|
||||||
|
signDesc.output.value, signDesc.witnessScript, sigHashAll, pri2)
|
||||||
|
return sig[:len(sig) - 1]
|
||||||
|
|
||||||
|
async def PublishTransaction(json):
|
||||||
|
req = rpc_pb2.PublishTransactionRequest()
|
||||||
|
json_format.Parse(json, req)
|
||||||
|
global NETWORK
|
||||||
|
tx = transaction.Transaction(binascii.hexlify(req.tx).decode("utf-8"))
|
||||||
|
suc, has = await NETWORK.broadcast_async(tx)
|
||||||
|
m = rpc_pb2.PublishTransactionResponse()
|
||||||
|
m.success = suc
|
||||||
|
m.error = str(has) if not suc else ""
|
||||||
|
if m.error:
|
||||||
|
print("PublishTransaction", m.error)
|
||||||
|
if "Missing inputs" in m.error:
|
||||||
|
print("inputs", tx.inputs())
|
||||||
|
return json_format.MessageToJson(m)
|
||||||
|
|
||||||
|
|
||||||
|
def ComputeInputScript(json):
|
||||||
|
req = rpc_pb2.ComputeInputScriptRequest()
|
||||||
|
json_format.Parse(json, req)
|
||||||
|
|
||||||
|
#assert len(req.signDesc.pubKey) in [33, 0]
|
||||||
|
assert len(req.signDesc.doubleTweak) in [32, 0]
|
||||||
|
assert len(req.signDesc.sigHashes.hashPrevOuts) == 64
|
||||||
|
assert len(req.signDesc.sigHashes.hashSequence) == 64
|
||||||
|
assert len(req.signDesc.sigHashes.hashOutputs) == 64
|
||||||
|
# singleTweak , witnessScript variable length
|
||||||
|
|
||||||
|
try:
|
||||||
|
inpscr = computeInputScript(req.tx, req.signDesc)
|
||||||
|
except:
|
||||||
|
print("catched!")
|
||||||
|
traceback.print_exc()
|
||||||
|
return None
|
||||||
|
|
||||||
|
m = rpc_pb2.ComputeInputScriptResponse()
|
||||||
|
|
||||||
|
m.witnessScript.append(inpscr.witness[0])
|
||||||
|
m.witnessScript.append(inpscr.witness[1])
|
||||||
|
m.scriptSig = inpscr.scriptSig
|
||||||
|
|
||||||
|
msg = json_format.MessageToJson(m)
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
def fetchPrivKey(str_address, keyLocatorFamily, keyLocatorIndex):
|
||||||
|
pri = None
|
||||||
|
|
||||||
|
if str_address is not None:
|
||||||
|
pri, redeem_script = WALLET.export_private_key(str_address, None)
|
||||||
|
|
||||||
|
if redeem_script:
|
||||||
|
print("ignoring redeem script", redeem_script)
|
||||||
|
|
||||||
|
typ, pri, compressed = bitcoin.deserialize_privkey(pri)
|
||||||
|
if keyLocatorFamily == 0 and keyLocatorIndex == 0: return EC_KEY(pri)
|
||||||
|
|
||||||
|
ks = keystore.BIP32_KeyStore({})
|
||||||
|
der = "m/0'/"
|
||||||
|
xtype = 'p2wpkh'
|
||||||
|
ks.add_xprv_from_seed(pri, xtype, der)
|
||||||
|
else:
|
||||||
|
ks = WALLET.keystore
|
||||||
|
|
||||||
|
if keyLocatorFamily != 0 or keyLocatorIndex != 0:
|
||||||
|
pri = ks.get_private_key([1017, keyLocatorFamily, keyLocatorIndex], password=None)[0]
|
||||||
|
pri = EC_KEY(pri)
|
||||||
|
|
||||||
|
assert pri is not None
|
||||||
|
|
||||||
|
return pri
|
||||||
|
|
||||||
|
|
||||||
|
def computeInputScript(tx, signdesc):
|
||||||
|
typ, str_address = transaction.get_address_from_output_script(
|
||||||
|
signdesc.output.pkScript)
|
||||||
|
assert typ != bitcoin.TYPE_SCRIPT
|
||||||
|
|
||||||
|
assert len(signdesc.keyDescriptor.pubKey) == 0
|
||||||
|
pri = fetchPrivKey(str_address, signdesc.keyDescriptor.keyLocator.family, signdesc.keyDescriptor.keyLocator.index)
|
||||||
|
|
||||||
|
isNestedWitness = False # because NewAddress only does native addresses
|
||||||
|
|
||||||
|
witnessProgram = None
|
||||||
|
ourScriptSig = None
|
||||||
|
|
||||||
|
if isNestedWitness:
|
||||||
|
pub = pri.get_public_key()
|
||||||
|
|
||||||
|
scr = bitcoin.hash_160(pub)
|
||||||
|
|
||||||
|
witnessProgram = b"\x00\x14" + scr
|
||||||
|
|
||||||
|
# \x14 is OP_20
|
||||||
|
ourScriptSig = b"\x16\x00\x14" + scr
|
||||||
|
else:
|
||||||
|
# TODO TEST
|
||||||
|
witnessProgram = signdesc.output.pkScript
|
||||||
|
ourScriptSig = b""
|
||||||
|
print("set empty ourScriptSig")
|
||||||
|
print("witnessProgram", witnessProgram)
|
||||||
|
|
||||||
|
# If a tweak (single or double) is specified, then we'll need to use
|
||||||
|
# this tweak to derive the final private key to be used for signing
|
||||||
|
# this output.
|
||||||
|
pri2 = maybeTweakPrivKey(signdesc, pri)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Generate a valid witness stack for the input.
|
||||||
|
# TODO(roasbeef): adhere to passed HashType
|
||||||
|
witnessScript, pkData = witnessSignature(tx, signdesc.sigHashes,
|
||||||
|
signdesc.inputIndex, signdesc.output.value, witnessProgram,
|
||||||
|
sigHashAll, pri2, True)
|
||||||
|
return InputScript(witness=(witnessScript, pkData), scriptSig=ourScriptSig)
|
||||||
|
|
||||||
|
from collections import namedtuple
|
||||||
|
QueueItem = namedtuple("QueueItem", ["methodName", "args"])
|
||||||
|
|
||||||
|
class LightningRPC(ForeverCoroutineJob):
|
||||||
|
def __init__(self):
|
||||||
|
super(LightningRPC, self).__init__()
|
||||||
|
self.queue = queue.Queue()
|
||||||
|
self.subscribers = []
|
||||||
|
# overridden
|
||||||
|
async def run(self, is_running):
|
||||||
|
print("RPC STARTED")
|
||||||
|
while is_running():
|
||||||
|
try:
|
||||||
|
qitem = self.queue.get(block=False)
|
||||||
|
except queue.Empty:
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
def lightningRpcNetworkRequestThreadTarget(qitem):
|
||||||
|
applyMethodName = lambda x: functools.partial(x, qitem.methodName)
|
||||||
|
client = Server("http://" + machine + ":8090")
|
||||||
|
argumentStrings = [str(x) for x in qitem.args]
|
||||||
|
lightningSessionKey = base64.b64encode(privateKeyHash[:6]).decode("ascii")
|
||||||
|
resolvedMethod = getattr(client, qitem.methodName)
|
||||||
|
try:
|
||||||
|
result = resolvedMethod(lightningSessionKey, *argumentStrings)
|
||||||
|
except BaseException as e:
|
||||||
|
traceback.print_exc()
|
||||||
|
for i in self.subscribers: applyMethodName(i)(e)
|
||||||
|
raise
|
||||||
|
toprint = result
|
||||||
|
try:
|
||||||
|
assert result["stderr"] == "" and result["returncode"] == 0, "LightningRPC detected error: " + result["stderr"]
|
||||||
|
toprint = json.loads(result["stdout"])
|
||||||
|
for i in self.subscribers: applyMethodName(i)(toprint)
|
||||||
|
except BaseException as e:
|
||||||
|
traceback.print_exc()
|
||||||
|
for i in self.subscribers: applyMethodName(i)(e)
|
||||||
|
if self.console:
|
||||||
|
self.console.newResult.emit(json.dumps(toprint, indent=4))
|
||||||
|
threading.Thread(target=lightningRpcNetworkRequestThreadTarget, args=(qitem, )).start()
|
||||||
|
def setConsole(self, console):
|
||||||
|
self.console = console
|
||||||
|
def subscribe(self, notifyFunction):
|
||||||
|
self.subscribers.append(notifyFunction)
|
||||||
|
def clearSubscribers():
|
||||||
|
self.subscribers = []
|
||||||
|
|
||||||
|
def lightningCall(rpc, methodName):
|
||||||
|
def fun(*args):
|
||||||
|
rpc.queue.put(QueueItem(methodName, args))
|
||||||
|
return fun
|
||||||
|
|
||||||
|
class LightningUI():
|
||||||
|
def __init__(self, lightningGetter):
|
||||||
|
self.rpc = lightningGetter
|
||||||
|
def __getattr__(self, nam):
|
||||||
|
synced, local, server = isSynced()
|
||||||
|
if not synced:
|
||||||
|
return lambda *args: "Not synced yet: local/server: {}/{}".format(local, server)
|
||||||
|
return lightningCall(self.rpc(), nam)
|
||||||
|
|
||||||
|
privateKeyHash = None
|
||||||
|
|
||||||
|
class LightningWorker(ForeverCoroutineJob):
|
||||||
|
def __init__(self, wallet, network, config):
|
||||||
|
global privateKeyHash
|
||||||
|
super(LightningWorker, self).__init__()
|
||||||
|
self.server = None
|
||||||
|
self.wallet = wallet
|
||||||
|
self.network = network
|
||||||
|
self.config = config
|
||||||
|
ks = self.wallet().keystore
|
||||||
|
assert hasattr(ks, "xprv"), "Wallet must have xprv, can't be e.g. imported"
|
||||||
|
try:
|
||||||
|
xprv = ks.get_master_private_key(None)
|
||||||
|
except:
|
||||||
|
raise BaseException("Could not get master private key, is the wallet password protected?")
|
||||||
|
xprv, xpub = bitcoin.bip32_private_derivation(xprv, "m/", "m/152/152/152/152")
|
||||||
|
tupl = bitcoin.deserialize_xprv(xprv)
|
||||||
|
privKey = tupl[-1]
|
||||||
|
assert type(privKey) is type(bytes([]))
|
||||||
|
privateKeyHash = bitcoin.Hash(privKey)
|
||||||
|
|
||||||
|
deser = bitcoin.deserialize_xpub(wallet().keystore.xpub)
|
||||||
|
assert deser[0] == "p2wpkh", deser
|
||||||
|
self.subscribers = []
|
||||||
|
|
||||||
|
async def run(self, is_running):
|
||||||
|
global WALLET, NETWORK
|
||||||
|
global CONFIG
|
||||||
|
|
||||||
|
wasAlreadyUpToDate = False
|
||||||
|
|
||||||
|
while is_running():
|
||||||
|
WALLET = self.wallet()
|
||||||
|
NETWORK = self.network()
|
||||||
|
CONFIG = self.config()
|
||||||
|
|
||||||
|
synced, local, server = isSynced()
|
||||||
|
if not synced:
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if not wasAlreadyUpToDate:
|
||||||
|
print("UP TO DATE FOR THE FIRST TIME")
|
||||||
|
print(NETWORK.get_status_value("updated"))
|
||||||
|
wasAlreadyUpToDate = True
|
||||||
|
|
||||||
|
writer = None
|
||||||
|
try:
|
||||||
|
reader, writer = await asyncio.wait_for(asyncio.open_connection(machine, 1080), 5)
|
||||||
|
writer.write(b"MAGIC")
|
||||||
|
writer.write(privateKeyHash[:6])
|
||||||
|
await asyncio.wait_for(writer.drain(), 5)
|
||||||
|
while is_running():
|
||||||
|
obj = await readJson(reader, is_running)
|
||||||
|
if not obj: continue
|
||||||
|
if "id" not in obj:
|
||||||
|
print("Invoice update?", obj)
|
||||||
|
for i in self.subscribers: i(obj)
|
||||||
|
continue
|
||||||
|
await asyncio.wait_for(readReqAndReply(obj, writer), 10)
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
continue
|
||||||
|
def subscribe(self, notifyFunction):
|
||||||
|
self.subscribers.append(functools.partial(notifyFunction, "LightningWorker"))
|
||||||
|
|
||||||
|
async def readJson(reader, is_running):
|
||||||
|
data = b""
|
||||||
|
while is_running():
|
||||||
|
newlines = sum(1 if x == b"\n"[0] else 0 for x in data)
|
||||||
|
if newlines > 1: print("Too many newlines in Electrum/lightning.py!", data)
|
||||||
|
try:
|
||||||
|
return json.loads(data)
|
||||||
|
except ValueError:
|
||||||
|
if data != b"": print("parse failed, data has", data)
|
||||||
|
try:
|
||||||
|
data += await asyncio.wait_for(reader.read(2048), 1)
|
||||||
|
except TimeoutError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
async def readReqAndReply(obj, writer):
|
||||||
|
methods = [
|
||||||
|
# SecretKeyRing
|
||||||
|
DerivePrivKey,
|
||||||
|
DeriveNextKey,
|
||||||
|
DeriveKey,
|
||||||
|
ScalarMult
|
||||||
|
# Signer / BlockchainIO
|
||||||
|
,ConfirmedBalance
|
||||||
|
,NewAddress
|
||||||
|
,ListUnspentWitness
|
||||||
|
,WriteDb
|
||||||
|
,FetchInputInfo
|
||||||
|
,ComputeInputScript
|
||||||
|
,SignOutputRaw
|
||||||
|
,PublishTransaction
|
||||||
|
,LockOutpoint
|
||||||
|
,UnlockOutpoint
|
||||||
|
,ListTransactionDetails
|
||||||
|
,SendOutputs
|
||||||
|
,IsSynced
|
||||||
|
,SignMessage]
|
||||||
|
result = None
|
||||||
|
found = False
|
||||||
|
try:
|
||||||
|
for method in methods:
|
||||||
|
if method.__name__ == obj["method"]:
|
||||||
|
params = obj["params"][0]
|
||||||
|
print("calling method", obj["method"], "with", params)
|
||||||
|
if asyncio.iscoroutinefunction(method):
|
||||||
|
result = await method(params)
|
||||||
|
else:
|
||||||
|
result = method(params)
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
except BaseException as e:
|
||||||
|
traceback.print_exc()
|
||||||
|
print("exception while calling method", obj["method"])
|
||||||
|
writer.write(json.dumps({"id":obj["id"],"error": {"code": -32002, "message": traceback.format_exc()}}).encode("ascii") + b"\n")
|
||||||
|
await writer.drain()
|
||||||
|
else:
|
||||||
|
if not found:
|
||||||
|
# TODO assumes obj has id
|
||||||
|
writer.write(json.dumps({"id":obj["id"],"error": {"code": -32601, "message": "invalid method"}}).encode("ascii") + b"\n")
|
||||||
|
else:
|
||||||
|
print("result was", result)
|
||||||
|
if result is None:
|
||||||
|
result = "{}"
|
||||||
|
try:
|
||||||
|
assert type({}) is type(json.loads(result))
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
|
print("wrong method implementation")
|
||||||
|
writer.write(json.dumps({"id":obj["id"],"error": {"code": -32000, "message": "wrong return type in electrum-lightning-hub"}}).encode("ascii") + b"\n")
|
||||||
|
else:
|
||||||
|
writer.write(json.dumps({"id":obj["id"],"result": result}).encode("ascii") + b"\n")
|
||||||
|
await writer.drain()
|
||||||
|
|
||||||
|
def privKeyForPubKey(pubKey):
|
||||||
|
global globalIdx
|
||||||
|
priv_keys = WALLET.storage.get("lightning_extra_keys", [])
|
||||||
|
for i in priv_keys:
|
||||||
|
candidate = EC_KEY(i.to_bytes(32, "big"))
|
||||||
|
if pubkFromECKEY(candidate) == pubKey:
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
attemptKeyIdx = globalIdx - 1
|
||||||
|
while attemptKeyIdx >= 0:
|
||||||
|
attemptPrivKey = fetchPrivKey(None, 9000, attemptKeyIdx)
|
||||||
|
attempt = pubkFromECKEY(attemptPrivKey)
|
||||||
|
if attempt == pubKey:
|
||||||
|
return attemptPrivKey
|
||||||
|
attemptKeyIdx -= 1
|
||||||
|
|
||||||
|
adr = bitcoin.pubkey_to_address('p2wpkh', binascii.hexlify(pubKey).decode("utf-8"))
|
||||||
|
pri, redeem_script = WALLET.export_private_key(adr, None)
|
||||||
|
|
||||||
|
if redeem_script:
|
||||||
|
print("ignoring redeem script", redeem_script)
|
||||||
|
|
||||||
|
typ, pri, compressed = bitcoin.deserialize_privkey(pri)
|
||||||
|
return EC_KEY(pri)
|
||||||
|
|
||||||
|
#assert False, "could not find private key for pubkey {} hex={}".format(pubKey, binascii.hexlify(pubKey).decode("ascii"))
|
||||||
|
|
||||||
|
def derivePrivKey(keyDesc):
|
||||||
|
keyDescFam = keyDesc.keyLocator.family
|
||||||
|
keyDescIdx = keyDesc.keyLocator.index
|
||||||
|
keyDescPubKey = keyDesc.pubKey
|
||||||
|
privKey = None
|
||||||
|
|
||||||
|
if len(keyDescPubKey) != 0:
|
||||||
|
return privKeyForPubKey(keyDescPubKey)
|
||||||
|
|
||||||
|
return fetchPrivKey(None, keyDescFam, keyDescIdx)
|
||||||
|
|
||||||
|
def DerivePrivKey(json):
|
||||||
|
req = rpc_pb2.DerivePrivKeyRequest()
|
||||||
|
json_format.Parse(json, req)
|
||||||
|
|
||||||
|
m = rpc_pb2.DerivePrivKeyResponse()
|
||||||
|
|
||||||
|
m.privKey = derivePrivKey(req.keyDescriptor).secret.to_bytes(32, "big")
|
||||||
|
|
||||||
|
msg = json_format.MessageToJson(m)
|
||||||
|
return msg
|
||||||
|
|
||||||
|
globalIdx = 0
|
||||||
|
|
||||||
|
def DeriveNextKey(json):
|
||||||
|
global globalIdx
|
||||||
|
req = rpc_pb2.DeriveNextKeyRequest()
|
||||||
|
json_format.Parse(json, req)
|
||||||
|
|
||||||
|
family = req.keyFamily
|
||||||
|
|
||||||
|
m = rpc_pb2.DeriveNextKeyResponse()
|
||||||
|
|
||||||
|
# lnd leaves these unset:
|
||||||
|
# source: https://github.com/lightningnetwork/lnd/pull/769/files#diff-c954f5135a8995b1a3dfa298101dd0efR160
|
||||||
|
#m.keyDescriptor.keyLocator.family =
|
||||||
|
#m.keyDescriptor.keyLocator.index =
|
||||||
|
|
||||||
|
m.keyDescriptor.pubKey = pubkFromECKEY(fetchPrivKey(None, 9000, globalIdx))
|
||||||
|
globalIdx += 1
|
||||||
|
|
||||||
|
msg = json_format.MessageToJson(m)
|
||||||
|
return msg
|
||||||
|
|
||||||
|
def DeriveKey(json):
|
||||||
|
req = rpc_pb2.DeriveKeyRequest()
|
||||||
|
json_format.Parse(json, req)
|
||||||
|
|
||||||
|
family = req.keyLocator.family
|
||||||
|
idx = req.keyLocator.index
|
||||||
|
|
||||||
|
m = rpc_pb2.DeriveKeyResponse()
|
||||||
|
|
||||||
|
#lnd sets these to parameter values
|
||||||
|
m.keyDescriptor.keyLocator.family = family
|
||||||
|
m.keyDescriptor.keyLocator.index = index
|
||||||
|
|
||||||
|
m.keyDescriptor.pubKey = pubkFromECKEY(fetchPrivKey(None, family, index))
|
||||||
|
|
||||||
|
msg = json_format.MessageToJson(m)
|
||||||
|
return msg
|
||||||
|
|
||||||
|
#// ScalarMult performs a scalar multiplication (ECDH-like operation) between
|
||||||
|
#// the target key descriptor and remote public key. The output returned will be
|
||||||
|
#// the sha256 of the resulting shared point serialized in compressed format. If
|
||||||
|
#// k is our private key, and P is the public key, we perform the following
|
||||||
|
#// operation:
|
||||||
|
#//
|
||||||
|
#// sx := k*P s := sha256(sx.SerializeCompressed())
|
||||||
|
def ScalarMult(json):
|
||||||
|
req = rpc_pb2.ScalarMultRequest()
|
||||||
|
json_format.Parse(json, req)
|
||||||
|
|
||||||
|
privKey = derivePrivKey(req.keyDescriptor)
|
||||||
|
|
||||||
|
point = bitcoin.ser_to_point(req.pubKey)
|
||||||
|
|
||||||
|
point = point * privKey.secret
|
||||||
|
|
||||||
|
c = hashlib.sha256()
|
||||||
|
c.update(bitcoin.point_to_ser(point, True))
|
||||||
|
|
||||||
|
m = rpc_pb2.ScalarMultResponse()
|
||||||
|
|
||||||
|
m.hashResult = c.digest()
|
||||||
|
|
||||||
|
msg = json_format.MessageToJson(m)
|
||||||
|
return msg
|
||||||
|
|
||||||
|
def pubkFromECKEY(eckey):
|
||||||
|
return bytes(bytearray.fromhex(eckey.get_public_key(True))) #compressed=True
|
15
protoc_lightning.sh
Executable file
15
protoc_lightning.sh
Executable file
|
@ -0,0 +1,15 @@
|
||||||
|
#!/bin/sh -ex
|
||||||
|
if [ ! -d $HOME/go/src/github.com/grpc-ecosystem ]; then
|
||||||
|
# from readme in https://github.com/grpc-ecosystem/grpc-gateway
|
||||||
|
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
|
||||||
|
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger
|
||||||
|
go get -u github.com/golang/protobuf/protoc-gen-go
|
||||||
|
fi
|
||||||
|
if [ ! -d $HOME/go/src/github.com/lightningnetwork/lnd ]; then
|
||||||
|
echo "You need an lnd with electrum-bridge (ysangkok/lnd maybe?) checked out since we implement the interface from there, and need it to generate code"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
mkdir -p lib/ln || true
|
||||||
|
touch lib/__init__.py
|
||||||
|
~/go/bin/protoc -I$HOME/include -I$HOME/go/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis --python_out=lib/ln $HOME/go/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis/google/api/*.proto
|
||||||
|
python3 -m grpc_tools.protoc -I $HOME/go/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis --proto_path $HOME/go/src/github.com/lightningnetwork/lnd/electrum-bridge --python_out=lib/ln --grpc_python_out=lib/ln ~/go/src/github.com/lightningnetwork/lnd/electrum-bridge/rpc.proto
|
21
testserver.py
Normal file
21
testserver.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
async def handler(reader, writer):
|
||||||
|
magic = await reader.read(5+6)
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
print("in five sec!")
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
writer.write(b'{\n "r_preimage": "6UNoNhDZ/0awtaDTM7KuCtlYcNkNljscxMLleoJv9+o=",\n "r_hash": "lQDtsJlLe8IzSRk0hrJcgglwRdtkHzX6mIwOhJrN7Ck=",\n "value": "8192",\n "settled": true,\n "creation_date": "1519994196",\n "settle_date": "1519994199",\n "payment_request": "lntb81920n1pdfj325pp5k7erq3avatceq8ca43h5uulxrhw2ma3a442a7c8fxrsw059c3m3sdqqcqzysdpwv4dn2xd74lfmea3taxj6pjfxrdl42t8w7ceptgv5ds0td0ypk47llryl6t4a48x54d7mnwremgcmljced4dhwty9g3pfywr307aqpwtkzf4",\n "expiry": "3600",\n "cltv_expiry": "144"\n}\n'.replace(b"\n",b""))
|
||||||
|
await writer.drain()
|
||||||
|
print(magic)
|
||||||
|
|
||||||
|
async def handler2(reader, writer):
|
||||||
|
while True:
|
||||||
|
data = await reader.read(2048)
|
||||||
|
if data != b'':
|
||||||
|
writer.write(b"HTTP/1.0 200 OK\r\nContent-length: 16\r\n\r\n{\"result\":\"lol\"}")
|
||||||
|
await writer.drain()
|
||||||
|
|
||||||
|
asyncio.ensure_future(asyncio.start_server(handler, "127.0.0.1", 1080))
|
||||||
|
asyncio.ensure_future(asyncio.start_server(handler2, "127.0.0.1", 8090))
|
||||||
|
asyncio.get_event_loop().run_forever()
|
Loading…
Add table
Reference in a new issue