diff --git a/frontend/squeak-node-frontend/src/components/BuyOfferDialog/BuyOfferDialog.js b/frontend/squeak-node-frontend/src/components/BuyOfferDialog/BuyOfferDialog.js new file mode 100644 index 00000000..1f7eba2a --- /dev/null +++ b/frontend/squeak-node-frontend/src/components/BuyOfferDialog/BuyOfferDialog.js @@ -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 ( + + ) + } + + function OutputIndexInput() { + return ( + + ) + } + + function LocalFundingAmountInput() { + return ( + + ) + } + + function CancelButton() { + return ( + + ) + } + + function CloseChannelButton() { + return ( + + ) + } + + return ( + + Close Channel +
+ + {TxIdInput()} + + + {OutputIndexInput()} + + + {CancelButton()} + {CloseChannelButton()} + +
+
+ ) +} diff --git a/frontend/squeak-node-frontend/src/components/BuyOfferDialog/package.json b/frontend/squeak-node-frontend/src/components/BuyOfferDialog/package.json new file mode 100644 index 00000000..7c81f856 --- /dev/null +++ b/frontend/squeak-node-frontend/src/components/BuyOfferDialog/package.json @@ -0,0 +1,6 @@ +{ + "name": "BuyOfferDialog", + "version": "0.0.0", + "private": true, + "main": "BuyOfferDialog.js" +} diff --git a/frontend/squeak-node-frontend/src/components/BuyOfferDialog/styles.js b/frontend/squeak-node-frontend/src/components/BuyOfferDialog/styles.js new file mode 100644 index 00000000..f2d64da1 --- /dev/null +++ b/frontend/squeak-node-frontend/src/components/BuyOfferDialog/styles.js @@ -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)", + }, + }, +})); diff --git a/itests/docker-compose.yml b/itests/docker-compose.yml index 373fe3f4..94364af7 100644 --- a/itests/docker-compose.yml +++ b/itests/docker-compose.yml @@ -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"] diff --git a/itests/run_itest.sh b/itests/run_itest.sh index 2ec7e5a0..6ebc631b 100755 --- a/itests/run_itest.sh +++ b/itests/run_itest.sh @@ -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 diff --git a/itests/tests/conftest.py b/itests/tests/conftest.py index 376554a7..5f836b5d 100644 --- a/itests/tests/conftest.py +++ b/itests/tests/conftest.py @@ -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() diff --git a/itests/tests/test_squeak_node.py b/itests/tests/test_squeak_node.py index f7cb9714..ec81baf0 100644 --- a/itests/tests/test_squeak_node.py +++ b/itests/tests/test_squeak_node.py @@ -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!" + ) diff --git a/proto/squeak_admin.proto b/proto/squeak_admin.proto index 58e0c3ca..dc5213c6 100644 --- a/proto/squeak_admin.proto +++ b/proto/squeak_admin.proto @@ -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; +} diff --git a/squeakserver/admin/squeak_admin_server_handler.py b/squeakserver/admin/squeak_admin_server_handler.py index 8dc2625e..6d2db453 100644 --- a/squeakserver/admin/squeak_admin_server_handler.py +++ b/squeakserver/admin/squeak_admin_server_handler.py @@ -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) diff --git a/squeakserver/admin/squeak_admin_server_servicer.py b/squeakserver/admin/squeak_admin_server_servicer.py index f9097dd5..1e1866fc 100644 --- a/squeakserver/admin/squeak_admin_server_servicer.py +++ b/squeakserver/admin/squeak_admin_server_servicer.py @@ -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 diff --git a/squeakserver/db/squeak_db.py b/squeakserver/db/squeak_db.py index 3a807bae..028ceee4 100644 --- a/squeakserver/db/squeak_db.py +++ b/squeakserver/db/squeak_db.py @@ -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 diff --git a/squeakserver/node/squeak_node.py b/squeakserver/node/squeak_node.py index d3ad7fd4..312ef6ec 100644 --- a/squeakserver/node/squeak_node.py +++ b/squeakserver/node/squeak_node.py @@ -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() diff --git a/squeakserver/node/squeak_peer_sync_worker.py b/squeakserver/node/squeak_peer_sync_worker.py index 6877ba09..786c2cd3 100644 --- a/squeakserver/node/squeak_peer_sync_worker.py +++ b/squeakserver/node/squeak_peer_sync_worker.py @@ -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): diff --git a/squeakserver/node/squeak_store.py b/squeakserver/node/squeak_store.py index 5dac1543..eea3e2d7 100644 --- a/squeakserver/node/squeak_store.py +++ b/squeakserver/node/squeak_store.py @@ -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, + ) diff --git a/squeakserver/server/sent_payment.py b/squeakserver/server/sent_payment.py new file mode 100644 index 00000000..7c496180 --- /dev/null +++ b/squeakserver/server/sent_payment.py @@ -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", +)