Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/new invoice logic #847

Merged
merged 10 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -1223,7 +1223,7 @@ func (db database) GetInvoice(payment_request string) InvoiceList {

func (db database) UpdateInvoice(payment_request string) InvoiceList {
ms := InvoiceList{}
db.db.Model(&Tribe{}).Where("payment_request = ?", payment_request).Update("status", true)
db.db.Model(&InvoiceList{}).Where("payment_request = ?", payment_request).Update("status", true)
ms.Status = true
return ms
}
Expand Down
16 changes: 15 additions & 1 deletion frontend/app/src/helpers/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/typedef */
import lighningDecoder from 'light-bolt11-decoder';
import { getHost } from '../config/host';
import { uiStore } from '../store/ui';

Expand All @@ -21,7 +22,7 @@ export const formatSatPrice = (amount = 0) => {
return dollarUSLocale.format(amount);
};

export const getOriginalNumberValue = (formattedValue: string = '') => {
export const getOriginalNumberValue = (formattedValue: string) => {
// Remove formatting (commas) from the formatted value
const unformattedValue = formattedValue.replace(/,/g, '');
return Number(unformattedValue);
Expand Down Expand Up @@ -186,3 +187,16 @@ export const toCapitalize = (word: string): string => {
const result = capitalizeStrings.join(' ');
return result;
};

export const isInvoiceExpired = (paymentRequest: string): boolean => {
// decode invoice to see if it has expired
const decoded = lighningDecoder.decode(paymentRequest);
const invoiceTimestamp = decoded.sections[4].value;
const expiry = decoded.sections[8].value;
const expired = invoiceTimestamp + expiry;

if (expired * 1000 > Date.now()) {
return false;
}
return true;
};
85 changes: 52 additions & 33 deletions frontend/app/src/people/utils/AssignBounty.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React, { useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { ConnectCardProps } from 'people/interfaces';
import { useStores } from 'store';
import { EuiGlobalToastList } from '@elastic/eui';
import moment from 'moment';
import { isInvoiceExpired } from 'helpers';
import Invoice from '../widgetViews/summaries/wantedSummaries/Invoice';
import { colors } from '../../config/colors';
import { Button, Modal } from '../../components/common';
Expand All @@ -19,7 +20,7 @@ export default function AssignBounty(props: ConnectCardProps) {
const [lnInvoice, setLnInvoice] = useState('');
const [invoiceStatus, setInvoiceStatus] = useState(false);

const pollMinutes = 2;
const pollMinutes = 5;

const [toasts, setToasts]: any = useState([]);

Expand All @@ -36,39 +37,37 @@ export default function AssignBounty(props: ConnectCardProps) {
setToasts([]);
};

async function startPolling(paymentRequest: string) {
let i = 0;
interval = setInterval(async () => {
try {
const invoiceData = await main.pollInvoice(paymentRequest);
if (invoiceData) {
if (invoiceData.success && invoiceData.response.settled) {
clearInterval(interval);

addToast();
setLnInvoice('');
setInvoiceStatus(true);
main.setLnInvoice('');

// get new wanted list
main.getPeopleBounties({ page: 1, resetPage: true });

props.dismiss();
if (props.dismissConnectModal) props.dismissConnectModal();
const startPolling = useCallback(
async (paymentRequest: string) => {
interval = setInterval(async () => {
try {
const invoiceData = await main.pollInvoice(paymentRequest);
if (invoiceData) {
if (invoiceData.success && invoiceData.response.settled) {
clearInterval(interval);

addToast();
setLnInvoice('');
setInvoiceStatus(true);
main.setAssignInvoice('');

// get new wanted list
main.getPeopleBounties({ page: 1, resetPage: true });

props.dismiss();
if (props.dismissConnectModal) props.dismissConnectModal();
}
}
} catch (e) {
console.warn('AssignBounty Modal Invoice Polling Error', e);
}
i++;
if (i > 100) {
if (interval) clearInterval(interval);
}
} catch (e) {
console.warn('AssignBounty Modal Invoice Polling Error', e);
}
}, 3000);
}
}, 5000);
},
[main, props]
);

const generateInvoice = async () => {
if (created && ui.meInfo?.websocketToken) {
if (created) {
const data = await main.getLnInvoice({
amount: 200 * bountyHours,
memo: '',
Expand All @@ -84,11 +83,31 @@ export default function AssignBounty(props: ConnectCardProps) {
).toUTCString()
});

setLnInvoice(data.response.invoice);
startPolling(data.response.invoice);
const paymentRequest = data.response.invoice;

if (paymentRequest) {
setLnInvoice(paymentRequest);
main.setAssignInvoice(paymentRequest);
startPolling(paymentRequest);
}
}
};

useEffect(() => {
if (main.assignInvoice !== '') {
const expired = isInvoiceExpired(main.assignInvoice);
if (!expired) {
startPolling(main.assignInvoice);
} else {
main.setAssignInvoice('');
}
}

return () => {
clearInterval(interval);
};
}, [main, startPolling]);

return (
<div onClick={(e: any) => e.stopPropagation()}>
<Modal style={props.modalStyle} overlayClick={() => props.dismiss()} visible={visible}>
Expand Down
73 changes: 70 additions & 3 deletions frontend/app/src/people/widgetViews/OrganizationDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Button } from 'components/common';
import { BountyRoles, BudgetHistory, Organization, PaymentHistory, Person } from 'store/main';
import MaterialIcon from '@material/react-material-icon';
import { Route, Router, Switch, useRouteMatch } from 'react-router-dom';
import { satToUsd, userHasRole } from 'helpers';
import { isInvoiceExpired, satToUsd, userHasRole } from 'helpers';
import { BountyModal } from 'people/main/bountyModal';
import history from '../../config/history';
import avatarIcon from '../../public/static/profile_avatar.svg';
Expand Down Expand Up @@ -89,6 +89,8 @@ const OrganizationDetails = (props: { close: () => void; org: Organization | und
const { org } = props;
const uuid = org?.uuid || '';

let interval;

function addToast(title: string, color: 'danger' | 'success') {
setToasts([
{
Expand Down Expand Up @@ -259,7 +261,6 @@ const OrganizationDetails = (props: { close: () => void; org: Organization | und
const successAction = () => {
addToast('Budget was added successfully', 'success');
setInvoiceStatus(true);
main.setLnInvoice('');

// get new organization budget
getOrganizationBudget();
Expand All @@ -268,6 +269,72 @@ const OrganizationDetails = (props: { close: () => void; org: Organization | und
closeBudgetHandler();
};

const deleteInvoice = (payReq: string, invoices: string[]): string[] => {
const index = invoices.indexOf(payReq);
invoices.splice(index, 1);
main.setBudgetInvoice(invoices);

return invoices;
};

const pollInvoices = () => {
let invoices = [...main.budgetInvoices];
if (invoices.length) {
interval = setInterval(async () => {
try {
for (const payReq of invoices) {
if (payReq) {
const expired = isInvoiceExpired(payReq);
if (!expired) {
const invoiceData = await main.pollInvoice(payReq);
if (invoiceData) {
if (invoiceData.success && invoiceData.response.settled) {
invoices = deleteInvoice(payReq, invoices);

getOrganizationBudget();
getBudgetHistory();
}
}
} else {
invoices = deleteInvoice(payReq, main.budgetInvoices);
}

if (invoices.length === 0) {
clearInterval(interval);
}
}
}
} catch (e) {
console.warn('Budget Invoices Polling Error', e);
}
}, 5000);
}
};

async function startPolling(paymentRequest: string) {
interval = setInterval(async () => {
try {
const invoiceData = await main.pollInvoice(paymentRequest);
if (invoiceData) {
if (invoiceData.success && invoiceData.response.settled) {
clearInterval(interval);
successAction();
}
}
} catch (e) {
console.warn('AddBudget Modal Invoice Polling Error', e);
}
}, 5000);
}

useEffect(() => {
pollInvoices();

return () => {
clearInterval(interval);
};
});

useEffect(() => {
getOrganizationUsers();
getBountyRoles();
Expand Down Expand Up @@ -455,7 +522,7 @@ const OrganizationDetails = (props: { close: () => void; org: Organization | und
close={closeBudgetHandler}
uuid={uuid}
invoiceStatus={invoiceStatus}
successAction={successAction}
startPolling={startPolling}
/>
)}
{isOpenHistory && (
Expand Down
35 changes: 10 additions & 25 deletions frontend/app/src/people/widgetViews/organization/AddBudgetModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,34 +21,12 @@ const AddBudgetModal = (props: AddBudgetModalProps) => {

const isMobile = useIsMobile();
const { ui, main } = useStores();
const { isOpen, close, invoiceStatus, uuid, successAction } = props;
const { isOpen, close, invoiceStatus, uuid, startPolling } = props;

const config = nonWidgetConfigs['organizationusers'];

const pollMinutes = 2;

async function startPolling(paymentRequest: string) {
let i = 0;
interval = setInterval(async () => {
try {
const invoiceData = await main.pollInvoice(paymentRequest);

if (invoiceData) {
if (invoiceData.success && invoiceData.response.settled) {
clearInterval(interval);
successAction();
}
}
i++;
if (i > 100) {
if (interval) clearInterval(interval);
}
} catch (e) {
console.warn('AddBudgetModal Invoice Polling Error', e);
}
}, 3000);
}

const generateInvoice = async () => {
if (uuid) {
const data = await main.getBudgetInvoice({
Expand All @@ -58,8 +36,15 @@ const AddBudgetModal = (props: AddBudgetModalProps) => {
payment_type: 'deposit'
});

setLnInvoice(data.response.invoice);
startPolling(data.response.invoice);
const paymentRequest = data.response.invoice;

if (paymentRequest) {
setLnInvoice(paymentRequest);
startPolling(paymentRequest);

const invoices = [...main.budgetInvoices, paymentRequest];
main.setBudgetInvoice(invoices);
}
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useStores } from 'store';
import styled from 'styled-components';
import { BudgetWithdrawSuccess } from 'store/main';
import { satToUsd } from 'helpers';
import LighningDecoder from 'light-bolt11-decoder';
import lighningDecoder from 'light-bolt11-decoder';
import { Button, Modal } from '../../../components/common';
import { colors } from '../../../config/colors';
import successIcon from '../../../public/static/withdraw_success.svg';
Expand Down Expand Up @@ -105,7 +105,7 @@ const WithdrawBudgetModal = (props: WithdrawModalProps) => {

const getInvoiceDetails = async (paymentRequest: string) => {
try {
const decoded = LighningDecoder.decode(paymentRequest);
const decoded = lighningDecoder.decode(paymentRequest);
const sats = decoded.sections[2].value / 1000;
setAmountInSats(sats);
} catch (e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export interface AddUserModalProps extends ModalProps {

export interface AddBudgetModalProps extends ModalProps {
invoiceStatus: boolean;
successAction: () => void;
startPolling: (inv: string) => void;
}

export interface WithdrawModalProps extends ModalProps {
Expand Down
Loading
Loading