Janus
7 years ago
committed by
ThomasV
8 changed files with 1230 additions and 0 deletions
@ -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) |
@ -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 |
@ -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_()) |
@ -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 |
@ -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 |
@ -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…
Reference in new issue