Add pay offer dialog and backend method (#296)

* Add insert sent payment method

* Delete db container from itest

* Add other container in itest

* Connect to other squeak node in itest

* Use client lightning container for itest to get offers from main node

* Get offers itest working

* Open channel to buy offer in itest

* Successfully pay offer

* Remove old print statements from itest

* Successfully unlock squeak in pay offer method

* Assert that decrypted content is correct in pay offer itest
This commit is contained in:
Jonathan Zernik 2020-10-18 00:27:49 -04:00 committed by GitHub
parent e9cfcd00c9
commit 8ebdc754ca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 510 additions and 13 deletions

View file

@ -0,0 +1,181 @@
import React, {useState, useEffect} from 'react';
import {
Paper,
IconButton,
Menu,
MenuItem,
Typography,
Grid,
Box,
Link,
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
TextField,
DialogActions,
Button,
FormControl,
InputLabel,
Select,
} from "@material-ui/core";
import { MoreVert as MoreIcon } from "@material-ui/icons";
import {useHistory} from "react-router-dom";
import classnames from "classnames";
// styles
import useStyles from "./styles";
import Widget from "../../components/Widget";
import SqueakThreadItem from "../../components/SqueakThreadItem";
import {
CloseChannelRequest,
ChannelPoint,
} from "../../proto/lnd_pb"
import { client } from "../../squeakclient/squeakclient"
export default function CloseChannelDialog({
open,
txId,
outputIndex,
handleClose,
...props
}) {
var classes = useStyles();
const history = useHistory();
var [amount, setAmount] = useState(0);
const resetFields = () => {
setAmount(0);
};
const handleChangeAmount = (event) => {
setAmount(event.target.value);
};
const closeChannel = (txId, outputIndex) => {
console.log("called closeChannel");
var closeChannelRequest = new CloseChannelRequest()
var channelPoint = new ChannelPoint();
channelPoint.setFundingTxidStr(txId);
channelPoint.setOutputIndex(outputIndex);
closeChannelRequest.setChannelPoint(channelPoint);
console.log(closeChannelRequest);
client.lndCloseChannel(closeChannelRequest, {}, (err, response) => {
if (err) {
console.log(err.message);
alert('Error closing channel: ' + err.message);
return;
}
console.log(response);
// goToProfilePage(response.getProfileId());
});
};
// const goToProfilePage = (profileId) => {
// history.push("/app/profile/" + profileId);
// };
function handleSubmit(event) {
event.preventDefault();
console.log( 'txId:', txId);
console.log( 'outputIndex:', outputIndex);
closeChannel(txId, outputIndex);
handleClose();
}
function TxIdInput() {
return (
<TextField
id="txid-textarea"
label="TxId"
required
autoFocus
value={txId}
fullWidth
inputProps={{
readOnly: true,
}}
/>
)
}
function OutputIndexInput() {
return (
<TextField
id="outputindex-textarea"
label="Output Index"
required
autoFocus
value={outputIndex}
fullWidth
inputProps={{
readOnly: true,
}}
/>
)
}
function LocalFundingAmountInput() {
return (
<TextField
id="amount-textarea"
label="Local Funding Amount"
required
autoFocus
value={amount}
onChange={handleChangeAmount}
fullWidth
inputProps={{ maxLength: 64 }}
/>
)
}
function CancelButton() {
return (
<Button
onClick={handleClose}
variant="contained"
color="secondary"
>
Cancel
</Button>
)
}
function CloseChannelButton() {
return (
<Button
type="submit"
variant="contained"
color="primary"
className={classes.button}
>
Close Channel
</Button>
)
}
return (
<Dialog open={open} onEnter={resetFields} onClose={handleClose} aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">Close Channel</DialogTitle>
<form className={classes.root} onSubmit={handleSubmit} noValidate autoComplete="off">
<DialogContent>
{TxIdInput()}
</DialogContent>
<DialogContent>
{OutputIndexInput()}
</DialogContent>
<DialogActions>
{CancelButton()}
{CloseChannelButton()}
</DialogActions>
</form>
</Dialog>
)
}

View file

@ -0,0 +1,6 @@
{
"name": "BuyOfferDialog",
"version": "0.0.0",
"private": true,
"main": "BuyOfferDialog.js"
}

View file

@ -0,0 +1,43 @@
import { makeStyles } from "@material-ui/styles";
export default makeStyles(theme => ({
widgetWrapper: {
display: "flex",
minHeight: "100%",
},
widgetHeader: {
padding: theme.spacing(3),
paddingBottom: theme.spacing(1),
display: "flex",
justifyContent: "space-between",
alignItems: "center",
},
widgetRoot: {
boxShadow: theme.customShadows.widget,
},
widgetBody: {
paddingBottom: theme.spacing(3),
paddingRight: theme.spacing(3),
paddingLeft: theme.spacing(3),
},
noPadding: {
padding: 0,
},
paper: {
display: "flex",
flexDirection: "column",
flexGrow: 1,
overflow: "hidden",
},
moreButton: {
margin: -theme.spacing(1),
padding: 0,
width: 40,
height: 40,
color: theme.palette.text.hint,
"&:hover": {
backgroundColor: theme.palette.primary.main,
color: "rgba(255, 255, 255, 0.35)",
},
},
}));

View file

@ -1,16 +1,6 @@
version: '3'
services:
db:
image: postgres
container_name: test_db
environment:
- POSTGRES_DB=postgres
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
volumes:
- ../createdb.sql:/docker-entrypoint-initdb.d/init.sql
btcd:
image: btcd
container_name: test_btcd
@ -93,8 +83,23 @@ services:
links:
- "btcd:btcd"
- "lnd_server:lnd"
depends_on:
- db
sysctls:
- net.ipv6.conf.all.disable_ipv6=0
entrypoint: ["./start-sqkserver.sh"]
sqkserver_other:
image: sqkserver
container_name: test_sqkserver_other
build:
context: ../
dockerfile: docker/sqkserver/Dockerfile
volumes:
- test_shared:/rpc
- test_lnd_client_dir:/root/.lnd
- ./config.ini:/app/config.ini
links:
- "btcd:btcd"
- "lnd_client:lnd"
sysctls:
- net.ipv6.conf.all.disable_ipv6=0
entrypoint: ["./start-sqkserver.sh"]

View file

@ -13,7 +13,7 @@ function mine_blocks {
cd itests
docker-compose down --volumes
docker-compose down --volumes --remove-orphans
COMPOSE_DOCKER_CLI_BUILD=1 docker-compose build
docker-compose up -d

View file

@ -18,12 +18,24 @@ def server_stub():
yield squeak_server_pb2_grpc.SqueakServerStub(server_channel)
@pytest.fixture
def other_server_stub():
with grpc.insecure_channel("sqkserver_other:8774") as server_channel:
yield squeak_server_pb2_grpc.SqueakServerStub(server_channel)
@pytest.fixture
def admin_stub():
with grpc.insecure_channel("sqkserver:8994") as admin_channel:
yield squeak_admin_pb2_grpc.SqueakAdminStub(admin_channel)
@pytest.fixture
def other_admin_stub():
with grpc.insecure_channel("sqkserver_other:8994") as admin_channel:
yield squeak_admin_pb2_grpc.SqueakAdminStub(admin_channel)
@pytest.fixture
def lightning_client():
return load_lightning_client()

View file

@ -778,3 +778,105 @@ def test_open_channel(server_stub, admin_stub, lightning_client, saved_squeak_ha
list_channels_response = admin_stub.LndListChannels(ln.ListChannelsRequest())
assert len(list_channels_response.channels) == 0
def test_connect_other_node(server_stub, admin_stub, other_server_stub, other_admin_stub, lightning_client, signing_profile_id, saved_squeak_hash):
# Get all squeak displays
get_followed_squeak_display_response = other_admin_stub.GetFollowedSqueakDisplays(
squeak_admin_pb2.GetFollowedSqueakDisplaysRequest()
)
assert len(get_followed_squeak_display_response.squeak_display_entries) == 0
# Add the main node as a peer
create_peer_response = other_admin_stub.CreatePeer(
squeak_admin_pb2.CreatePeerRequest(
host="sqkserver",
port=8774,
)
)
peer_id = create_peer_response.peer_id
# Set the peer to be downloading
other_admin_stub.SetPeerDownloading(
squeak_admin_pb2.SetPeerDownloadingRequest(
peer_id=peer_id,
downloading=True,
)
)
# Get the squeak profile
get_squeak_profile_response = admin_stub.GetSqueakProfile(
squeak_admin_pb2.GetSqueakProfileRequest(
profile_id=signing_profile_id,
)
)
squeak_profile_address = get_squeak_profile_response.squeak_profile.address
squeak_profile_name = get_squeak_profile_response.squeak_profile.profile_name
print("Got squeak profile: {} with address: {}".format(squeak_profile_name, squeak_profile_address))
# Set the signing profile to be sharing on the main server
admin_stub.SetSqueakProfileSharing(
squeak_admin_pb2.SetSqueakProfileSharingRequest(
profile_id=signing_profile_id,
sharing=True,
)
)
# Add the contact profile to the other server and set the profile to be following
create_contact_profile_response = other_admin_stub.CreateContactProfile(
squeak_admin_pb2.CreateContactProfileRequest(
profile_name=squeak_profile_name,
address=squeak_profile_address,
)
)
contact_profile_id = create_contact_profile_response.profile_id
other_admin_stub.SetSqueakProfileFollowing(
squeak_admin_pb2.SetSqueakProfileFollowingRequest(
profile_id=contact_profile_id,
following=True,
)
)
# Sync squeaks
other_admin_stub.SyncSqueaks(
squeak_admin_pb2.SyncSqueaksRequest(),
)
time.sleep(10)
# Get the buy offer
get_buy_offers_response = other_admin_stub.GetBuyOffers(
squeak_admin_pb2.GetBuyOffersRequest(
squeak_hash=saved_squeak_hash.hex(),
)
)
print(get_buy_offers_response)
assert len(get_buy_offers_response.offers) > 0
offer = get_buy_offers_response.offers[0]
with connect_peer(lightning_client, offer.node_host, offer.node_pubkey), \
open_channel(lightning_client, offer.node_pubkey, 1000000):
list_channels_response = lightning_client.list_channels()
print(list_channels_response)
# Pay the offer
pay_offer_response = other_admin_stub.PayOffer(
squeak_admin_pb2.PayOfferRequest(
offer_id=offer.offer_id,
)
)
print(pay_offer_response)
assert pay_offer_response.sent_payment_id > 0
# Get the squeak display item
get_squeak_display_response = other_admin_stub.GetSqueakDisplay(
squeak_admin_pb2.GetSqueakDisplayRequest(
squeak_hash=saved_squeak_hash.hex(),
)
)
assert (
get_squeak_display_response.squeak_display_entry.content_str
== "Hello from the profile on the server!"
)

View file

@ -156,6 +156,14 @@ service SqueakAdmin {
*/
rpc GetBuyOffer (GetBuyOfferRequest) returns (GetBuyOfferReply) {}
/** sqkadmin: `syncsqueaks`
*/
rpc SyncSqueaks (SyncSqueaksRequest) returns (SyncSqueaksReply) {}
/** sqkadmin: `payoffer`
*/
rpc PayOffer (PayOfferRequest) returns (PayOfferReply) {}
}
message CreateSigningProfileRequest {
@ -515,3 +523,19 @@ message OfferDisplayEntry {
/// The invoice expiry
int32 invoice_expiry = 9;
}
message SyncSqueaksRequest {
}
message SyncSqueaksReply {
}
message PayOfferRequest {
/// Offer id
int32 offer_id = 1;
}
message PayOfferReply {
/// Sent payment id
int32 sent_payment_id = 1;
}

View file

@ -276,3 +276,11 @@ class SqueakAdminServerHandler(object):
def handle_get_buy_offer(self, offer_id):
logger.info("Handle get buy offer for hash: {}".format(offer_id))
return self.squeak_node.get_buy_offer_with_peer(offer_id)
def handle_sync_squeaks(self):
logger.info("Handle get sync squeaks")
self.squeak_node.sync_squeaks()
def handle_pay_offer(self, offer_id):
logger.info("Handle pay offer for offer id: {}".format(offer_id))
return self.squeak_node.pay_offer(offer_id)

View file

@ -293,6 +293,17 @@ class SqueakAdminServerServicer(squeak_admin_pb2_grpc.SqueakAdminServicer):
offer=offer_msg,
)
def SyncSqueaks(self, request, context):
self.handler.handle_sync_squeaks()
return squeak_admin_pb2.SyncSqueaksReply()
def PayOffer(self, request, context):
offer_id = request.offer_id
sent_payment_id = self.handler.handle_pay_offer(offer_id)
return squeak_admin_pb2.PayOfferReply(
sent_payment_id=sent_payment_id,
)
def _squeak_entry_to_message(self, squeak_entry_with_profile):
if squeak_entry_with_profile is None:
return None

View file

@ -764,6 +764,17 @@ class SqueakDb:
# # execute the UPDATE statement
# curs.execute(sql, (block_header, squeak_hash_str,))
def set_squeak_decryption_key(self, squeak_hash, vch_decryption_key):
""" Set the decryption key of a squeak. """
squeak_hash_str = squeak_hash.hex()
stmt = (
self.squeaks.update()
.where(self.squeaks.c.hash == squeak_hash_str)
.values(vch_decryption_key=vch_decryption_key)
)
with self.get_connection() as connection:
connection.execute(stmt)
def delete_squeak(self, squeak_hash):
""" Delete a squeak. """
squeak_hash_str = squeak_hash.hex()
@ -975,6 +986,23 @@ class SqueakDb:
# rows = curs.fetchall()
# return len(rows)
def insert_sent_payment(self, sent_payment):
""" Insert a new sent payment. """
ins = self.sent_payments.insert().values(
offer_id=sent_payment.offer_id,
peer_id=sent_payment.peer_id,
squeak_hash=sent_payment.squeak_hash,
preimage_hash=sent_payment.preimage_hash,
preimage=sent_payment.preimage,
amount=sent_payment.amount,
node_pubkey=sent_payment.node_pubkey,
preimage_is_valid=sent_payment.preimage_is_valid,
)
with self.get_connection() as connection:
res = connection.execute(ins)
sent_payment_id = res.inserted_primary_key[0]
return sent_payment_id
def _parse_squeak_entry(self, row):
if row is None:
return None

View file

@ -1,8 +1,11 @@
import logging
from squeak.core.encryption import (
CEncryptedDecryptionKey,
generate_initialization_vector,
)
from squeak.core.signing import CSigningKey, CSqueakAddress
from squeak.core import CheckSqueak
from squeakserver.core.squeak_address_validator import SqueakAddressValidator
from squeakserver.node.squeak_block_periodic_worker import SqueakBlockPeriodicWorker
@ -19,9 +22,13 @@ from squeakserver.node.squeak_whitelist import SqueakWhitelist
from squeakserver.server.buy_offer import BuyOffer
from squeakserver.server.squeak_peer import SqueakPeer
from squeakserver.server.squeak_profile import SqueakProfile
from squeakserver.server.sent_payment import SentPayment
from squeakserver.server.util import generate_offer_preimage
logger = logging.getLogger(__name__)
class SqueakNode:
def __init__(
self,
@ -256,3 +263,60 @@ class SqueakNode:
def get_buy_offer_with_peer(self, offer_id):
return self.postgres_db.get_offer_with_peer(offer_id)
def pay_offer(self, offer_id):
# Get the offer from the database
offer_with_peer = self.postgres_db.get_offer_with_peer(offer_id)
offer = offer_with_peer.offer
# Pay the invoice
payment = self.lightning_client.pay_invoice_sync(offer.payment_request)
preimage = payment.payment_preimage
# TODO: Check if preimage is valid
is_valid = True
# Unlock the squeak
squeak_entry = self.postgres_db.get_squeak_entry(bytes.fromhex(offer.squeak_hash))
squeak = squeak_entry.squeak
# Verify with the payment preimage and decryption key ciphertext
decryption_key_cipher_bytes = offer.key_cipher
iv = offer.iv
encrypted_decryption_key = CEncryptedDecryptionKey.from_bytes(
decryption_key_cipher_bytes
)
# Decrypt the decryption key
decryption_key = encrypted_decryption_key.get_decryption_key(preimage, iv)
serialized_decryption_key = decryption_key.get_bytes()
# Check the decryption key
squeak.SetDecryptionKey(serialized_decryption_key)
CheckSqueak(squeak)
logger.info("squeak.GetDecryptedContentStr():")
logger.info(squeak.GetDecryptedContentStr())
# Set the decryption key in the database
self.squeak_store.unlock_squeak(
bytes.fromhex(offer.squeak_hash),
serialized_decryption_key,
)
# Save the preimage of the sent payment
sent_payment = SentPayment(
sent_payment_id=None,
offer_id=offer_id,
peer_id=offer.peer_id,
squeak_hash=offer.squeak_hash,
preimage_hash=offer.payment_hash,
preimage=preimage,
amount=offer.price_msat,
node_pubkey=offer.destination,
preimage_is_valid=is_valid,
)
sent_payment_id = self.postgres_db.insert_sent_payment(sent_payment)
return sent_payment_id
def sync_squeaks(self):
self.squeak_peer_sync_worker.sync_peers()

View file

@ -21,6 +21,7 @@ class SqueakPeerSyncWorker:
def sync_peers(self):
logger.info("Syncing peers...")
peers = self._get_peers()
logger.info("Syncing peers: {}".format(peers))
self.squeak_sync_controller.sync_peers(peers)
def start_running(self):

View file

@ -96,3 +96,9 @@ class SqueakStore:
max_block,
peer_id,
)
def unlock_squeak(self, squeak_hash, vch_decryption_key):
self.postgres_db.set_squeak_decryption_key(
squeak_hash,
vch_decryption_key,
)

View file

@ -0,0 +1,6 @@
from collections import namedtuple
SentPayment = namedtuple(
"SentPayment",
"sent_payment_id, offer_id, peer_id, squeak_hash, preimage_hash, preimage, amount, node_pubkey, preimage_is_valid",
)