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 (
+
+ )
+}
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",
+)