diff --git a/frontend/react-material-admin/src/components/Layout/Layout.js b/frontend/react-material-admin/src/components/Layout/Layout.js index 991b766c..32eccb69 100644 --- a/frontend/react-material-admin/src/components/Layout/Layout.js +++ b/frontend/react-material-admin/src/components/Layout/Layout.js @@ -20,6 +20,7 @@ import Dashboard from "../../pages/dashboard"; import SqueakAddress from "../../pages/squeakaddress"; import Profile from "../../pages/profile"; import CreateSigningProfile from "../../pages/createsigningprofile"; +import CreateContactProfile from "../../pages/createcontactprofile"; import MakeSqueak from "../../pages/makesqueak"; import Lightning from "../../pages/lightning"; import Notifications from "../../pages/notifications"; @@ -54,6 +55,7 @@ function Layout(props) { + diff --git a/frontend/react-material-admin/src/pages/createcontactprofile/CreateContactProfile.js b/frontend/react-material-admin/src/pages/createcontactprofile/CreateContactProfile.js new file mode 100644 index 00000000..dcbcafc2 --- /dev/null +++ b/frontend/react-material-admin/src/pages/createcontactprofile/CreateContactProfile.js @@ -0,0 +1,83 @@ +import React, {useState, useEffect} from 'react'; +import {useHistory} from "react-router-dom"; +import {Grid, TextField, Button, Typography, Paper} from "@material-ui/core"; + +// styles +import useStyles from "./styles"; + +// components +import PageTitle from "../../components/PageTitle"; +import Widget from "../../components/Widget"; +// import { Typography } from "../../components/Wrappers"; + +import {CreateContactProfileRequest} from "../../proto/squeak_admin_pb" +import {SqueakAdminClient} from "../../proto/squeak_admin_grpc_web_pb" + +var client = new SqueakAdminClient('http://' + window.location.hostname + ':8080') + +export default function CreateContactProfilePage() { + const [profileName, setProfileName] = useState(''); + const [address, setAddress] = useState(''); + + var classes = useStyles(); + const history = useHistory(); + + const goToProfilePage = (profileId) => { + history.push("/app/profile/" + profileId); + }; + + function handleSubmit(event) { + event.preventDefault(); + console.log( 'profileName:', profileName); + console.log( 'address:', address); + // You should see email and password in console. + // ..code to submit form to backend here... + createContactProfile(profileName, address); + } + + const createContactProfile = (profileName, squeakAddress) => { + console.log("called createContactProfile"); + + var createContactProfileRequest = new CreateContactProfileRequest() + createContactProfileRequest.setProfileName(profileName); + createContactProfileRequest.setAddress(squeakAddress); + console.log(createContactProfileRequest); + + client.createContactProfile(createContactProfileRequest, {}, (err, response) => { + if (err) { + console.log(err.message); + alert('Error creating contact profile: ' + err.message); + return; + } + console.log(response); + console.log(response.getProfileId()); + goToProfilePage(response.getProfileId()); + }); + }; + + return ( + <> + < PageTitle title = "Create Contact Profile" /> + +
+ setProfileName(e.target.value)} + /> + setAddress(e.target.value)} + /> + + + +
+); +} diff --git a/frontend/react-material-admin/src/pages/createcontactprofile/package.json b/frontend/react-material-admin/src/pages/createcontactprofile/package.json new file mode 100644 index 00000000..4e6a85cd --- /dev/null +++ b/frontend/react-material-admin/src/pages/createcontactprofile/package.json @@ -0,0 +1,6 @@ +{ + "name": "CreateContactProfile", + "version": "0.0.0", + "main": "CreateContactProfile.js", + "private": true +} diff --git a/frontend/react-material-admin/src/pages/createcontactprofile/styles.js b/frontend/react-material-admin/src/pages/createcontactprofile/styles.js new file mode 100644 index 00000000..39d05eff --- /dev/null +++ b/frontend/react-material-admin/src/pages/createcontactprofile/styles.js @@ -0,0 +1,21 @@ +import { makeStyles } from "@material-ui/styles"; + +export default makeStyles(theme => ({ + dashedBorder: { + border: "1px dashed", + borderColor: theme.palette.primary.main, + padding: theme.spacing(2), + paddingTop: theme.spacing(4), + paddingBottom: theme.spacing(4), + marginTop: theme.spacing(1), + }, + text: { + marginBottom: theme.spacing(2), + }, + root: { + '& .MuiTextField-root': { + margin: theme.spacing(1), + width: '25ch', + }, +}, +})); diff --git a/frontend/react-material-admin/src/pages/createsigningprofile/CreateSigningProfile.js b/frontend/react-material-admin/src/pages/createsigningprofile/CreateSigningProfile.js index 706b251f..9b202752 100644 --- a/frontend/react-material-admin/src/pages/createsigningprofile/CreateSigningProfile.js +++ b/frontend/react-material-admin/src/pages/createsigningprofile/CreateSigningProfile.js @@ -41,6 +41,11 @@ export default function CreateSigningProfilePage() { console.log(createSigningProfileRequest); client.createSigningProfile(createSigningProfileRequest, {}, (err, response) => { + if (err) { + console.log(err.message); + alert('Error creating signing profile: ' + err.message); + return; + } console.log(response); console.log(response.getProfileId()); goToProfilePage(response.getProfileId()); diff --git a/frontend/react-material-admin/src/pages/profiles/Profiles.js b/frontend/react-material-admin/src/pages/profiles/Profiles.js index baf28ddc..f451bc52 100644 --- a/frontend/react-material-admin/src/pages/profiles/Profiles.js +++ b/frontend/react-material-admin/src/pages/profiles/Profiles.js @@ -1,6 +1,15 @@ import React, {useState, useEffect} from 'react'; import {useHistory} from "react-router-dom"; -import {Grid, Button} from "@material-ui/core"; +import { + Grid, + Button, + Paper, + Tabs, + Tab, + AppBar, + Box, + Typography, + } from "@material-ui/core"; import MUIDataTable from "mui-datatables"; // styles @@ -15,7 +24,12 @@ import Table from "../dashboard/components/Table/Table"; import mock from "../dashboard/mock"; import {GetInfoRequest} from "../../proto/lnd_pb" -import {HelloRequest, GetFollowedSqueakDisplaysRequest, GetSigningProfilesRequest} from "../../proto/squeak_admin_pb" +import { + HelloRequest, + GetFollowedSqueakDisplaysRequest, + GetSigningProfilesRequest, + GetContactProfilesRequest, +} from "../../proto/squeak_admin_pb" import {SqueakAdminClient} from "../../proto/squeak_admin_grpc_web_pb" var client = new SqueakAdminClient('http://' + window.location.hostname + ':8080') @@ -85,8 +99,21 @@ const useStyles = makeStyles((theme) => ({ export default function Profiles() { const classes = useStyles(); const [signingProfiles, setSigningProfiles] = useState([]); + const [contactProfiles, setContactProfiles] = useState([]); + const [value, setValue] = useState(0); const history = useHistory(); + function a11yProps(index) { + return { + id: `simple-tab-${index}`, + 'aria-controls': `simple-tabpanel-${index}`, + }; + } + + const handleChange = (event, newValue) => { + setValue(newValue); + }; + const getLndInfo = () => { console.log("called getLndInfo"); @@ -114,11 +141,29 @@ export default function Profiles() { setSigningProfiles(response.getSqueakProfilesList()); }); }; + const getContactProfiles = () => { + console.log("called getContactProfiles"); + + var getContactProfilesRequest = new GetContactProfilesRequest() + + client.getContactProfiles(getContactProfilesRequest, {}, (err, response) => { + if (err) { + console.log(err.message); + return; + } + console.log(response); + setContactProfiles(response.getSqueakProfilesList()); + }); + }; const goToCreateSigningProfilePage = () => { history.push("/app/createsigningprofile"); }; + const goToCreateContactProfilePage = () => { + history.push("/app/createcontactprofile"); + }; + const goToSqueakAddressPage = (squeakAddress) => { history.push("/app/squeakaddress/" + squeakAddress); }; @@ -129,11 +174,52 @@ export default function Profiles() { useEffect(() => { getSigningProfiles() }, []); + useEffect(() => { + getContactProfiles() + }, []); - return ( - <> - < PageTitle title = "Profiles" /> - + function TabPanel(props) { + const { children, value, index, ...other } = props; + + return ( + + ); + } + + function ProfilesTabs() { + return ( + <> + + + + + + + + {SigningProfiles()} + + + {ContactProfiles()} + + + ) + } + + function CreateSigningProfileButton() { + return ( + <>
+
+
+ + ) + } + + function CreateContactProfileButton() { + return ( + <> + +
- - - [ - p.getProfileName(), - p.getAddress(), - p.getFollowing().toString(), - p.getSharing().toString(), - ] - )} - columns={["Name", "Address", "Following", "Sharing"]} - options={{ - filter: false, - print: false, - viewColumns: false, - selectableRows: "none", - onRowClick: rowData => { - var address = rowData[1]; - goToSqueakAddressPage(address); - } - }}/> - -
+ + ) + } + + function SigningProfiles() { + return ( + <> + + {CreateSigningProfileButton()} + + + [ + p.getProfileName(), + p.getAddress(), + p.getFollowing().toString(), + p.getSharing().toString(), + ] + )} + columns={["Name", "Address", "Following", "Sharing"]} + options={{ + filter: false, + print: false, + viewColumns: false, + selectableRows: "none", + onRowClick: rowData => { + var address = rowData[1]; + goToSqueakAddressPage(address); + } + }}/> + + + + ) + } + + function ContactProfiles() { + return ( + <> + + {CreateContactProfileButton()} + + + [ + p.getProfileName(), + p.getAddress(), + p.getFollowing().toString(), + p.getSharing().toString(), + ] + )} + columns={["Name", "Address", "Following", "Sharing"]} + options={{ + filter: false, + print: false, + viewColumns: false, + selectableRows: "none", + onRowClick: rowData => { + var address = rowData[1]; + goToSqueakAddressPage(address); + } + }}/> + + + + ) + } + + return ( + <> + < PageTitle title = "Profiles" /> + {ProfilesTabs()} < />); } diff --git a/itests/test.py b/itests/test.py index a9d027c6..e406dec8 100644 --- a/itests/test.py +++ b/itests/test.py @@ -261,6 +261,21 @@ def run(): ) profile_id = create_signing_profile_response.profile_id + # Create a new contact profile + contact_name = "carol" + contact_address = "1GbFEcAaAzi2fGRaTsgDMm4N8cue5P26mc" + create_contact_profile_response = admin_stub.CreateContactProfile( + squeak_admin_pb2.CreateContactProfileRequest( + profile_name=contact_name, + address=contact_address, + ) + ) + print( + "Get create contact profile response: " + + str(create_contact_profile_response) + ) + contact_profile_id = create_contact_profile_response.profile_id + # Get the new squeak profile get_squeak_profile_response = admin_stub.GetSqueakProfile( squeak_admin_pb2.GetSqueakProfileRequest(profile_id=profile_id,) @@ -342,6 +357,15 @@ def run(): len(get_signing_profiles_response.squeak_profiles) == 1 ) + # Get all contact profiles + get_contact_profiles_response = admin_stub.GetContactProfiles( + squeak_admin_pb2.GetContactProfilesRequest() + ) + print("Get contact profiles response: " + str(get_contact_profiles_response)) + assert ( + len(get_contact_profiles_response.squeak_profiles) == 1 + ) + # Get all squeak displays for the known address get_address_squeak_display_response = admin_stub.GetAddressSqueakDisplays( squeak_admin_pb2.GetAddressSqueakDisplaysRequest( diff --git a/proto/squeak_admin.proto b/proto/squeak_admin.proto index 00219d41..165334a8 100644 --- a/proto/squeak_admin.proto +++ b/proto/squeak_admin.proto @@ -26,10 +26,18 @@ service SqueakAdmin { */ rpc CreateSigningProfile (CreateSigningProfileRequest) returns (CreateSigningProfileReply) {} + /** sqkadmin: `createcontactprofile` + */ + rpc CreateContactProfile (CreateContactProfileRequest) returns (CreateContactProfileReply) {} + /** sqkadmin: `getsigningprofiles` */ rpc GetSigningProfiles (GetSigningProfilesRequest) returns (GetSigningProfilesReply) {} + /** sqkadmin: `getcontactprofiles` + */ + rpc GetContactProfiles (GetContactProfilesRequest) returns (GetContactProfilesReply) {} + /** sqkadmin: `getsqueakprofile` */ rpc GetSqueakProfile (GetSqueakProfileRequest) returns (GetSqueakProfileReply) {} @@ -74,6 +82,19 @@ message CreateSigningProfileReply { int32 profile_id = 1; } +message CreateContactProfileRequest { + /// The name of the new signing profile + string profile_name = 1; + + /// The address + string address = 2; +} + +message CreateContactProfileReply { + /// The profile id + int32 profile_id = 1; +} + message GetSigningProfilesRequest { } @@ -82,6 +103,14 @@ message GetSigningProfilesReply { repeated SqueakProfile squeak_profiles = 1; } +message GetContactProfilesRequest { +} + +message GetContactProfilesReply { + /// The squeak profiles + repeated SqueakProfile squeak_profiles = 1; +} + message GetSqueakProfileRequest { /// The profile id int32 profile_id = 1; diff --git a/squeakserver/admin/squeak_admin_server_handler.py b/squeakserver/admin/squeak_admin_server_handler.py index 5afffbb6..f3c51d9a 100644 --- a/squeakserver/admin/squeak_admin_server_handler.py +++ b/squeakserver/admin/squeak_admin_server_handler.py @@ -30,12 +30,24 @@ class SqueakAdminServerHandler(object): logger.info("New profile_id: {}".format(profile_id)) return profile_id + def handle_create_contact_profile(self, profile_name, squeak_address): + logger.info("Handle create contact profile with name: {}, address: {}".format(profile_name, squeak_address)) + profile_id = self.squeak_node.create_contact_profile(profile_name, squeak_address) + logger.info("New profile_id: {}".format(profile_id)) + return profile_id + def handle_get_signing_profiles(self): logger.info("Handle get signing profiles.") profiles = self.squeak_node.get_signing_profiles() logger.info("Got number of profiles: {}".format(len(profiles))) return profiles + def handle_get_contact_profiles(self): + logger.info("Handle get contact profiles.") + profiles = self.squeak_node.get_contact_profiles() + logger.info("Got number of profiles: {}".format(len(profiles))) + return profiles + def handle_get_squeak_profile(self, profile_id): logger.info("Handle get squeak profile with id: {}".format(profile_id)) squeak_profile = self.squeak_node.get_squeak_profile(profile_id) diff --git a/squeakserver/admin/squeak_admin_server_servicer.py b/squeakserver/admin/squeak_admin_server_servicer.py index 7fbc8a39..7efffbd5 100644 --- a/squeakserver/admin/squeak_admin_server_servicer.py +++ b/squeakserver/admin/squeak_admin_server_servicer.py @@ -34,6 +34,15 @@ class SqueakAdminServerServicer(squeak_admin_pb2_grpc.SqueakAdminServicer): profile_id = self.handler.handle_create_signing_profile(profile_name) return squeak_admin_pb2.CreateSigningProfileReply(profile_id=profile_id,) + def CreateContactProfile(self, request, context): + profile_name = request.profile_name + squeak_address = request.address + profile_id = self.handler.handle_create_contact_profile( + profile_name, + squeak_address, + ) + return squeak_admin_pb2.CreateContactProfileReply(profile_id=profile_id,) + def GetSigningProfiles(self, request, context): profiles = self.handler.handle_get_signing_profiles() profile_msgs = [ @@ -45,6 +54,17 @@ class SqueakAdminServerServicer(squeak_admin_pb2_grpc.SqueakAdminServicer): squeak_profiles=profile_msgs ) + def GetContactProfiles(self, request, context): + profiles = self.handler.handle_get_contact_profiles() + profile_msgs = [ + self._squeak_profile_to_message(profile) + for profile in + profiles + ] + return squeak_admin_pb2.GetContactProfilesReply( + squeak_profiles=profile_msgs + ) + def GetSqueakProfile(self, request, context): profile_id = request.profile_id squeak_profile = self.handler.handle_get_squeak_profile(profile_id) diff --git a/squeakserver/core/squeak_address_validator.py b/squeakserver/core/squeak_address_validator.py new file mode 100644 index 00000000..d0d8e795 --- /dev/null +++ b/squeakserver/core/squeak_address_validator.py @@ -0,0 +1,26 @@ +import logging + +from bitcoin.base58 import Base58ChecksumError +from bitcoin.wallet import CBitcoinAddressError +from squeak.core import CheckSqueak, CheckSqueakError, CSqueak +from squeak.core.signing import CSqueakAddress + +logger = logging.getLogger(__name__) + + +class SqueakAddressValidator(object): + """Validates addresses + """ + + def __init__(self) -> None: + pass + + def validate(self, squeak_address_str: str) -> bool: + if not squeak_address_str: + return False + try: + CSqueakAddress(squeak_address_str) + return True + except (Base58ChecksumError, CBitcoinAddressError) as e: + logger.info("Got invalid address with error: {}".format(e)) + return False diff --git a/squeakserver/node/squeak_node.py b/squeakserver/node/squeak_node.py index 6220bdae..353492ba 100644 --- a/squeakserver/node/squeak_node.py +++ b/squeakserver/node/squeak_node.py @@ -4,6 +4,7 @@ from squeak.core.encryption import ( ) from squeak.core.signing import CSigningKey, CSqueakAddress +from squeakserver.core.squeak_address_validator import SqueakAddressValidator from squeakserver.node.squeak_block_periodic_worker import SqueakBlockPeriodicWorker from squeakserver.node.squeak_block_queue_worker import SqueakBlockQueueWorker from squeakserver.node.squeak_block_verifier import SqueakBlockVerifier @@ -111,9 +112,26 @@ class SqueakNode: ) return self.postgres_db.insert_profile(squeak_profile) + def create_contact_profile(self, profile_name, squeak_address): + address_validator = SqueakAddressValidator() + if not address_validator.validate(squeak_address): + raise Exception('Invalid squeak address: {}'.format(squeak_address)) + squeak_profile = SqueakProfile( + profile_id=None, + profile_name=profile_name, + private_key=None, + address=squeak_address, + sharing=False, + following=False, + ) + return self.postgres_db.insert_profile(squeak_profile) + def get_signing_profiles(self): return self.postgres_db.get_signing_profiles() + def get_contact_profiles(self): + return self.postgres_db.get_contact_profiles() + def get_squeak_profile(self, profile_id): return self.postgres_db.get_profile(profile_id) diff --git a/squeakserver/server/postgres_db.py b/squeakserver/server/postgres_db.py index 2a62db8f..46715bb8 100644 --- a/squeakserver/server/postgres_db.py +++ b/squeakserver/server/postgres_db.py @@ -188,7 +188,7 @@ class PostgresDb: return row["profile_id"] def get_signing_profiles(self): - """ Get a profile. """ + """ Get all signing profiles. """ sql = """ SELECT * FROM profile WHERE private_key IS NOT NULL; @@ -199,6 +199,18 @@ class PostgresDb: profiles = [self._parse_squeak_profile(row) for row in rows] return profiles + def get_contact_profiles(self): + """ Get all contact profiles. """ + sql = """ + SELECT * FROM profile + WHERE private_key IS NULL; + """ + with self.get_cursor() as curs: + curs.execute(sql) + rows = curs.fetchall() + profiles = [self._parse_squeak_profile(row) for row in rows] + return profiles + def get_profile(self, profile_id): """ Get a profile. """ sql = """