Add global sell price setting (#1791)

* Add sell price setting to db

* Add rpc method for set and get sell price setting

* Fix servicer methods for get and set sell price

* Add settings page in frontend

* Include default sell price in get sell price rpc

* Show sell price in settings page

* Got set sell price working in frontend

* Remove frontend validation of sell price dialog

* Use sell price from settings when determining price

* Add rpc to clear sell price setting

* Add clear sell price to frontend

* Remove unused imports in frontend

* Update frontend build
This commit is contained in:
Jonathan Zernik 2021-11-10 01:53:55 -08:00 committed by GitHub
parent 3674595683
commit 488ed5dfc4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 937 additions and 28 deletions

View file

@ -0,0 +1,88 @@
import React from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
FormControl,
FormLabel,
} from '@material-ui/core';
// styles
import useStyles from './styles';
import {
clearSellPriceRequest,
} from '../../squeakclient/requests';
export default function ClearSellPriceDialog({
open,
handleClose,
reloadSellPriceFn,
...props
}) {
const classes = useStyles();
const clearSellPrice = () => {
clearSellPriceRequest(() => {
reloadSellPriceFn();
});
};
function handleSubmit(event) {
event.preventDefault();
console.log('clear price msat');
clearSellPrice();
handleClose();
}
function CancelButton() {
return (
<Button
onClick={handleClose}
variant="contained"
color="secondary"
>
Cancel
</Button>
);
}
function SubmitButton() {
return (
<Button
type="submit"
variant="contained"
color="primary"
className={classes.button}
>
Clear Sell Price
</Button>
);
}
function SellPriceForm() {
return (
<FormControl component="fieldset">
<FormLabel component="legend">Clear Sell Price</FormLabel>
</FormControl>
);
}
return (
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">Clear Sell Price</DialogTitle>
<form className={classes.root} onSubmit={handleSubmit} noValidate autoComplete="off">
<DialogContent>
{SellPriceForm()}
</DialogContent>
<DialogActions>
{CancelButton()}
{SubmitButton()}
</DialogActions>
</form>
</Dialog>
);
}

View file

@ -0,0 +1,6 @@
{
"name": "ClearSellPriceDialog",
"version": "0.0.0",
"private": true,
"main": "ClearSellPriceDialog.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

@ -40,6 +40,7 @@ import {
import {
reloadRoute,
goToSearchPage,
goToSettingsPage,
} from '../../navigation/navigation';
const notifications = [];
@ -206,6 +207,19 @@ export default function Header(props) {
{network}
</Typography>
</div>
<div className={classes.profileMenuUser}>
<Typography
className={classes.settingsMenuLink}
color="primary"
onClick={() => {
goToSettingsPage(history)
setProfileMenu(null);
}
}
>
Settings
</Typography>
</div>
<div className={classes.profileMenuUser}>
<Typography
className={classes.profileMenuLink}

View file

@ -129,6 +129,13 @@ export default makeStyles((theme) => ({
cursor: 'pointer',
},
},
settingsMenuLink: {
fontSize: 16,
textDecoration: 'none',
'&:hover': {
cursor: 'pointer',
},
},
messageNotification: {
height: 'auto',
display: 'flex',

View file

@ -37,6 +37,7 @@ import Peers from '../../pages/peers';
import Peer from '../../pages/peer';
import PeerAddress from '../../pages/peeraddress';
import Twitter from '../../pages/twitter';
import Settings from '../../pages/settings';
// context
import { useLayoutState } from '../../context/LayoutContext';
@ -81,6 +82,7 @@ function Layout(props) {
<Route path="/app/peeraddress/:network/:host/:port" component={PeerAddress} />
<Route path="/app/notifications" component={Notifications} />
<Route path="/app/twitter" component={Twitter} />
<Route path="/app/settings" component={Settings} />
<Route
exact
path="/app/ui"

View file

@ -0,0 +1,133 @@
import React, { useState } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
FormControl,
FormLabel,
TextField,
} from '@material-ui/core';
// styles
import useStyles from './styles';
import {
setSellPriceRequest,
} from '../../squeakclient/requests';
export default function SetSellPriceDialog({
open,
handleClose,
reloadSellPriceFn,
...props
}) {
const classes = useStyles();
const [sellPriceMsat, setSellPriceMsat] = useState(0);
const setSellPrice = (priceMsat) => {
setSellPriceRequest(priceMsat, () => {
reloadSellPriceFn();
});
};
// const setUseCustomPrice = (id, useCustomPrice) => {
// setSqueakProfileUseCustomPriceRequest(id, useCustomPrice, () => {
// reloadProfile();
// });
// };
//
// const setCustomPriceMsat = (id, customPriceMsat) => {
// setSqueakProfileCustomPriceRequest(id, customPriceMsat, () => {
// reloadProfile();
// });
// };
// const handleSettingsFollowingChange = (event) => {
// console.log(`Following changed for profile id: ${squeakProfile.getProfileId()}`);
// console.log(`Following changed to: ${event.target.checked}`);
// setFollowing(squeakProfile.getProfileId(), event.target.checked);
// };
//
// const handleSettingsUseCustomPriceChange = (event) => {
// console.log(`UseCustomPrice changed for profile id: ${squeakProfile.getProfileId()}`);
// console.log(`UseCustomPrice changed to: ${event.target.checked}`);
// setUseCustomPrice(squeakProfile.getProfileId(), event.target.checked);
// };
const handlePriceMsatChange = (event) => {
console.log(`Price changed:`);
const newPriceMsat = event.target.value;
console.log(`Price changed to: ${newPriceMsat}`);
setSellPriceMsat(newPriceMsat);
};
function handleSubmit(event) {
event.preventDefault();
console.log('sell price msat:', sellPriceMsat);
if (!sellPriceMsat) {
alert('Sell price cannot be empty.');
return;
}
setSellPrice(sellPriceMsat);
handleClose();
}
function CancelButton() {
return (
<Button
onClick={handleClose}
variant="contained"
color="secondary"
>
Cancel
</Button>
);
}
function SubmitButton() {
return (
<Button
type="submit"
variant="contained"
color="primary"
className={classes.button}
>
Set Sell Price
</Button>
);
}
function SellPriceForm() {
return (
<FormControl component="fieldset">
<FormLabel component="legend">Set Sell Price</FormLabel>
<TextField
required
id="standard-required"
label="Price (msats)"
type="number"
defaultValue={0}
onChange={handlePriceMsatChange}
/>
</FormControl>
);
}
return (
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">Sell Price</DialogTitle>
<form className={classes.root} onSubmit={handleSubmit} noValidate autoComplete="off">
<DialogContent>
{SellPriceForm()}
</DialogContent>
<DialogActions>
{CancelButton()}
{SubmitButton()}
</DialogActions>
</form>
</Dialog>
);
}

View file

@ -0,0 +1,6 @@
{
"name": "SetSellPriceDialog",
"version": "0.0.0",
"private": true,
"main": "SetSellPriceDialog.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

@ -52,3 +52,7 @@ export const goToSentPaymentsPage = (history) => {
export const goToReceivedPaymentsPage = (history) => {
history.push('/app/receivedpayments/');
};
export const goToSettingsPage = (history) => {
history.push('/app/settings/');
};

View file

@ -0,0 +1,213 @@
import React, { useState, useEffect, useCallback } from 'react';
import {
Grid,
Button,
Tabs,
Tab,
AppBar,
Box,
FormLabel,
Typography,
} from '@material-ui/core';
// components
import Widget from '../../components/Widget';
import SetSellPriceDialog from '../../components/SetSellPriceDialog';
import ClearSellPriceDialog from '../../components/ClearSellPriceDialog';
import {
getSellPriceRequest,
} from '../../squeakclient/requests';
export default function Settings() {
const [value, setValue] = useState(0);
const [sellPriceMsat, setSellPriceMsat] = useState(null);
const [waitingForSellPriceMsat, setWaitingForSellPriceMsat] = useState(false);
const [setSellPriceDialogOpen, setSetSellPriceDialogOpen] = useState(false);
const [clearSellPriceDialogOpen, setClearSellPriceDialogOpen] = useState(false);
function a11yProps(index) {
return {
id: `simple-tab-${index}`,
'aria-controls': `simple-tabpanel-${index}`,
};
}
const handleChange = (event, newValue) => {
setValue(newValue);
};
const handleCloseSetSellPriceDialog = () => {
setSetSellPriceDialogOpen(false);
};
const handleClickSetSellPriceDialog = () => {
setSetSellPriceDialogOpen(true);
};
const handleCloseClearSellPriceDialog = () => {
setClearSellPriceDialogOpen(false);
};
const handleClickClearSellPriceDialog = () => {
setClearSellPriceDialogOpen(true);
};
const loadSellPrice = useCallback(() => {
setWaitingForSellPriceMsat(true);
getSellPriceRequest((resp => {
setWaitingForSellPriceMsat(false);
console.log(resp);
setSellPriceMsat(resp);
}));
},
[]);
useEffect(() => {
loadSellPrice();
}, [loadSellPrice]);
function TabPanel(props) {
const {
children, value, index, ...other
} = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && (
<div>{children}</div>
)}
</div>
);
}
function SentPaymentContent() {
const usingDefault = !sellPriceMsat.getPriceMsatIsSet();
const priceSats = sellPriceMsat.getPriceMsat() / 1000;
const defaultPriceSats = sellPriceMsat.getDefaultPriceMsat() / 1000;
console.log('sell price' + priceSats);
console.log('default sell price' + defaultPriceSats);
console.log('use default' + usingDefault);
return (
<Grid container spacing={2}>
<Grid item xs={12}>
<Widget disableWidgetMenu>
<Grid item>
<FormLabel>
Sell price
</FormLabel>
<Typography size="md">
{usingDefault
? defaultPriceSats
: priceSats}
{' sats'}
{usingDefault && ' (using default)'}
</Typography>
{SetSellPriceButtonContent()}
{ClearSellPriceButtonContent()}
</Grid>
</Widget>
</Grid>
</Grid>
);
}
function SettingsTabs() {
return (
<>
<AppBar position="static" color="default">
<Tabs value={value} onChange={handleChange} aria-label="simple tabs example">
<Tab label="Settings" {...a11yProps(0)} />
</Tabs>
</AppBar>
<TabPanel value={value} index={0}>
{sellPriceMsat && SentPaymentContent()}
</TabPanel>
</>
);
}
function GridContent() {
return (
<Grid container spacing={0}>
<Grid item xs={12} sm={9}>
{SettingsTabs()}
</Grid>
<Grid item xs={12} sm={3} />
</Grid>
);
}
function SetSellPriceDialogContent() {
return (
<>
<SetSellPriceDialog
open={setSellPriceDialogOpen}
handleClose={handleCloseSetSellPriceDialog}
reloadSellPriceFn={loadSellPrice}
/>
</>
);
}
function ClearSellPriceDialogContent() {
return (
<>
<ClearSellPriceDialog
open={clearSellPriceDialogOpen}
handleClose={handleCloseClearSellPriceDialog}
reloadSellPriceFn={loadSellPrice}
/>
</>
);
}
function SetSellPriceButtonContent() {
return (
<>
<Box p={1}>
<Button
variant="contained"
onClick={handleClickSetSellPriceDialog}
>
Set Sell Price
</Button>
</Box>
</>
);
}
function ClearSellPriceButtonContent() {
return (
<>
<Box p={1}>
<Button
variant="contained"
onClick={handleClickClearSellPriceDialog}
>
Clear Sell Price
</Button>
</Box>
</>
);
}
return (
<>
{!waitingForSellPriceMsat && GridContent()}
{SetSellPriceDialogContent()}
{ClearSellPriceDialogContent()}
< />
);
}

View file

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

View file

@ -143,6 +143,12 @@ import {
DeleteTwitterAccountReply,
SetPeerShareForFreeRequest,
SetPeerShareForFreeReply,
SetSellPriceRequest,
SetSellPriceReply,
GetSellPriceRequest,
GetSellPriceReply,
ClearSellPriceRequest,
ClearSellPriceReply,
} from '../proto/squeak_admin_pb';
console.log('The value of REACT_APP_DEV_MODE_ENABLED is:', Boolean(process.env.REACT_APP_DEV_MODE_ENABLED));
@ -1263,6 +1269,39 @@ export function deleteTwitterAccountRequest(twitterAccountId, handleResponse) {
);
}
export function setSellPriceRequest(priceMsat, handleResponse) {
const request = new SetSellPriceRequest();
request.setPriceMsat(priceMsat);
makeRequest(
'setsellprice',
request,
SetSellPriceReply.deserializeBinary,
handleResponse,
);
}
export function clearSellPriceRequest(handleResponse) {
const request = new ClearSellPriceRequest();
makeRequest(
'clearsellprice',
request,
ClearSellPriceReply.deserializeBinary,
handleResponse,
);
}
export function getSellPriceRequest(handleResponse) {
const request = new GetSellPriceRequest();
makeRequest(
'getsellprice',
request,
GetSellPriceReply.deserializeBinary,
(response) => {
handleResponse(response);
},
);
}
// export function subscribeConnectedPeersRequest(handleResponse) {
// const request = new SubscribeConnectedPeersRequest();
// const stream = client.subscribeConnectedPeers(request);

View file

@ -31,6 +31,7 @@ from squeak.core import CSqueak
from proto import lnd_pb2 as ln
from proto import squeak_admin_pb2
from tests.util import channel
from tests.util import clear_sell_price
from tests.util import connect_squeak_peer
from tests.util import create_contact_profile
from tests.util import create_saved_peer
@ -49,6 +50,7 @@ from tests.util import get_hash
from tests.util import get_network
from tests.util import get_peer_by_address
from tests.util import get_search_squeaks
from tests.util import get_sell_price
from tests.util import get_squeak_display
from tests.util import get_squeak_profile
from tests.util import get_twitter_bearer_token
@ -57,6 +59,7 @@ from tests.util import make_squeak
from tests.util import open_peer_connection
from tests.util import peer_connection
from tests.util import send_coins
from tests.util import set_sell_price
from tests.util import set_twitter_bearer_token
from tests.util import subscribe_connected_peers
from tests.util import subscribe_squeak_ancestor_entries
@ -71,6 +74,29 @@ def test_get_network(admin_stub):
assert network == "simnet"
def test_get_sell_price(admin_stub):
# Get the sell price
price = get_sell_price(admin_stub)
assert price.price_msat == 0
assert not price.price_msat_is_set
assert price.default_price_msat == 1000000
set_sell_price(admin_stub, 98765)
price = get_sell_price(admin_stub)
assert price.price_msat == 98765
assert price.price_msat_is_set
assert price.default_price_msat == 1000000
clear_sell_price(admin_stub)
price = get_sell_price(admin_stub)
assert price.price_msat == 0
assert not price.price_msat_is_set
assert price.default_price_msat == 1000000
def test_get_twitter_bearer_token(admin_stub):
# Get the twitter bearer token
bearer_token = get_twitter_bearer_token(admin_stub)

View file

@ -307,6 +307,27 @@ def get_network(node_stub):
return get_network_response.network
def set_sell_price(node_stub, price_msat):
node_stub.SetSellPrice(
squeak_admin_pb2.SetSellPriceRequest(
price_msat=price_msat,
)
)
def clear_sell_price(node_stub):
node_stub.ClearSellPrice(
squeak_admin_pb2.ClearSellPriceRequest()
)
def get_sell_price(node_stub):
get_sell_price_response = node_stub.GetSellPrice(
squeak_admin_pb2.GetSellPriceRequest()
)
return get_sell_price_response
def set_twitter_bearer_token(node_stub, bearer_token):
node_stub.SetTwitterBearerToken(
squeak_admin_pb2.SetTwitterBearerTokenRequest(

View file

@ -336,6 +336,18 @@ service SqueakAdmin {
*/
rpc GetDefaultPeerPort (GetDefaultPeerPortRequest) returns (GetDefaultPeerPortReply) {}
/** sqkadmin: `setsellprice`
*/
rpc SetSellPrice (SetSellPriceRequest) returns (SetSellPriceReply) {}
/** sqkadmin: `clearsellprice`
*/
rpc ClearSellPrice (ClearSellPriceRequest) returns (ClearSellPriceReply) {}
/** sqkadmin: `getsellprice`
*/
rpc GetSellPrice (GetSellPriceRequest) returns (GetSellPriceReply) {}
/** sqkadmin: `settwitterbearertoken`
*/
rpc SetTwitterBearerToken (SetTwitterBearerTokenRequest) returns (SetTwitterBearerTokenReply) {}
@ -1262,6 +1274,34 @@ message GetDefaultPeerPortReply {
int32 port = 1;
}
message SetSellPriceRequest {
/// The price in msats
int64 price_msat = 1;
}
message SetSellPriceReply {
}
message ClearSellPriceRequest {
}
message ClearSellPriceReply {
}
message GetSellPriceRequest {
}
message GetSellPriceReply {
/// The price in msats
int64 price_msat = 1;
/// Price is set
bool price_msat_is_set = 2;
/// The defauly price in msats
int64 default_price_msat = 3;
}
message SetTwitterBearerTokenRequest {
/// The bearer token string.
string bearer_token = 1;

View file

@ -1067,6 +1067,34 @@ class SqueakAdminServerHandler(object):
port=default_peer_port,
)
def handle_set_sell_price(self, request):
sell_price_msat = request.price_msat
logger.info("Handle set sell price msat to: {}".format(
sell_price_msat,
))
self.squeak_controller.set_sell_price_msat(sell_price_msat)
return squeak_admin_pb2.SetSellPriceReply()
def handle_clear_sell_price(self, request):
logger.info("Handle clear sell price.")
self.squeak_controller.clear_sell_price_msat()
return squeak_admin_pb2.ClearSellPriceReply()
def handle_get_sell_price(self, request):
logger.info("Handle get sell price")
sell_price_msat = self.squeak_controller.get_sell_price_msat()
price_msat_is_set = sell_price_msat is not None
default_sell_price_msat = self.squeak_controller.get_default_sell_price_msat()
logger.info("sell price: {}".format(sell_price_msat))
logger.info("price_msat_is_set: {}".format(price_msat_is_set))
logger.info("default_sell_price_msat: {}".format(
default_sell_price_msat))
return squeak_admin_pb2.GetSellPriceReply(
price_msat=sell_price_msat,
price_msat_is_set=price_msat_is_set,
default_price_msat=default_sell_price_msat,
)
def handle_set_twitter_bearer_token(self, request):
twitter_bearer_token = request.bearer_token
logger.info("Handle set twitter bearer token with value: {}".format(

View file

@ -386,6 +386,15 @@ class SqueakAdminServerServicer(squeak_admin_pb2_grpc.SqueakAdminServicer):
def GetDefaultPeerPort(self, request, context):
return self.handler.handle_get_default_peer_port(request)
def SetSellPrice(self, request, context):
return self.handler.handle_set_sell_price(request)
def ClearSellPrice(self, request, context):
return self.handler.handle_clear_sell_price(request)
def GetSellPrice(self, request, context):
return self.handler.handle_get_sell_price(request)
def SetTwitterBearerToken(self, request, context):
return self.handler.handle_set_twitter_bearer_token(request)

View file

@ -529,6 +529,24 @@ def create_app(handler, username, password):
def getdefaultpeerport(msg):
return handler.handle_get_default_peer_port(msg)
@app.route("/setsellprice", methods=["POST"])
@login_required
@protobuf_serialized(squeak_admin_pb2.SetSellPriceRequest())
def setsellprice(msg):
return handler.handle_set_sell_price(msg)
@app.route("/clearsellprice", methods=["POST"])
@login_required
@protobuf_serialized(squeak_admin_pb2.ClearSellPriceRequest())
def clearsellprice(msg):
return handler.handle_clear_sell_price(msg)
@app.route("/getsellprice", methods=["POST"])
@login_required
@protobuf_serialized(squeak_admin_pb2.GetSellPriceRequest())
def getsellprice(msg):
return handler.handle_get_sell_price(msg)
@app.route("/settwitterbearertoken", methods=["POST"])
@login_required
@protobuf_serialized(squeak_admin_pb2.SetTwitterBearerTokenRequest())

View file

@ -1,17 +1,17 @@
{
"files": {
"main.js": "/static/js/main.9f21cade.chunk.js",
"main.js.map": "/static/js/main.9f21cade.chunk.js.map",
"main.js": "/static/js/main.944b5f3d.chunk.js",
"main.js.map": "/static/js/main.944b5f3d.chunk.js.map",
"runtime-main.js": "/static/js/runtime-main.9f0ba400.js",
"runtime-main.js.map": "/static/js/runtime-main.9f0ba400.js.map",
"static/css/2.ea4ba2f0.chunk.css": "/static/css/2.ea4ba2f0.chunk.css",
"static/js/2.8ecc8a7d.chunk.js": "/static/js/2.8ecc8a7d.chunk.js",
"static/js/2.8ecc8a7d.chunk.js.map": "/static/js/2.8ecc8a7d.chunk.js.map",
"static/js/2.180244ea.chunk.js": "/static/js/2.180244ea.chunk.js",
"static/js/2.180244ea.chunk.js.map": "/static/js/2.180244ea.chunk.js.map",
"index.html": "/index.html",
"precache-manifest.2a415349b9ea2acdcf58091b9ea62b17.js": "/precache-manifest.2a415349b9ea2acdcf58091b9ea62b17.js",
"precache-manifest.4e0fbc5da97436978be7e48838bc6485.js": "/precache-manifest.4e0fbc5da97436978be7e48838bc6485.js",
"service-worker.js": "/service-worker.js",
"static/css/2.ea4ba2f0.chunk.css.map": "/static/css/2.ea4ba2f0.chunk.css.map",
"static/js/2.8ecc8a7d.chunk.js.LICENSE.txt": "/static/js/2.8ecc8a7d.chunk.js.LICENSE.txt",
"static/js/2.180244ea.chunk.js.LICENSE.txt": "/static/js/2.180244ea.chunk.js.LICENSE.txt",
"static/media/font-awesome.min.css": "/static/media/fontawesome-webfont.fee66e71.woff",
"static/media/google.svg": "/static/media/google.695a3160.svg",
"static/media/logo.svg": "/static/media/logo.a0185b04.svg"
@ -19,7 +19,7 @@
"entrypoints": [
"static/js/runtime-main.9f0ba400.js",
"static/css/2.ea4ba2f0.chunk.css",
"static/js/2.8ecc8a7d.chunk.js",
"static/js/main.9f21cade.chunk.js"
"static/js/2.180244ea.chunk.js",
"static/js/main.944b5f3d.chunk.js"
]
}

View file

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="shortcut icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"/><meta name="theme-color" content="#000000"/><link rel="manifest" href="/manifest.json"/><title>Squeaknode</title><meta name="description" content="Squeaknode is a frontend for accessing a squeak node"><meta name="keywords" content="squeak, bitcoin, lightning"><meta name="author" content="Flatlogic LLC."><link href="/static/css/2.ea4ba2f0.chunk.css" rel="stylesheet"></head><body style="font-family:Roboto,sans-serif"><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(e){function r(r){for(var n,f,l=r[0],a=r[1],i=r[2],c=0,s=[];c<l.length;c++)f=l[c],Object.prototype.hasOwnProperty.call(o,f)&&o[f]&&s.push(o[f][0]),o[f]=0;for(n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n]);for(p&&p(r);s.length;)s.shift()();return u.push.apply(u,i||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,l=1;l<t.length;l++){var a=t[l];0!==o[a]&&(n=!1)}n&&(u.splice(r--,1),e=f(f.s=t[0]))}return e}var n={},o={1:0},u=[];function f(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,f),t.l=!0,t.exports}f.m=e,f.c=n,f.d=function(e,r,t){f.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},f.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},f.t=function(e,r){if(1&r&&(e=f(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(f.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)f.d(t,n,function(r){return e[r]}.bind(null,n));return t},f.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return f.d(r,"a",r),r},f.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},f.p="/";var l=this["webpackJsonpsqueak-node-frontend"]=this["webpackJsonpsqueak-node-frontend"]||[],a=l.push.bind(l);l.push=r,l=l.slice();for(var i=0;i<l.length;i++)r(l[i]);var p=a;t()}([])</script><script src="/static/js/2.8ecc8a7d.chunk.js"></script><script src="/static/js/main.9f21cade.chunk.js"></script></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="shortcut icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"/><meta name="theme-color" content="#000000"/><link rel="manifest" href="/manifest.json"/><title>Squeaknode</title><meta name="description" content="Squeaknode is a frontend for accessing a squeak node"><meta name="keywords" content="squeak, bitcoin, lightning"><meta name="author" content="Flatlogic LLC."><link href="/static/css/2.ea4ba2f0.chunk.css" rel="stylesheet"></head><body style="font-family:Roboto,sans-serif"><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(e){function r(r){for(var n,f,l=r[0],a=r[1],i=r[2],c=0,s=[];c<l.length;c++)f=l[c],Object.prototype.hasOwnProperty.call(o,f)&&o[f]&&s.push(o[f][0]),o[f]=0;for(n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n]);for(p&&p(r);s.length;)s.shift()();return u.push.apply(u,i||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,l=1;l<t.length;l++){var a=t[l];0!==o[a]&&(n=!1)}n&&(u.splice(r--,1),e=f(f.s=t[0]))}return e}var n={},o={1:0},u=[];function f(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,f),t.l=!0,t.exports}f.m=e,f.c=n,f.d=function(e,r,t){f.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},f.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},f.t=function(e,r){if(1&r&&(e=f(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(f.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)f.d(t,n,function(r){return e[r]}.bind(null,n));return t},f.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return f.d(r,"a",r),r},f.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},f.p="/";var l=this["webpackJsonpsqueak-node-frontend"]=this["webpackJsonpsqueak-node-frontend"]||[],a=l.push.bind(l);l.push=r,l=l.slice();for(var i=0;i<l.length;i++)r(l[i]);var p=a;t()}([])</script><script src="/static/js/2.180244ea.chunk.js"></script><script src="/static/js/main.944b5f3d.chunk.js"></script></body></html>

View file

@ -1,23 +1,23 @@
self.__precacheManifest = (self.__precacheManifest || []).concat([
{
"revision": "4130c19a04cad84365d566866ca6d862",
"revision": "3f3a1b0ed1a3bea48e2734d35986cea1",
"url": "/index.html"
},
{
"revision": "e75dba031da32477061e",
"revision": "36d7b205176816f91b11",
"url": "/static/css/2.ea4ba2f0.chunk.css"
},
{
"revision": "e75dba031da32477061e",
"url": "/static/js/2.8ecc8a7d.chunk.js"
"revision": "36d7b205176816f91b11",
"url": "/static/js/2.180244ea.chunk.js"
},
{
"revision": "ce334ad48c4dc2f747f813c26790dfba",
"url": "/static/js/2.8ecc8a7d.chunk.js.LICENSE.txt"
"url": "/static/js/2.180244ea.chunk.js.LICENSE.txt"
},
{
"revision": "6d9bdb1984d80a44b61e",
"url": "/static/js/main.9f21cade.chunk.js"
"revision": "229ed8e46986393e095e",
"url": "/static/js/main.944b5f3d.chunk.js"
},
{
"revision": "cc9816de5a8639d377ea",

View file

@ -14,7 +14,7 @@
importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js");
importScripts(
"/precache-manifest.2a415349b9ea2acdcf58091b9ea62b17.js"
"/precache-manifest.4e0fbc5da97436978be7e48838bc6485.js"
);
self.addEventListener('message', (event) => {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -27,3 +27,4 @@ class UserConfig(NamedTuple):
"""Represents a config for a user."""
username: str
twitter_bearer_token: Optional[str] = None
sell_price_msat: Optional[int] = None

View file

@ -0,0 +1,48 @@
# MIT License
#
# Copyright (c) 2020 Jonathan Zernik
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Add sell price column to config table.
Revision ID: 5bd8f4075339
Revises: c2d80c9fcbfa
Create Date: 2021-11-08 21:04:08.193324
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = '5bd8f4075339'
down_revision = 'c2d80c9fcbfa'
branch_labels = None
depends_on = None
def upgrade():
with op.batch_alter_table('config', schema=None) as batch_op:
batch_op.add_column(sa.Column('sell_price_msat',
sa.Integer(), nullable=True))
def downgrade():
with op.batch_alter_table('config', schema=None) as batch_op:
batch_op.drop_column('sell_price_msat')

View file

@ -186,6 +186,7 @@ class Models:
self.metadata,
Column("username", String, primary_key=True),
Column("twitter_bearer_token", String, nullable=True),
Column("sell_price_msat", Integer, nullable=True),
)
self.twitter_accounts = Table(

View file

@ -1403,6 +1403,26 @@ class SqueakDb:
with self.get_connection() as connection:
connection.execute(stmt)
def set_config_sell_price_msat(self, username: str, sell_price_msat: int) -> None:
""" Set a config sell price msat. """
stmt = (
self.configs.update()
.where(self.configs.c.username == username)
.values(sell_price_msat=sell_price_msat)
)
with self.get_connection() as connection:
connection.execute(stmt)
def clear_config_sell_price_msat(self, username: str) -> None:
""" Clear a config sell price msat. """
stmt = (
self.configs.update()
.where(self.configs.c.username == username)
.values(sell_price_msat=None)
)
with self.get_connection() as connection:
connection.execute(stmt)
def insert_twitter_account(self, twitter_account: TwitterAccount) -> Optional[int]:
""" Insert a new twitter account mapping to a squeak profile.
@ -1595,6 +1615,7 @@ class SqueakDb:
return UserConfig(
username=row["username"],
twitter_bearer_token=row["twitter_bearer_token"],
sell_price_msat=row["sell_price_msat"],
)
def _parse_twitter_account_entry(self, row) -> TwitterAccountEntry:

View file

@ -28,6 +28,7 @@ from squeaknode.config.config import SqueaknodeConfig
from squeaknode.core.peer_address import PeerAddress
from squeaknode.core.squeak_peer import SqueakPeer
from squeaknode.core.squeak_profile import SqueakProfile
from squeaknode.core.user_config import UserConfig
from squeaknode.db.squeak_db import SqueakDb
@ -53,6 +54,10 @@ class PricePolicy:
squeak_profile = self.get_profile(squeak_address)
if squeak_profile is not None and squeak_profile.use_custom_price:
return squeak_profile.custom_price_msat
# Return sell price from settings if configured
sell_price = self.get_sell_price_msat()
if sell_price is not None:
return sell_price
return self.get_default_price()
def get_peer(self, peer_address: PeerAddress) -> Optional[SqueakPeer]:
@ -63,3 +68,14 @@ class PricePolicy:
def get_default_price(self) -> int:
return self.config.node.price_msat
def get_user_config(self) -> Optional[UserConfig]:
return self.squeak_db.get_config(
username=self.config.webadmin.username,
)
def get_sell_price_msat(self) -> Optional[int]:
user_config = self.get_user_config()
if user_config is None:
return None
return user_config.sell_price_msat

View file

@ -858,6 +858,32 @@ class SqueakController:
user_config = UserConfig(username=self.config.webadmin.username)
return self.squeak_db.insert_config(user_config)
def set_sell_price_msat(self, sell_price_msat: int) -> None:
self.insert_user_config()
if sell_price_msat < 0:
raise Exception("Sell price cannot be negative.")
self.squeak_db.set_config_sell_price_msat(
username=self.config.webadmin.username,
sell_price_msat=sell_price_msat,
)
def clear_sell_price_msat(self) -> None:
self.insert_user_config()
self.squeak_db.clear_config_sell_price_msat(
username=self.config.webadmin.username,
)
def get_sell_price_msat(self) -> Optional[int]:
user_config = self.squeak_db.get_config(
username=self.config.webadmin.username,
)
if user_config is None:
return None
return user_config.sell_price_msat
def get_default_sell_price_msat(self) -> int:
return self.config.node.price_msat
def set_twitter_bearer_token(self, twitter_bearer_token: str) -> None:
self.insert_user_config()
self.squeak_db.set_config_twitter_bearer_token(

View file

@ -423,6 +423,20 @@ def user_config_with_twitter_bearer_token_username(
yield inserted_user_config_username
@pytest.fixture
def user_config_with_sell_price_msat_username(
squeak_db,
user_config,
inserted_user_config_username,
price_msat,
):
squeak_db.set_config_sell_price_msat(
inserted_user_config_username,
price_msat,
)
yield inserted_user_config_username
@pytest.fixture
def twitter_account(inserted_signing_profile_id):
yield TwitterAccount(
@ -1608,6 +1622,17 @@ def test_set_twitter_bearer_token(
assert retrieved_config.twitter_bearer_token == twitter_bearer_token
def test_set_sell_price_msat(
squeak_db,
user_config_with_sell_price_msat_username,
price_msat,
):
retrieved_config = squeak_db.get_config(
user_config_with_sell_price_msat_username)
assert retrieved_config.sell_price_msat == price_msat
def test_get_twitter_account(
squeak_db,
twitter_account,

View file

@ -30,66 +30,91 @@ def price_policy():
yield PricePolicy(None, None)
def test_get_price(price_policy, squeak, peer_address):
def test_get_price(price_policy, squeak, peer_address, user_config):
with mock.patch.object(price_policy, 'get_peer', autospec=True) as mock_get_peer, \
mock.patch.object(price_policy, 'get_profile', autospec=True) as mock_get_profile, \
mock.patch.object(price_policy, 'get_user_config', autospec=True) as mock_get_user_config, \
mock.patch.object(price_policy, 'get_default_price', autospec=True) as mock_get_default_price:
mock_get_peer.return_value = None
mock_get_profile.return_value = None
mock_get_user_config.return_value = user_config
mock_get_default_price.return_value = 555
assert price_policy.get_price(squeak, peer_address) == 555
def test_get_price_profile_custom_price(price_policy, squeak, peer_address, signing_profile):
def test_get_price_profile_custom_price(price_policy, squeak, peer_address, signing_profile, user_config):
with mock.patch.object(price_policy, 'get_peer', autospec=True) as mock_get_peer, \
mock.patch.object(price_policy, 'get_profile', autospec=True) as mock_get_profile, \
mock.patch.object(price_policy, 'get_user_config', autospec=True) as mock_get_user_config, \
mock.patch.object(price_policy, 'get_default_price', autospec=True) as mock_get_default_price:
mock_get_peer.return_value = None
mock_get_profile.return_value = signing_profile._replace(
use_custom_price=True,
custom_price_msat=54321,
)
mock_get_user_config.return_value = user_config
mock_get_default_price.return_value = 555
assert price_policy.get_price(squeak, peer_address) == 54321
def test_get_price_profile_no_custom_price(price_policy, squeak, peer_address, signing_profile):
def test_get_price_profile_no_custom_price(price_policy, squeak, peer_address, signing_profile, user_config):
with mock.patch.object(price_policy, 'get_peer', autospec=True) as mock_get_peer, \
mock.patch.object(price_policy, 'get_profile', autospec=True) as mock_get_profile, \
mock.patch.object(price_policy, 'get_user_config', autospec=True) as mock_get_user_config, \
mock.patch.object(price_policy, 'get_default_price', autospec=True) as mock_get_default_price:
mock_get_peer.return_value = None
mock_get_profile.return_value = signing_profile._replace(
use_custom_price=False,
custom_price_msat=54321,
)
mock_get_user_config.return_value = user_config
mock_get_default_price.return_value = 555
assert price_policy.get_price(squeak, peer_address) == 555
def test_get_price_profile_share_free_peer(price_policy, squeak, peer_address, peer):
def test_get_price_profile_share_free_peer(price_policy, squeak, peer_address, peer, user_config):
with mock.patch.object(price_policy, 'get_peer', autospec=True) as mock_get_peer, \
mock.patch.object(price_policy, 'get_profile', autospec=True) as mock_get_profile, \
mock.patch.object(price_policy, 'get_user_config', autospec=True) as mock_get_user_config, \
mock.patch.object(price_policy, 'get_default_price', autospec=True) as mock_get_default_price:
mock_get_peer.return_value = peer._replace(
share_for_free=True,
)
mock_get_profile.return_value = None
mock_get_user_config.return_value = user_config
mock_get_default_price.return_value = 555
assert price_policy.get_price(squeak, peer_address) == 0
def test_get_price_profile_no_share_free_peer(price_policy, squeak, peer_address, peer):
def test_get_price_profile_no_share_free_peer(price_policy, squeak, peer_address, peer, user_config):
with mock.patch.object(price_policy, 'get_peer', autospec=True) as mock_get_peer, \
mock.patch.object(price_policy, 'get_profile', autospec=True) as mock_get_profile, \
mock.patch.object(price_policy, 'get_user_config', autospec=True) as mock_get_user_config, \
mock.patch.object(price_policy, 'get_default_price', autospec=True) as mock_get_default_price:
mock_get_peer.return_value = peer._replace(
share_for_free=False,
)
mock_get_profile.return_value = None
mock_get_user_config.return_value = user_config
mock_get_default_price.return_value = 555
assert price_policy.get_price(squeak, peer_address) == 555
def test_get_price_sell_price_set(price_policy, squeak, peer_address, user_config):
with mock.patch.object(price_policy, 'get_peer', autospec=True) as mock_get_peer, \
mock.patch.object(price_policy, 'get_profile', autospec=True) as mock_get_profile, \
mock.patch.object(price_policy, 'get_user_config', autospec=True) as mock_get_user_config, \
mock.patch.object(price_policy, 'get_default_price', autospec=True) as mock_get_default_price:
mock_get_peer.return_value = None
mock_get_profile.return_value = None
mock_get_user_config.return_value = user_config._replace(
sell_price_msat=7777,
)
mock_get_default_price.return_value = 555
assert price_policy.get_price(squeak, peer_address) == 7777