mirror of
https://github.com/yzernik/squeaknode.git
synced 2026-08-13 12:33:25 +02:00
Show payment summary for peer (#2160)
* Add endpoint for payment summary for peer * Got basic payment summary working for peer page * Improve display of address in peer page * Show peer address network in peer page * Add endpoints for get sent payments for peer and get received payments for peer. * Got sent payments for peer and received payments for peer working in frontend * Update frontend build
This commit is contained in:
parent
80a484ef3b
commit
6d2eeb856f
23 changed files with 594 additions and 54 deletions
|
|
@ -155,6 +155,12 @@ import {
|
|||
GetSentPaymentsForPubkeyReply,
|
||||
GetReceivedPaymentsForPubkeyRequest,
|
||||
GetReceivedPaymentsForPubkeyReply,
|
||||
GetPaymentSummaryForPeerRequest,
|
||||
GetPaymentSummaryForPeerReply,
|
||||
GetSentPaymentsForPeerRequest,
|
||||
GetSentPaymentsForPeerReply,
|
||||
GetReceivedPaymentsForPeerRequest,
|
||||
GetReceivedPaymentsForPeerReply,
|
||||
} from '../proto/squeak_admin_pb';
|
||||
|
||||
import axios from 'axios'
|
||||
|
|
@ -391,6 +397,22 @@ export const getPaymentSummaryForPubkey = (pubkey) => {
|
|||
});
|
||||
}
|
||||
|
||||
export const getPaymentSummaryForPeer = (network, host, port) => {
|
||||
console.log('Calling getPaymentSummaryForPeer');
|
||||
const request = new GetPaymentSummaryForPeerRequest();
|
||||
const peerAddress = new PeerAddress();
|
||||
peerAddress.setNetwork(network);
|
||||
peerAddress.setHost(host);
|
||||
peerAddress.setPort(port);
|
||||
request.setPeerAddress(peerAddress);
|
||||
const deser = GetPaymentSummaryForPeerReply.deserializeBinary;
|
||||
return baseRequest({
|
||||
url: '/getpaymentsummaryforpeer',
|
||||
req: request,
|
||||
deser: deser,
|
||||
});
|
||||
}
|
||||
|
||||
export const getSentPayments = (limit, lastSentPayment) => {
|
||||
console.log('Calling getSentPayments');
|
||||
const request = new GetSentPaymentsRequest();
|
||||
|
|
@ -438,6 +460,26 @@ export const getSentPaymentsForPubkey = (pubkey, limit, lastSentPayment) => {
|
|||
});
|
||||
}
|
||||
|
||||
export const getSentPaymentsForPeer = (network, host, port, limit, lastSentPayment) => {
|
||||
console.log('Calling getSentPaymentsForPubkey');
|
||||
const request = new GetSentPaymentsForPeerRequest();
|
||||
const peerAddress = new PeerAddress();
|
||||
peerAddress.setNetwork(network);
|
||||
peerAddress.setHost(host);
|
||||
peerAddress.setPort(port);
|
||||
request.setPeerAddress(peerAddress);
|
||||
request.setLimit(limit);
|
||||
if (lastSentPayment) {
|
||||
request.setLastSentPayment(lastSentPayment);
|
||||
}
|
||||
const deser = GetSentPaymentsForPeerReply.deserializeBinary;
|
||||
return baseRequest({
|
||||
url: '/getsentpaymentsforpeer',
|
||||
req: request,
|
||||
deser: deser,
|
||||
});
|
||||
}
|
||||
|
||||
export const getReceivedPayments = (limit, lastReceivedPayment) => {
|
||||
console.log('Calling getReceivedPayments');
|
||||
const request = new GetReceivedPaymentsRequest();
|
||||
|
|
@ -485,6 +527,26 @@ export const getReceivedPaymentsForPubkey = (pubkey, limit, lastReceivedPayment)
|
|||
});
|
||||
}
|
||||
|
||||
export const getReceivedPaymentsForPeer = (network, host, port, limit, lastReceivedPayment) => {
|
||||
console.log('Calling getReceivedPaymentsForPeer');
|
||||
const request = new GetReceivedPaymentsForPeerRequest();
|
||||
const peerAddress = new PeerAddress();
|
||||
peerAddress.setNetwork(network);
|
||||
peerAddress.setHost(host);
|
||||
peerAddress.setPort(port);
|
||||
request.setPeerAddress(peerAddress);
|
||||
request.setLimit(limit);
|
||||
if (lastReceivedPayment) {
|
||||
request.setLastReceivedPayment(lastReceivedPayment);
|
||||
}
|
||||
const deser = GetReceivedPaymentsForPeerReply.deserializeBinary;
|
||||
return baseRequest({
|
||||
url: '/getreceivedpaymentsforpeer',
|
||||
req: request,
|
||||
deser: deser,
|
||||
});
|
||||
}
|
||||
|
||||
export const getSearchSqueaks = (searchText, limit, lastSqueak) => {
|
||||
console.log('Calling getSearchSqueaks');
|
||||
const request = new GetSearchSqueakDisplaysRequest();
|
||||
|
|
|
|||
|
|
@ -23,6 +23,13 @@ import {
|
|||
setPeerAutoconnectEnabled,
|
||||
setPeerAutoconnectDisabled,
|
||||
} from '../../features/peers/peersSlice'
|
||||
import {
|
||||
fetchPaymentSummaryForPeer,
|
||||
selectPaymentSummaryForPeer,
|
||||
} from '../../features/payments/paymentsSlice'
|
||||
import SentPayments from '../../features/payments/SentPayments'
|
||||
import ReceivedPayments from '../../features/payments/ReceivedPayments'
|
||||
|
||||
|
||||
|
||||
const Peer = (props) => {
|
||||
|
|
@ -34,6 +41,7 @@ const Peer = (props) => {
|
|||
const [editModalOpen, setEditModalOpen] = useState(false)
|
||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false)
|
||||
const [savePeerModalOpen, setSavePeerModalOpen] = useState(false)
|
||||
const [spendingModalOpen, setSpendingModalOpen] = useState(false)
|
||||
const [banner, setBanner] = useState('')
|
||||
const [saved, setSaved] = useState(false)
|
||||
const [tab, setTab] = useState('Members')
|
||||
|
|
@ -41,6 +49,7 @@ const Peer = (props) => {
|
|||
const [styleBody, setStyleBody] = useState(false)
|
||||
|
||||
const peer = useSelector(selectCurrentPeer);
|
||||
const paymentSummary = useSelector(selectPaymentSummaryForPeer)
|
||||
const dispatch = useDispatch();
|
||||
|
||||
|
||||
|
|
@ -51,6 +60,11 @@ const Peer = (props) => {
|
|||
host: props.match.params.host,
|
||||
port: props.match.params.port,
|
||||
}));
|
||||
dispatch(fetchPaymentSummaryForPeer({
|
||||
network: props.match.params.network,
|
||||
host: props.match.params.host,
|
||||
port: props.match.params.port,
|
||||
}));
|
||||
}, [])
|
||||
|
||||
const isInitialMount = useRef(true);
|
||||
|
|
@ -140,6 +154,13 @@ const Peer = (props) => {
|
|||
setTimeout(()=>{ setSavePeerModalOpen(!savePeerModalOpen) },20)
|
||||
}
|
||||
|
||||
const toggleSpendingModal = (param, type) => {
|
||||
setStyleBody(!styleBody)
|
||||
if(type){setTab(type)}
|
||||
if(type){setTab(type)}
|
||||
setTimeout(()=>{ setSpendingModalOpen(!spendingModalOpen) },20)
|
||||
}
|
||||
|
||||
const handleModalClick = (e) => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
|
@ -177,40 +198,42 @@ const Peer = (props) => {
|
|||
</Form>
|
||||
);
|
||||
|
||||
const DeletePeerForm = () => (
|
||||
<Form onSubmit={deletePeer} className="Squeak-input-side">
|
||||
<div className="inner-input-links">
|
||||
<div className="input-links-side">
|
||||
</div>
|
||||
<div className="squeak-btn-holder">
|
||||
<div style={{ fontSize: '13px', color: null }}>
|
||||
</div>
|
||||
<button type="submit" className={'squeak-btn-side squeak-btn-active'}>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
const DeletePeerForm = () => (
|
||||
<Form onSubmit={deletePeer} className="Squeak-input-side">
|
||||
<div className="inner-input-links">
|
||||
<div className="input-links-side">
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
<div className="squeak-btn-holder">
|
||||
<div style={{ fontSize: '13px', color: null }}>
|
||||
</div>
|
||||
<button type="submit" className={'squeak-btn-side squeak-btn-active'}>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
|
||||
const EditPeerForm = () => (
|
||||
<Form onSubmit={editPeer} className="Squeak-input-side">
|
||||
<div className="edit-input-wrap">
|
||||
<Input class="informed-input" name="name" label="Peer Name" />
|
||||
const EditPeerForm = () => (
|
||||
<Form onSubmit={editPeer} className="Squeak-input-side">
|
||||
<div className="edit-input-wrap">
|
||||
<Input class="informed-input" name="name" label="Peer Name" />
|
||||
</div>
|
||||
<div className="inner-input-links">
|
||||
<div className="input-links-side">
|
||||
</div>
|
||||
<div className="inner-input-links">
|
||||
<div className="input-links-side">
|
||||
</div>
|
||||
<div className="squeak-btn-holder">
|
||||
<div style={{ fontSize: '13px', color: null }}>
|
||||
</div>
|
||||
<button type="submit" className={'squeak-btn-side squeak-btn-active'}>
|
||||
Submit
|
||||
</button>
|
||||
<div className="squeak-btn-holder">
|
||||
<div style={{ fontSize: '13px', color: null }}>
|
||||
</div>
|
||||
<button type="submit" className={'squeak-btn-side squeak-btn-active'}>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
|
||||
console.log(paymentSummary);
|
||||
|
||||
return(
|
||||
<div>
|
||||
|
|
@ -268,9 +291,25 @@ const Peer = (props) => {
|
|||
}
|
||||
</div>
|
||||
|
||||
<div className="profile-details-box">
|
||||
<div className="profile-name">{peer && peer.getPeerName()}</div>
|
||||
<div className="profile-username">{props.match.params.host}:{props.match.params.port}</div>
|
||||
<div className="profile-header-content">
|
||||
<div className="profile-header-name">
|
||||
<div className="profile-name">{peer && peer.getPeerName()}</div>
|
||||
<div className="profile-username">Network: {props.match.params.network}</div>
|
||||
<div className="profile-username">{props.match.params.host}:{props.match.params.port}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="profile-social-box">
|
||||
{/* TODO: Implement sats spent */}
|
||||
<div onClick={()=>toggleSpendingModal('members','Sent Payments')}>
|
||||
<p className="follow-num"> {paymentSummary && paymentSummary.getAmountSpentMsat() / 1000} </p>
|
||||
<p className="follow-text"> sats spent </p>
|
||||
</div>
|
||||
{/* TODO: Implement sats eaned */}
|
||||
<div onClick={()=>toggleSpendingModal('members', 'Received Payments')}>
|
||||
<p className="follow-num"> {paymentSummary && paymentSummary.getAmountEarnedMsat() / 1000} </p>
|
||||
<p className="follow-text"> sats earned </p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="profile-options">
|
||||
|
|
@ -286,14 +325,6 @@ const Peer = (props) => {
|
|||
|
||||
<div className="feed-wrapper">
|
||||
<div className="feed-trending-card">
|
||||
<div className="feed-card-trend">
|
||||
<div>Number of downloads</div>
|
||||
<div>TODO</div>
|
||||
</div>
|
||||
<div className="feed-card-trend">
|
||||
<div>Number of purchases</div>
|
||||
<div>TODO</div>
|
||||
</div>
|
||||
<div className="feed-card-trend">
|
||||
<div>Last connection time</div>
|
||||
<div>TODO</div>
|
||||
|
|
@ -358,6 +389,49 @@ const Peer = (props) => {
|
|||
</div>
|
||||
|
||||
|
||||
{/* Modal for sats spent and earned */}
|
||||
<div onClick={()=>toggleSpendingModal()} style={{display: spendingModalOpen ? 'block' : 'none'}} className="modal-edit">
|
||||
<div onClick={(e)=>handleModalClick(e)} className="modal-content">
|
||||
<div className="modal-header no-b-border">
|
||||
<div className="modal-closeIcon">
|
||||
<div onClick={()=>toggleSpendingModal()} className="modal-closeIcon-wrap">
|
||||
<ICON_CLOSE />
|
||||
</div>
|
||||
</div>
|
||||
<p className="modal-title">{null}</p>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="explore-nav-menu">
|
||||
<div onClick={()=>setTab('Sent Payments')} className={tab =='Sent Payments' ? `explore-nav-item activeTab` : `explore-nav-item`}>
|
||||
Sent Payments
|
||||
</div>
|
||||
<div onClick={()=>setTab('Received Payments')} className={tab =='Received Payments' ? `explore-nav-item activeTab` : `explore-nav-item`}>
|
||||
Received Payments
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-scroll">
|
||||
{tab === 'Sent Payments' ?
|
||||
<>
|
||||
<SentPayments network={props.match.params.network} host={props.match.params.host} port={props.match.params.port} />
|
||||
</>
|
||||
:
|
||||
tab === 'Received Payments' ?
|
||||
<>
|
||||
<ReceivedPayments network={props.match.params.network} host={props.match.params.host} port={props.match.params.port} />
|
||||
</>
|
||||
: <div className="try-searching">
|
||||
Nothing to see here ..
|
||||
<div/>
|
||||
Try searching for people, usernames, or keywords
|
||||
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ const ReceivedPayments = (props) => {
|
|||
dispatch(fetchReceivedPayments({
|
||||
squeakHash: props.squeakHash,
|
||||
pubkey: props.pubkey,
|
||||
network: props.network,
|
||||
host: props.host,
|
||||
port: props.port,
|
||||
limit: 10,
|
||||
lastReceivedPayment: null,
|
||||
}));
|
||||
|
|
@ -39,6 +42,9 @@ const ReceivedPayments = (props) => {
|
|||
dispatch(fetchReceivedPayments({
|
||||
squeakHash: props.squeakHash,
|
||||
pubkey: props.pubkey,
|
||||
network: props.network,
|
||||
host: props.host,
|
||||
port: props.port,
|
||||
limit: 10,
|
||||
lastReceivedPayment: lastReceivedPayment,
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ const SentPayments = (props) => {
|
|||
dispatch(fetchSentPayments({
|
||||
squeakHash: props.squeakHash,
|
||||
pubkey: props.pubkey,
|
||||
network: props.network,
|
||||
host: props.host,
|
||||
port: props.port,
|
||||
limit: 10,
|
||||
lastSentPayment: null,
|
||||
}));
|
||||
|
|
@ -39,6 +42,9 @@ const SentPayments = (props) => {
|
|||
dispatch(fetchSentPayments({
|
||||
squeakHash: props.squeakHash,
|
||||
pubkey: props.pubkey,
|
||||
network: props.network,
|
||||
host: props.host,
|
||||
port: props.port,
|
||||
limit: 10,
|
||||
lastSentPayment: lastSentPayment,
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -8,12 +8,15 @@ import {
|
|||
getSentPayments,
|
||||
getSentPaymentsForSqueak,
|
||||
getSentPaymentsForPubkey,
|
||||
getSentPaymentsForPeer,
|
||||
getReceivedPayments,
|
||||
getReceivedPaymentsForSqueak,
|
||||
getReceivedPaymentsForPubkey,
|
||||
getReceivedPaymentsForPeer,
|
||||
getPaymentSummary,
|
||||
getPaymentSummaryForSqueak,
|
||||
getPaymentSummaryForPubkey,
|
||||
getReceivedPaymentsForPubkey,
|
||||
getPaymentSummaryForPeer,
|
||||
} from '../../api/client'
|
||||
|
||||
const initialState = {
|
||||
|
|
@ -24,6 +27,7 @@ const initialState = {
|
|||
paymentSummary: null,
|
||||
paymentSummaryForSqueak: null,
|
||||
paymentSummaryForPubkey: null,
|
||||
paymentSummaryForPeer: null,
|
||||
}
|
||||
|
||||
// Thunk functions
|
||||
|
|
@ -40,6 +44,13 @@ export const fetchSentPayments = createAsyncThunk(
|
|||
values.limit,
|
||||
values.lastSentPayment,
|
||||
)
|
||||
: (values.network && values.host && values.port) ? await getSentPaymentsForPeer(
|
||||
values.network,
|
||||
values.host,
|
||||
values.port,
|
||||
values.limit,
|
||||
values.lastSentPayment,
|
||||
)
|
||||
: await getSentPayments(
|
||||
values.limit,
|
||||
values.lastSentPayment,
|
||||
|
|
@ -61,6 +72,13 @@ export const fetchReceivedPayments = createAsyncThunk(
|
|||
values.limit,
|
||||
values.lastReceivedPayment,
|
||||
)
|
||||
: (values.network && values.host && values.port) ? await getReceivedPaymentsForPeer(
|
||||
values.network,
|
||||
values.host,
|
||||
values.port,
|
||||
values.limit,
|
||||
values.lastReceivedPayment,
|
||||
)
|
||||
: await getReceivedPayments(
|
||||
values.limit,
|
||||
values.lastReceivedPayment,
|
||||
|
|
@ -96,6 +114,15 @@ export const fetchPaymentSummaryForPubkey = createAsyncThunk(
|
|||
}
|
||||
)
|
||||
|
||||
export const fetchPaymentSummaryForPeer = createAsyncThunk(
|
||||
'payments/fetchPaymentSummaryForPeer',
|
||||
async (values) => {
|
||||
console.log('Fetching paymentSummary for peer:', values.network, values.host, values.port);
|
||||
const response = await getPaymentSummaryForPeer(values.network, values.host, values.port);
|
||||
return response.getPaymentSummary();
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
const paymentsSlice = createSlice({
|
||||
name: 'sentPayments',
|
||||
|
|
@ -140,6 +167,10 @@ const paymentsSlice = createSlice({
|
|||
const paymentSummaryForPubkey = action.payload;
|
||||
state.paymentSummaryForPubkey = paymentSummaryForPubkey;
|
||||
})
|
||||
.addCase(fetchPaymentSummaryForPeer.fulfilled, (state, action) => {
|
||||
const paymentSummaryForPeer = action.payload;
|
||||
state.paymentSummaryForPeer = paymentSummaryForPeer;
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -173,3 +204,5 @@ export const selectPaymentSummary = state => state.payments.paymentSummary
|
|||
export const selectPaymentSummaryForSqueak = state => state.payments.paymentSummaryForSqueak
|
||||
|
||||
export const selectPaymentSummaryForPubkey = state => state.payments.paymentSummaryForPubkey
|
||||
|
||||
export const selectPaymentSummaryForPeer = state => state.payments.paymentSummaryForPeer
|
||||
|
|
|
|||
|
|
@ -236,6 +236,10 @@ service SqueakAdmin {
|
|||
*/
|
||||
rpc GetSentPaymentsForPubkey (GetSentPaymentsForPubkeyRequest) returns (GetSentPaymentsForPubkeyReply) {}
|
||||
|
||||
/** sqkadmin: `getsentpaymentsforpeer`
|
||||
*/
|
||||
rpc GetSentPaymentsForPeer (GetSentPaymentsForPeerRequest) returns (GetSentPaymentsForPeerReply) {}
|
||||
|
||||
/** sqkadmin: `getsentpayment`
|
||||
*/
|
||||
rpc GetSentPayment (GetSentPaymentRequest) returns (GetSentPaymentReply) {}
|
||||
|
|
@ -256,6 +260,10 @@ service SqueakAdmin {
|
|||
*/
|
||||
rpc GetReceivedPaymentsForPubkey (GetReceivedPaymentsForPubkeyRequest) returns (GetReceivedPaymentsForPubkeyReply) {}
|
||||
|
||||
/** sqkadmin: `getreceivedpaymentsforpeer`
|
||||
*/
|
||||
rpc GetReceivedPaymentsForPeer (GetReceivedPaymentsForPeerRequest) returns (GetReceivedPaymentsForPeerReply) {}
|
||||
|
||||
/** sqkadmin: `subscribereceivedpayments`
|
||||
*/
|
||||
rpc SubscribeReceivedPayments (SubscribeReceivedPaymentsRequest) returns (stream ReceivedPayment) {}
|
||||
|
|
@ -276,6 +284,10 @@ service SqueakAdmin {
|
|||
*/
|
||||
rpc GetPaymentSummaryForPubkey (GetPaymentSummaryForPubkeyRequest) returns (GetPaymentSummaryForPubkeyReply) {}
|
||||
|
||||
/** sqkadmin: `getpaymentsummaryforpeer`
|
||||
*/
|
||||
rpc GetPaymentSummaryForPeer (GetPaymentSummaryForPeerRequest) returns (GetPaymentSummaryForPeerReply) {}
|
||||
|
||||
/** sqkadmin: `reprocessreceivedpayments`
|
||||
*/
|
||||
rpc ReprocessReceivedPayments (ReprocessReceivedPaymentsRequest) returns (ReprocessReceivedPaymentsReply) {}
|
||||
|
|
@ -998,6 +1010,22 @@ message GetSentPaymentsForPubkeyReply {
|
|||
repeated SentPayment sent_payments = 1;
|
||||
}
|
||||
|
||||
message GetSentPaymentsForPeerRequest {
|
||||
/// Peer address
|
||||
PeerAddress peer_address = 1;
|
||||
|
||||
/// Limit number of results
|
||||
int32 limit = 2;
|
||||
|
||||
/// Last entry
|
||||
SentPayment last_sent_payment = 3;
|
||||
}
|
||||
|
||||
message GetSentPaymentsForPeerReply {
|
||||
/// The sent payments
|
||||
repeated SentPayment sent_payments = 1;
|
||||
}
|
||||
|
||||
message GetSentPaymentRequest {
|
||||
/// Sent payment id
|
||||
int32 sent_payment_id = 1;
|
||||
|
|
@ -1144,6 +1172,22 @@ message GetReceivedPaymentsForPubkeyReply {
|
|||
repeated ReceivedPayment received_payments = 1;
|
||||
}
|
||||
|
||||
message GetReceivedPaymentsForPeerRequest {
|
||||
/// Peer address
|
||||
PeerAddress peer_address = 1;
|
||||
|
||||
/// Limit number of results
|
||||
int32 limit = 2;
|
||||
|
||||
/// Last entry
|
||||
ReceivedPayment last_received_payment = 3;
|
||||
}
|
||||
|
||||
message GetReceivedPaymentsForPeerReply {
|
||||
/// The received payments
|
||||
repeated ReceivedPayment received_payments = 1;
|
||||
}
|
||||
|
||||
message ReceivedPayment {
|
||||
/// The received payment id
|
||||
int32 received_payment_id = 1;
|
||||
|
|
@ -1205,6 +1249,16 @@ message GetPaymentSummaryForPubkeyReply {
|
|||
PaymentSummary payment_summary = 1;
|
||||
}
|
||||
|
||||
message GetPaymentSummaryForPeerRequest {
|
||||
/// Peer address
|
||||
PeerAddress peer_address = 1;
|
||||
}
|
||||
|
||||
message GetPaymentSummaryForPeerReply {
|
||||
// Payment summary of the node
|
||||
PaymentSummary payment_summary = 1;
|
||||
}
|
||||
|
||||
message PaymentSummary {
|
||||
// Number of received payments
|
||||
int32 num_received_payments = 1;
|
||||
|
|
|
|||
|
|
@ -804,6 +804,38 @@ class SqueakAdminServerHandler(object):
|
|||
sent_payments=sent_payment_msgs,
|
||||
)
|
||||
|
||||
def handle_get_sent_payments_for_peer(self, request):
|
||||
peer_address = message_to_peer_address(request.peer_address)
|
||||
limit = request.limit
|
||||
last_sent_payment = message_to_sent_payment(request.last_sent_payment) if request.HasField(
|
||||
"last_sent_payment") else None
|
||||
logger.info("""Handle get sent payments with
|
||||
peer_address: {}
|
||||
limit: {}
|
||||
last_sent_payment: {}
|
||||
""".format(
|
||||
peer_address,
|
||||
limit,
|
||||
last_sent_payment,
|
||||
))
|
||||
sent_payments = self.squeak_controller.get_sent_payments_for_peer(
|
||||
peer_address,
|
||||
limit,
|
||||
last_sent_payment,
|
||||
)
|
||||
logger.info(
|
||||
"Got number of sent payments: {}".format(
|
||||
len(sent_payments)
|
||||
)
|
||||
)
|
||||
sent_payment_msgs = [
|
||||
sent_payment_to_message(sent_payment)
|
||||
for sent_payment in sent_payments
|
||||
]
|
||||
return squeak_admin_pb2.GetSentPaymentsReply(
|
||||
sent_payments=sent_payment_msgs,
|
||||
)
|
||||
|
||||
def handle_get_sent_payment(self, request):
|
||||
sent_payment_id = request.sent_payment_id
|
||||
logger.info(
|
||||
|
|
@ -918,6 +950,38 @@ class SqueakAdminServerHandler(object):
|
|||
received_payments=received_payment_msgs,
|
||||
)
|
||||
|
||||
def handle_get_received_payments_for_peer(self, request):
|
||||
peer_address = message_to_peer_address(request.peer_address)
|
||||
limit = request.limit
|
||||
last_received_payment = message_to_received_payment(request.last_received_payment) if request.HasField(
|
||||
"last_received_payment") else None
|
||||
logger.info("""Handle get received payments with
|
||||
peer_address: {}
|
||||
limit: {}
|
||||
last_received_payment: {}
|
||||
""".format(
|
||||
peer_address,
|
||||
limit,
|
||||
last_received_payment,
|
||||
))
|
||||
received_payments = self.squeak_controller.get_received_payments_for_peer(
|
||||
peer_address,
|
||||
limit,
|
||||
last_received_payment,
|
||||
)
|
||||
logger.info(
|
||||
"Got number of received payments: {}".format(
|
||||
len(received_payments)
|
||||
)
|
||||
)
|
||||
received_payment_msgs = [
|
||||
received_payment_to_message(received_payment)
|
||||
for received_payment in received_payments
|
||||
]
|
||||
return squeak_admin_pb2.GetReceivedPaymentsReply(
|
||||
received_payments=received_payment_msgs,
|
||||
)
|
||||
|
||||
def handle_subscribe_received_payments(self, request, stopped):
|
||||
payment_index = request.payment_index
|
||||
logger.info(
|
||||
|
|
@ -980,6 +1044,20 @@ class SqueakAdminServerHandler(object):
|
|||
payment_summary=payment_summary_msg,
|
||||
)
|
||||
|
||||
def handle_get_payment_summary_for_peer(self, request):
|
||||
peer_address = message_to_peer_address(request.peer_address)
|
||||
logger.info("Handle get payment summary for peer address: {}".format(
|
||||
peer_address,
|
||||
))
|
||||
payment_summary = self.squeak_controller.get_payment_summary_for_peer(
|
||||
peer_address)
|
||||
payment_summary_msg = payment_summary_to_message(
|
||||
payment_summary,
|
||||
)
|
||||
return squeak_admin_pb2.GetPaymentSummaryForPubkeyReply(
|
||||
payment_summary=payment_summary_msg,
|
||||
)
|
||||
|
||||
def handle_reprocess_received_payments(self, request):
|
||||
logger.info("Handle reprocess received payments")
|
||||
self.squeak_controller.reprocess_received_payments()
|
||||
|
|
|
|||
|
|
@ -221,6 +221,9 @@ class SqueakAdminServerServicer(squeak_admin_pb2_grpc.SqueakAdminServicer):
|
|||
def GetSentPaymentsForPubkey(self, request, context):
|
||||
return self.handler.handle_get_sent_payments_for_pubkey(request)
|
||||
|
||||
def GetSentPaymentsForPeer(self, request, context):
|
||||
return self.handler.handle_get_sent_payments_for_peer(request)
|
||||
|
||||
def GetSentPayment(self, request, context):
|
||||
return self.handler.handle_get_sent_payment(request)
|
||||
|
||||
|
|
@ -236,6 +239,9 @@ class SqueakAdminServerServicer(squeak_admin_pb2_grpc.SqueakAdminServicer):
|
|||
def GetReceivedPaymentsForPubkey(self, request, context):
|
||||
return self.handler.handle_get_received_payments_for_pubkey(request)
|
||||
|
||||
def GetReceivedPaymentsForPeer(self, request, context):
|
||||
return self.handler.handle_get_received_payments_for_peer(request)
|
||||
|
||||
def SubscribeReceivedPayments(self, request, context):
|
||||
stopped = threading.Event()
|
||||
|
||||
|
|
@ -257,6 +263,12 @@ class SqueakAdminServerServicer(squeak_admin_pb2_grpc.SqueakAdminServicer):
|
|||
def GetPaymentSummaryForSqueak(self, request, context):
|
||||
return self.handler.handle_get_payment_summary_for_squeak(request)
|
||||
|
||||
def GetPaymentSummaryForPubkey(self, request, context):
|
||||
return self.handler.handle_get_payment_summary_for_pubkey(request)
|
||||
|
||||
def GetPaymentSummaryForPeer(self, request, context):
|
||||
return self.handler.handle_get_payment_summary_for_peer(request)
|
||||
|
||||
def ReprocessReceivedPayments(self, request, context):
|
||||
return self.handler.handle_reprocess_received_payments(request)
|
||||
|
||||
|
|
|
|||
|
|
@ -433,6 +433,12 @@ def create_app(handler, username, password):
|
|||
def getsentpaymentsforpubkey(msg):
|
||||
return handler.handle_get_sent_payments_for_pubkey(msg)
|
||||
|
||||
@app.route("/getsentpaymentsforpeer", methods=["POST"])
|
||||
@login_required
|
||||
@protobuf_serialized(squeak_admin_pb2.GetSentPaymentsForPeerRequest())
|
||||
def getsentpaymentsforpeer(msg):
|
||||
return handler.handle_get_sent_payments_for_peer(msg)
|
||||
|
||||
@app.route("/getsentoffers", methods=["POST"])
|
||||
@login_required
|
||||
@protobuf_serialized(squeak_admin_pb2.GetSentOffersRequest())
|
||||
|
|
@ -457,6 +463,12 @@ def create_app(handler, username, password):
|
|||
def getreceivedpaymentsforpubkey(msg):
|
||||
return handler.handle_get_received_payments_for_pubkey(msg)
|
||||
|
||||
@app.route("/getreceivedpaymentsforpeer", methods=["POST"])
|
||||
@login_required
|
||||
@protobuf_serialized(squeak_admin_pb2.GetReceivedPaymentsForPeerRequest())
|
||||
def getreceivedpaymentsforpeer(msg):
|
||||
return handler.handle_get_received_payments_for_peer(msg)
|
||||
|
||||
@app.route("/getnetwork", methods=["POST"])
|
||||
@login_required
|
||||
@protobuf_serialized(squeak_admin_pb2.GetNetworkRequest())
|
||||
|
|
@ -487,6 +499,12 @@ def create_app(handler, username, password):
|
|||
def getpaymentsummaryforpubkey(msg):
|
||||
return handler.handle_get_payment_summary_for_pubkey(msg)
|
||||
|
||||
@app.route("/getpaymentsummaryforpeer", methods=["POST"])
|
||||
@login_required
|
||||
@protobuf_serialized(squeak_admin_pb2.GetPaymentSummaryForPeerRequest())
|
||||
def getpaymentsummaryforpeer(msg):
|
||||
return handler.handle_get_payment_summary_for_peer(msg)
|
||||
|
||||
@app.route("/reprocessreceivedpayments", methods=["POST"])
|
||||
@login_required
|
||||
@protobuf_serialized(squeak_admin_pb2.ReprocessReceivedPaymentsRequest())
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
{
|
||||
"files": {
|
||||
"main.css": "/static/css/main.206389cd.css",
|
||||
"main.js": "/static/js/main.0e504b96.js",
|
||||
"main.js": "/static/js/main.26267c8e.js",
|
||||
"static/css/306.7f7715a0.chunk.css": "/static/css/306.7f7715a0.chunk.css",
|
||||
"static/js/306.90b2a66c.chunk.js": "/static/js/306.90b2a66c.chunk.js",
|
||||
"static/css/900.b66ac5fb.chunk.css": "/static/css/900.b66ac5fb.chunk.css",
|
||||
"static/js/900.1b104345.chunk.js": "/static/js/900.1b104345.chunk.js",
|
||||
"static/css/841.693c3c31.chunk.css": "/static/css/841.693c3c31.chunk.css",
|
||||
"static/js/841.8faa8984.chunk.js": "/static/js/841.8faa8984.chunk.js",
|
||||
"static/js/841.9f23f674.chunk.js": "/static/js/841.9f23f674.chunk.js",
|
||||
"static/media/icon.svg": "/static/media/icon.982210ec9275956e4d3cc77fa90167fd.svg",
|
||||
"index.html": "/index.html",
|
||||
"main.206389cd.css.map": "/static/css/main.206389cd.css.map",
|
||||
"main.0e504b96.js.map": "/static/js/main.0e504b96.js.map",
|
||||
"main.26267c8e.js.map": "/static/js/main.26267c8e.js.map",
|
||||
"306.7f7715a0.chunk.css.map": "/static/css/306.7f7715a0.chunk.css.map",
|
||||
"306.90b2a66c.chunk.js.map": "/static/js/306.90b2a66c.chunk.js.map",
|
||||
"900.b66ac5fb.chunk.css.map": "/static/css/900.b66ac5fb.chunk.css.map",
|
||||
"900.1b104345.chunk.js.map": "/static/js/900.1b104345.chunk.js.map",
|
||||
"841.693c3c31.chunk.css.map": "/static/css/841.693c3c31.chunk.css.map",
|
||||
"841.8faa8984.chunk.js.map": "/static/js/841.8faa8984.chunk.js.map"
|
||||
"841.9f23f674.chunk.js.map": "/static/js/841.9f23f674.chunk.js.map"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.206389cd.css",
|
||||
"static/js/main.0e504b96.js"
|
||||
"static/js/main.26267c8e.js"
|
||||
]
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#1da1f2"/><meta nam="description" content="Squeaknode!"/><meta name="og:title" content="Squeaknode"/><meta name="og:description" content="check out my the Squeaknode I made"/><link href="https://fonts.googleapis.com/css2?family=Assistant&display=swap" rel="stylesheet"><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>Squeaknode</title><script defer="defer" src="/static/js/main.0e504b96.js"></script><link href="/static/css/main.206389cd.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#1da1f2"/><meta nam="description" content="Squeaknode!"/><meta name="og:title" content="Squeaknode"/><meta name="og:description" content="check out my the Squeaknode I made"/><link href="https://fonts.googleapis.com/css2?family=Assistant&display=swap" rel="stylesheet"><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>Squeaknode</title><script defer="defer" src="/static/js/main.26267c8e.js"></script><link href="/static/css/main.206389cd.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1411,6 +1411,53 @@ class SqueakDb:
|
|||
self._parse_sent_payment(row) for row in rows]
|
||||
return sent_payments
|
||||
|
||||
def get_sent_payments_for_peer(
|
||||
self,
|
||||
peer_address: PeerAddress,
|
||||
limit: int,
|
||||
last_sent_payment: Optional[SentPayment],
|
||||
) -> List[SentPayment]:
|
||||
""" Get sent payments for a peer. """
|
||||
last_created_time = last_sent_payment.created_time_ms if last_sent_payment else self.timestamp_now_ms
|
||||
last_payment_hash = last_sent_payment.payment_hash if last_sent_payment else MAX_HASH
|
||||
logger.info("""Get sent payments db query with
|
||||
peer_address: {}
|
||||
limit: {}
|
||||
last_created_time: {}
|
||||
last_payment_hash: {}
|
||||
""".format(
|
||||
peer_address,
|
||||
limit,
|
||||
last_created_time,
|
||||
last_payment_hash.hex(),
|
||||
))
|
||||
s = (
|
||||
select([self.sent_payments])
|
||||
.where(self.received_payments.c.peer_network == peer_address.network.name)
|
||||
.where(self.received_payments.c.peer_host == peer_address.host)
|
||||
.where(self.received_payments.c.peer_port == peer_address.port)
|
||||
.where(
|
||||
tuple_(
|
||||
self.sent_payments.c.created_time_ms,
|
||||
self.sent_payments.c.payment_hash,
|
||||
) < tuple_(
|
||||
last_created_time,
|
||||
last_payment_hash,
|
||||
)
|
||||
)
|
||||
.order_by(
|
||||
self.sent_payments.c.created_time_ms.desc(),
|
||||
self.sent_payments.c.payment_hash.desc(),
|
||||
)
|
||||
.limit(limit)
|
||||
)
|
||||
with self.get_connection() as connection:
|
||||
result = connection.execute(s)
|
||||
rows = result.fetchall()
|
||||
sent_payments = [
|
||||
self._parse_sent_payment(row) for row in rows]
|
||||
return sent_payments
|
||||
|
||||
def get_sent_payment(self, sent_payment_id: int) -> Optional[SentPayment]:
|
||||
""" Get sent payment by id. """
|
||||
s = (
|
||||
|
|
@ -1673,6 +1720,53 @@ class SqueakDb:
|
|||
self._parse_received_payment(row) for row in rows]
|
||||
return received_payments
|
||||
|
||||
def get_received_payments_for_peer(
|
||||
self,
|
||||
peer_address: PeerAddress,
|
||||
limit: int,
|
||||
last_received_payment: Optional[ReceivedPayment],
|
||||
) -> List[ReceivedPayment]:
|
||||
""" Get received payments for a squeak. """
|
||||
last_created_time = last_received_payment.created_time_ms if last_received_payment else self.timestamp_now_ms
|
||||
last_payment_hash = last_received_payment.payment_hash if last_received_payment else MAX_HASH
|
||||
logger.info("""Get received payments db query with
|
||||
peer_address: {}
|
||||
limit: {}
|
||||
last_created_time: {}
|
||||
last_payment_hash: {}
|
||||
""".format(
|
||||
peer_address,
|
||||
limit,
|
||||
last_created_time,
|
||||
last_payment_hash.hex(),
|
||||
))
|
||||
s = (
|
||||
select([self.received_payments])
|
||||
.where(self.received_payments.c.peer_network == peer_address.network.name)
|
||||
.where(self.received_payments.c.peer_host == peer_address.host)
|
||||
.where(self.received_payments.c.peer_port == peer_address.port)
|
||||
.where(
|
||||
tuple_(
|
||||
self.received_payments.c.created_time_ms,
|
||||
self.received_payments.c.payment_hash,
|
||||
) < tuple_(
|
||||
last_created_time,
|
||||
last_payment_hash,
|
||||
)
|
||||
)
|
||||
.order_by(
|
||||
self.received_payments.c.created_time_ms.desc(),
|
||||
self.received_payments.c.payment_hash.desc(),
|
||||
)
|
||||
.limit(limit)
|
||||
)
|
||||
with self.get_connection() as connection:
|
||||
result = connection.execute(s)
|
||||
rows = result.fetchall()
|
||||
received_payments = [
|
||||
self._parse_received_payment(row) for row in rows]
|
||||
return received_payments
|
||||
|
||||
def get_latest_settle_index(self) -> Optional[int]:
|
||||
""" Get the lnd settled index of the most recent received payment. """
|
||||
s = select(
|
||||
|
|
@ -1816,6 +1910,45 @@ class SqueakDb:
|
|||
sent_payment_summary = self._parse_sent_payment_summary(row)
|
||||
return sent_payment_summary
|
||||
|
||||
def get_received_payment_summary_for_peer(self, peer_address: PeerAddress) -> ReceivedPaymentSummary:
|
||||
""" Get received payment summary for a single peer. """
|
||||
s = (
|
||||
select([
|
||||
func.count().label("num_payments_received"),
|
||||
func.sum(self.received_payments.c.price_msat).label(
|
||||
"total_amount_received_msat"),
|
||||
])
|
||||
.select_from(self.received_payments)
|
||||
.where(self.received_payments.c.peer_network == peer_address.network.name)
|
||||
.where(self.received_payments.c.peer_host == peer_address.host)
|
||||
.where(self.received_payments.c.peer_port == peer_address.port)
|
||||
)
|
||||
with self.get_connection() as connection:
|
||||
result = connection.execute(s)
|
||||
row = result.fetchone()
|
||||
received_payment_summary = self._parse_received_payment_summary(
|
||||
row)
|
||||
return received_payment_summary
|
||||
|
||||
def get_sent_payment_summary_for_peer(self, peer_address: PeerAddress) -> SentPaymentSummary:
|
||||
""" Get sent payment summary for a peer. """
|
||||
s = (
|
||||
select([
|
||||
func.count().label("num_payments_sent"),
|
||||
func.sum(self.sent_payments.c.price_msat).label(
|
||||
"total_amount_sent_msat"),
|
||||
])
|
||||
.select_from(self.sent_payments)
|
||||
.where(self.received_payments.c.peer_network == peer_address.network.name)
|
||||
.where(self.received_payments.c.peer_host == peer_address.host)
|
||||
.where(self.received_payments.c.peer_port == peer_address.port)
|
||||
)
|
||||
with self.get_connection() as connection:
|
||||
result = connection.execute(s)
|
||||
row = result.fetchone()
|
||||
sent_payment_summary = self._parse_sent_payment_summary(row)
|
||||
return sent_payment_summary
|
||||
|
||||
def insert_config(self, user_config: UserConfig) -> Optional[str]:
|
||||
""" Insert a new config.
|
||||
|
||||
|
|
|
|||
|
|
@ -241,6 +241,18 @@ class SqueakController:
|
|||
last_sent_payment,
|
||||
)
|
||||
|
||||
def get_sent_payments_for_peer(
|
||||
self,
|
||||
peer_address: PeerAddress,
|
||||
limit: int,
|
||||
last_sent_payment: Optional[SentPayment],
|
||||
) -> List[SentPayment]:
|
||||
return self.squeak_store.get_sent_payments_for_peer(
|
||||
peer_address,
|
||||
limit,
|
||||
last_sent_payment,
|
||||
)
|
||||
|
||||
def get_sent_payment(self, sent_payment_id: int) -> Optional[SentPayment]:
|
||||
return self.squeak_store.get_sent_payment(sent_payment_id)
|
||||
|
||||
|
|
@ -281,6 +293,18 @@ class SqueakController:
|
|||
last_received_payment,
|
||||
)
|
||||
|
||||
def get_received_payments_for_peer(
|
||||
self,
|
||||
peer_address: PeerAddress,
|
||||
limit: int,
|
||||
last_received_payment: Optional[ReceivedPayment],
|
||||
) -> List[ReceivedPayment]:
|
||||
return self.squeak_store.get_received_payments_for_peer(
|
||||
peer_address,
|
||||
limit,
|
||||
last_received_payment,
|
||||
)
|
||||
|
||||
def delete_all_expired_offers(self):
|
||||
self.squeak_store.delete_all_expired_offers()
|
||||
|
||||
|
|
@ -397,6 +421,16 @@ class SqueakController:
|
|||
received_payment_summary=received_payment_summary,
|
||||
)
|
||||
|
||||
def get_payment_summary_for_peer(self, peer_address: PeerAddress) -> PaymentSummary:
|
||||
received_payment_summary = self.squeak_store.get_received_payment_summary_for_peer(
|
||||
peer_address)
|
||||
sent_payment_summary = self.squeak_store.get_sent_payment_summary_for_peer(
|
||||
peer_address)
|
||||
return PaymentSummary(
|
||||
sent_payment_summary=sent_payment_summary,
|
||||
received_payment_summary=received_payment_summary,
|
||||
)
|
||||
|
||||
# def get_received_payment_summary(self) -> ReceivedPaymentSummary:
|
||||
# return self.squeak_store.get_received_payment_summary()
|
||||
|
||||
|
|
|
|||
|
|
@ -430,6 +430,18 @@ class SqueakStore:
|
|||
last_sent_payment,
|
||||
)
|
||||
|
||||
def get_sent_payments_for_peer(
|
||||
self,
|
||||
peer_address: PeerAddress,
|
||||
limit: int,
|
||||
last_sent_payment: Optional[SentPayment],
|
||||
) -> List[SentPayment]:
|
||||
return self.squeak_db.get_sent_payments_for_peer(
|
||||
peer_address,
|
||||
limit,
|
||||
last_sent_payment,
|
||||
)
|
||||
|
||||
def get_sent_payment(self, sent_payment_id: int) -> Optional[SentPayment]:
|
||||
return self.squeak_db.get_sent_payment(sent_payment_id)
|
||||
|
||||
|
|
@ -473,6 +485,18 @@ class SqueakStore:
|
|||
last_received_payment,
|
||||
)
|
||||
|
||||
def get_received_payments_for_peer(
|
||||
self,
|
||||
peer_address: PeerAddress,
|
||||
limit: int,
|
||||
last_received_payment: Optional[ReceivedPayment],
|
||||
) -> List[ReceivedPayment]:
|
||||
return self.squeak_db.get_received_payments_for_peer(
|
||||
peer_address,
|
||||
limit,
|
||||
last_received_payment,
|
||||
)
|
||||
|
||||
def delete_all_expired_offers(self):
|
||||
self.delete_all_expired_received_offers()
|
||||
self.delete_all_expired_sent_offers()
|
||||
|
|
@ -591,6 +615,9 @@ class SqueakStore:
|
|||
def get_received_payment_summary_for_pubkey(self, pubkey: SqueakPublicKey) -> ReceivedPaymentSummary:
|
||||
return self.squeak_db.get_received_payment_summary_for_pubkey(pubkey)
|
||||
|
||||
def get_received_payment_summary_for_peer(self, peer_address: PeerAddress) -> ReceivedPaymentSummary:
|
||||
return self.squeak_db.get_received_payment_summary_for_peer(peer_address)
|
||||
|
||||
def get_sent_payment_summary(self) -> SentPaymentSummary:
|
||||
return self.squeak_db.get_sent_payment_summary()
|
||||
|
||||
|
|
@ -600,6 +627,9 @@ class SqueakStore:
|
|||
def get_sent_payment_summary_for_pubkey(self, pubkey: SqueakPublicKey) -> SentPaymentSummary:
|
||||
return self.squeak_db.get_sent_payment_summary_for_pubkey(pubkey)
|
||||
|
||||
def get_sent_payment_summary_for_peer(self, peer_address: PeerAddress) -> SentPaymentSummary:
|
||||
return self.squeak_db.get_sent_payment_summary_for_peer(peer_address)
|
||||
|
||||
def clear_received_payment_settle_indices(self) -> None:
|
||||
self.squeak_db.clear_received_payment_settle_indices()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue