chore: 🔧 add score query

This commit is contained in:
Anthony Potdevin 2020-12-14 14:16:31 +01:00
parent d1fec2b92d
commit 7fa7cfc3c2
No known key found for this signature in database
GPG key ID: 4403F1DFBE779457
55 changed files with 1863 additions and 52 deletions

View file

@ -43,9 +43,10 @@ module.exports = {
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 0,
'import/no-unresolved': 'off',
'import/order': 2,
'no-unused-vars': 2,
'no-unused-vars': 0,
camelcase: 'off',
'@typescript-eslint/camelcase': 'off',
'react/prop-types': 'off',

110
package-lock.json generated
View file

@ -8533,6 +8533,12 @@
"integrity": "sha512-6+OPzqhKX/cx5xh+yO8Cqg3u3alrkhoxhE5ZOdSEv0DOzJ13lwJ6laqGU0Kv6+XDMFmlnGId04LtY22PsFLQUw==",
"dev": true
},
"@types/d3-array": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-2.8.0.tgz",
"integrity": "sha512-Q0ubcGHAmCRPh90/hoYB4eKWhxYKUxphwSeQrlz2tiabQ8S9zqhaE2CZJtCaLH2cjqKcjr52WPvmOA7ha0O4ZA==",
"dev": true
},
"@types/d3-chord": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-1.0.10.tgz",
@ -8557,9 +8563,9 @@
"integrity": "sha512-NaIeSIBiFgSC6IGUBjZWcscUJEq7vpVu7KthHN8eieTV9d9MqkSOZLH4chq1PmcKy06PNe3axLeKmRIyxJ+PZQ=="
},
"@types/d3-scale": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-3.2.1.tgz",
"integrity": "sha512-j+FryQSVk3GHLqjOX/RsHwGHg4XByJ0xIO1ASBTgzhE9o1tgeV4kEWLOzMzJRembKalflk5F03lEkM+4V6LDrQ==",
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-3.2.2.tgz",
"integrity": "sha512-qpQe8G02tzUwt9sdWX1h8A/W0Q1+N48wMnYXVOkrzeLUkCfvzJYV9Ee3aORCS4dN4ONRLFmMvaXdziQ29XGLjQ==",
"requires": {
"@types/d3-time": "*"
}
@ -8915,7 +8921,6 @@
"version": "17.0.0",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.0.tgz",
"integrity": "sha512-lUqY7OlkF/RbNtD5nIq7ot8NquXrdFrjSOR6+w9a9RFQevGi1oZO1dcJbXMeONAPKtZ2UrZOEJ5UOCVsxbLk/g==",
"dev": true,
"requires": {
"@types/react": "*"
}
@ -9437,6 +9442,16 @@
"resolved": "https://registry.npmjs.org/@ungap/global-this/-/global-this-0.4.2.tgz",
"integrity": "sha512-uFg7Kz+E12RBlgBLMlWVjmn2OIeE2J1Lzij0RseNcCVsrJX+LEB4fQ9MnoPXkXJmO5cHtTEzI5ATtb3IJfQ9tQ=="
},
"@visx/bounds": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@visx/bounds/-/bounds-1.0.0.tgz",
"integrity": "sha512-QxD/OkZVkzpeP6L0YxUnIAsxlFemkDPfOumchVDRlrO4lZ3YXLmsnaEEiJpU5tSgNamZAUh+Tz3d2RbHp3qqxA==",
"requires": {
"@types/react": "*",
"@types/react-dom": "*",
"prop-types": "^15.5.10"
}
},
"@visx/chord": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@visx/chord/-/chord-1.0.0.tgz",
@ -9459,6 +9474,15 @@
"d3-shape": "^1.0.6"
}
},
"@visx/event": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@visx/event/-/event-1.0.0.tgz",
"integrity": "sha512-GQFsLVVbVs1elvRMNP+dscB/hOvYpt/zRxnJgoBIYgAJ5HJZ4n2PweSKagdaDB/MSCAvRZUj/iM6PMTZXnlnew==",
"requires": {
"@types/react": "*",
"@visx/point": "1.0.0"
}
},
"@visx/group": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@visx/group/-/group-1.0.0.tgz",
@ -9470,6 +9494,11 @@
"prop-types": "^15.6.2"
}
},
"@visx/point": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@visx/point/-/point-1.0.0.tgz",
"integrity": "sha512-0L3ILwv6ro0DsQVbA1lo8fo6q3wvIeSTt9C8NarUUkoTNSFZaJtlmvwg2238r8fwwmSv0v9QFBj1hBz4o0bHrg=="
},
"@visx/responsive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@visx/responsive/-/responsive-1.1.0.tgz",
@ -9496,9 +9525,9 @@
},
"dependencies": {
"d3-array": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.8.0.tgz",
"integrity": "sha512-6V272gsOeg7+9pTW1jSYOR1QE37g95I3my1hBmY+vOUNHRrk9yt4OTz/gK7PMkVAVDrYYq4mq3grTiZ8iJdNIw=="
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz",
"integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg=="
},
"d3-scale": {
"version": "3.2.3",
@ -9534,6 +9563,19 @@
"prop-types": "^15.5.10"
}
},
"@visx/tooltip": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@visx/tooltip/-/tooltip-1.1.0.tgz",
"integrity": "sha512-xVB1vjZoL6OqiLp2VjyUYAoklPm/pQJmOD90TaBnLjku9KyYmElziO4q2qEpWmtY+K4XjXful10gBRPkt/e70Q==",
"requires": {
"@types/classnames": "^2.2.9",
"@types/react": "*",
"@visx/bounds": "1.0.0",
"classnames": "^2.2.5",
"prop-types": "^15.5.10",
"react-use-measure": "2.0.1"
}
},
"@webassemblyjs/ast": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz",
@ -14467,9 +14509,9 @@
}
},
"d3-array": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz",
"integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.9.1.tgz",
"integrity": "sha512-Ob7RdOtkqsjx1NWyQHMFLtCSk6/aKTxDdC4ZIolX+O+mDD2RzrsYgAyc0WGAlfYFVELLSilS7w8BtE3PKM8bHg=="
},
"d3-chord": {
"version": "1.0.6",
@ -14478,6 +14520,13 @@
"requires": {
"d3-array": "1",
"d3-path": "1"
},
"dependencies": {
"d3-array": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz",
"integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="
}
}
},
"d3-collection": {
@ -14525,6 +14574,21 @@
"d3-interpolate": "1",
"d3-time": "1",
"d3-time-format": "2"
},
"dependencies": {
"d3-array": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz",
"integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="
},
"d3-time-format": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz",
"integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==",
"requires": {
"d3-time": "1"
}
}
}
},
"d3-shape": {
@ -14541,11 +14605,11 @@
"integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA=="
},
"d3-time-format": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz",
"integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz",
"integrity": "sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==",
"requires": {
"d3-time": "1"
"d3-time": "1 - 2"
}
},
"d3-timer": {
@ -14652,8 +14716,7 @@
"debounce": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz",
"integrity": "sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg==",
"dev": true
"integrity": "sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg=="
},
"debug": {
"version": "2.6.9",
@ -25547,6 +25610,14 @@
"prop-types": "^15.6.2"
}
},
"react-use-measure": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.0.1.tgz",
"integrity": "sha512-lFfHiqcXbJ2/6aUkZwt8g5YYM7EGqNVxJhMqMPqv1BVXRKp8D7jYLlmma0SvhRY4WYxxkZpCdbJvhDylb5gcEA==",
"requires": {
"debounce": "^1.2.0"
}
},
"read-pkg": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
@ -29060,6 +29131,13 @@
"lodash": "^4.17.19",
"prop-types": "^15.5.8",
"victory-core": "^35.4.3"
},
"dependencies": {
"d3-array": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz",
"integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="
}
}
},
"victory-brush-container": {

View file

@ -36,10 +36,13 @@
"@apollo/client": "^3.3.5",
"@next/bundle-analyzer": "^10.0.3",
"@visx/chord": "^1.0.0",
"@visx/curve": "^1.0.0",
"@visx/event": "^1.0.0",
"@visx/group": "^1.0.0",
"@visx/responsive": "^1.1.0",
"@visx/scale": "^1.1.0",
"@visx/shape": "^1.2.0",
"@visx/tooltip": "^1.1.0",
"apollo-server-micro": "^2.19.0",
"balanceofsatoshis": "^7.10.0",
"bcryptjs": "^2.4.3",
@ -50,6 +53,7 @@
"boltz-core": "^0.3.5",
"cookie": "^0.4.1",
"crypto-js": "^4.0.0",
"d3-array": "^2.9.1",
"date-fns": "^2.16.1",
"graphql": "^15.4.0",
"graphql-iso-date": "^3.6.1",
@ -109,6 +113,7 @@
"@types/bcryptjs": "^2.4.2",
"@types/cookie": "^0.4.0",
"@types/crypto-js": "^4.0.1",
"@types/d3-array": "^2.8.0",
"@types/graphql-iso-date": "^3.4.0",
"@types/js-cookie": "^2.2.6",
"@types/js-yaml": "^3.12.5",

View file

@ -7,6 +7,7 @@ import { StyledToastContainer } from 'src/components/toastContainer/ToastContain
import { AppProps } from 'next/app';
import { ApolloProvider } from '@apollo/client';
import { useApollo } from 'config/client';
import { BaseProvider } from 'src/context/BaseContext';
import { ContextProvider } from '../src/context/ContextProvider';
import { useConfigState, ConfigProvider } from '../src/context/ConfigContext';
import { GlobalStyles } from '../src/styles/GlobalStyle';
@ -46,11 +47,13 @@ export default function App({ Component, pageProps }: AppProps) {
<title>ThunderHub - Lightning Node Manager</title>
</Head>
<ConfigProvider initialConfig={pageProps.initialConfig}>
<ContextProvider>
<Wrapper>
<Component {...pageProps} />
</Wrapper>
</ContextProvider>
<BaseProvider initialHasToken={pageProps.hasToken}>
<ContextProvider>
<Wrapper>
<Component {...pageProps} />
</Wrapper>
</ContextProvider>
</BaseProvider>
</ConfigProvider>
<StyledToastContainer />
</ApolloProvider>

61
pages/scores/[id].tsx Normal file
View file

@ -0,0 +1,61 @@
import { GridWrapper } from 'src/components/gridWrapper/GridWrapper';
import { NextPageContext } from 'next';
import { getProps } from 'src/utils/ssr';
import { NodeInfo } from 'src/views/scores/NodeInfo';
import { useRouter } from 'next/router';
import { useBaseDispatch, useBaseState } from 'src/context/BaseContext';
import { useEffect, useState } from 'react';
import { appendBasePath } from 'src/utils/basePath';
import { NodeScores } from 'src/views/scores/NodeScores';
import { Graph } from 'src/views/scores/NodeGraph';
import { useDeleteBaseTokenMutation } from 'src/graphql/mutations/__generated__/deleteBaseToken.generated';
const NodeScoreView = () => {
const [loading, setLoading] = useState<boolean>(true);
const { push } = useRouter();
const { hasToken } = useBaseState();
const dispatch = useBaseDispatch();
const [deleteToken] = useDeleteBaseTokenMutation();
useEffect(() => {
if (!hasToken) {
push(appendBasePath('/token'));
}
}, [hasToken, push]);
const handleAuthError = () => {
dispatch({ type: 'change', hasToken: false });
deleteToken();
push(appendBasePath('/token'));
};
return (
<>
{!loading && (
<>
<Graph />
<NodeInfo />
</>
)}
<NodeScores
callback={() => setLoading(false)}
errorCallback={handleAuthError}
/>
</>
);
};
const Wrapped = () => (
<GridWrapper>
<NodeScoreView />
</GridWrapper>
);
export default Wrapped;
export async function getServerSideProps(context: NextPageContext) {
return await getProps(context);
}

104
pages/scores/index.tsx Normal file
View file

@ -0,0 +1,104 @@
import React from 'react';
import { GridWrapper } from 'src/components/gridWrapper/GridWrapper';
import { NextPageContext } from 'next';
import { getProps } from 'src/utils/ssr';
import { useGetBosScoresQuery } from 'src/graphql/queries/__generated__/getBosScores.generated';
import { LoadingCard } from 'src/components/loading/LoadingCard';
import { ScoreCard } from 'src/views/scores/ScoreCard';
import {
Card,
CardWithTitle,
DarkSubTitle,
Separation,
SingleLine,
SubTitle,
} from 'src/components/generic/Styled';
import { useNodeInfo } from 'src/hooks/UseNodeInfo';
import { getFormatDate, getNodeLink } from 'src/components/generic/helpers';
import { Table } from 'src/components/table';
import { BarChart2 } from 'react-feather';
import styled from 'styled-components';
import { chartColors } from 'src/styles/Themes';
import { Link } from 'src/components/link/Link';
import { toast } from 'react-toastify';
import { getErrorContent } from 'src/utils/error';
const S = {
Icon: styled.div`
cursor: pointer;
&:hover {
color: ${chartColors.orange};
}
`,
};
const Wrapped = () => {
const { publicKey } = useNodeInfo();
const { data, loading } = useGetBosScoresQuery({
onError: err => toast.error(getErrorContent(err)),
});
if (loading) {
return (
<GridWrapper>
<LoadingCard title={'BOS Scores'} />
</GridWrapper>
);
}
const scores = data?.getBosScores?.scores.filter(Boolean) || [];
const date = data?.getBosScores?.updated || '';
const thisNode =
scores.find(score => score?.public_key === publicKey) || null;
const columns = [
{ Header: 'Index', accessor: 'index' },
{ Header: 'Alias', accessor: 'alias' },
{ Header: 'PubKey', accessor: 'key' },
{ Header: 'Position', accessor: 'position' },
{ Header: 'Score', accessor: 'score' },
{ Header: 'History', accessor: 'chart' },
];
const tableData = scores.map((s, index) => ({
...s,
index: index + 1,
key: getNodeLink(s?.public_key),
chart: (
<Link to={`/scores/${s?.public_key}`}>
<S.Icon>
<BarChart2 />
</S.Icon>
</Link>
),
}));
return (
<GridWrapper>
<CardWithTitle>
<SingleLine>
<SubTitle>BOS Scores</SubTitle>
<DarkSubTitle>{`Updated: ${getFormatDate(date)}`}</DarkSubTitle>
</SingleLine>
<Card>
<ScoreCard score={thisNode} />
<Separation />
<Table
filterPlaceholder={'nodes'}
withBorder={true}
tableData={tableData}
tableColumns={columns}
/>
</Card>
</CardWithTitle>
</GridWrapper>
);
};
export default Wrapped;
export async function getServerSideProps(context: NextPageContext) {
return await getProps(context);
}

41
pages/token.tsx Normal file
View file

@ -0,0 +1,41 @@
import React, { useState } from 'react';
import { NextPageContext } from 'next';
import { getProps } from 'src/utils/ssr';
import { TokenCard } from 'src/views/token/TokenCard';
import { GridWrapper } from 'src/components/gridWrapper/GridWrapper';
import { PaidCard } from 'src/views/token/PaidCard';
import { useBaseState } from 'src/context/BaseContext';
import { Card } from 'src/components/generic/Styled';
import { RecoverToken } from 'src/views/token/RecoverToken';
const TokenView = () => {
const { hasToken } = useBaseState();
const [id, setId] = useState<string | null>();
if (id) {
return <PaidCard id={id} />;
}
if (hasToken) {
return <Card>You already have a token!</Card>;
}
return (
<>
<TokenCard paidCallback={id => setId(id)} />
<RecoverToken />
</>
);
};
const Wrapped = () => (
<GridWrapper>
<TokenView />
</GridWrapper>
);
export default Wrapped;
export async function getServerSideProps(context: NextPageContext) {
return await getProps(context);
}

View file

@ -22,7 +22,11 @@ export const bitcoinResolvers = {
throw new Error('Problem getting Bitcoin price.');
}
},
getBitcoinFees: async (_: undefined, params: any, context: ContextType) => {
getBitcoinFees: async (
_: undefined,
__: undefined,
context: ContextType
) => {
await requestLimiter(context.ip, 'bitcoinFee');
try {

View file

@ -32,7 +32,7 @@ export const chainResolvers = {
Query: {
getChainBalance: async (
_: undefined,
params: any,
__: undefined,
context: ContextType
) => {
await requestLimiter(context.ip, 'chainBalance');
@ -48,16 +48,14 @@ export const chainResolvers = {
},
getPendingChainBalance: async (
_: undefined,
params: any,
__: undefined,
context: ContextType
) => {
await requestLimiter(context.ip, 'pendingChainBalance');
const { lnd } = context;
const pendingValue: PendingChainBalanceProps = await to<
GetPendingChainBalanceType
>(
const pendingValue: PendingChainBalanceProps = await to<GetPendingChainBalanceType>(
getPendingChainBalance({
lnd,
})
@ -66,7 +64,7 @@ export const chainResolvers = {
},
getChainTransactions: async (
_: undefined,
params: any,
__: undefined,
context: ContextType
) => {
await requestLimiter(context.ip, 'chainTransactions');
@ -85,7 +83,7 @@ export const chainResolvers = {
).reverse();
return transactions;
},
getUtxos: async (_: undefined, params: any, context: ContextType) => {
getUtxos: async (_: undefined, __: undefined, context: ContextType) => {
await requestLimiter(context.ip, 'getUtxos');
const { lnd } = context;

View file

@ -11,7 +11,7 @@ interface ChannelBalanceProps {
export const getChannelBalance = async (
_: undefined,
params: any,
__: undefined,
context: ContextType
) => {
await requestLimiter(context.ip, 'channelBalance');

View file

@ -26,7 +26,7 @@ interface PendingChannelProps {
export const getPendingChannels = async (
_: undefined,
params: any,
__: undefined,
context: ContextType
) => {
await requestLimiter(context.ip, 'pendingChannels');

View file

@ -47,6 +47,7 @@ export const getContext = (context: ResolverContext) => {
const cookies = cookie.parse(req.headers.cookie ?? '') || {};
const auth = cookies[appConstants.cookieName];
const lnMarketsAuth = cookies[appConstants.lnMarketsAuth];
const tokenAuth = cookies[appConstants.tokenCookieName];
let lnd: LndObject | null = null;
let id: string | null = null;
@ -72,6 +73,7 @@ export const getContext = (context: ResolverContext) => {
accounts: accountConfig,
res,
lnMarketsAuth,
tokenAuth,
};
return resolverContext;

View file

@ -8,7 +8,7 @@ export const githubResolvers = {
Query: {
getLatestVersion: async (
_: undefined,
params: any,
__: undefined,
context: ContextType
) => {
await requestLimiter(context.ip, 'getLnPay');

View file

@ -15,7 +15,7 @@ type ChannelFeesType = {
myFeeRate: number;
};
export default async (_: undefined, params: any, context: ContextType) => {
export default async (_: undefined, __: undefined, context: ContextType) => {
await requestLimiter(context.ip, 'getFeeHealth');
const { lnd } = context;

View file

@ -11,7 +11,7 @@ import { getChannelVolume, getChannelIdInfo, getAverage } from '../helpers';
const monthInBlocks = 4380;
export default async (_: undefined, params: any, context: ContextType) => {
export default async (_: undefined, __: undefined, context: ContextType) => {
await requestLimiter(context.ip, 'getVolumeHealth');
const { lnd } = context;

View file

@ -16,7 +16,11 @@ interface NetworkInfoProps {
export const networkResolvers = {
Query: {
getNetworkInfo: async (_: undefined, params: any, context: ContextType) => {
getNetworkInfo: async (
_: undefined,
__: undefined,
context: ContextType
) => {
await requestLimiter(context.ip, 'networkInfo');
const { lnd } = context;

View file

@ -28,7 +28,7 @@ export const nodeResolvers = {
return { lnd, publicKey, withChannels: !withoutChannels };
},
getNodeInfo: async (_: undefined, params: any, context: ContextType) => {
getNodeInfo: async (_: undefined, __: undefined, context: ContextType) => {
await requestLimiter(context.ip, 'nodeInfo');
const { lnd } = context;

View file

@ -19,7 +19,7 @@ interface PeerProps {
export const peerResolvers = {
Query: {
getPeers: async (_: undefined, params: any, context: ContextType) => {
getPeers: async (_: undefined, __: undefined, context: ContextType) => {
await requestLimiter(context.ip, 'getPeers');
const { lnd } = context;

View file

@ -3,6 +3,10 @@ import { requestLimiter } from 'server/helpers/rateLimiter';
import { toWithError } from 'server/helpers/async';
import { appUrls } from 'server/utils/appUrls';
import { request, gql } from 'graphql-request';
import { logger } from 'server/helpers/logger';
import { GraphQLError } from 'graphql';
import { appConstants } from 'server/utils/appConstants';
import cookieLib from 'cookie';
const getBaseCanConnectQuery = gql`
{
@ -10,6 +14,16 @@ const getBaseCanConnectQuery = gql`
}
`;
const getBaseInfoQuery = gql`
{
getInfo {
lastBosUpdate
apiTokenSatPrice
apiTokenOriginalSatPrice
}
}
`;
const getBaseNodesQuery = gql`
{
getNodes {
@ -39,6 +53,15 @@ const createBaseInvoiceQuery = gql`
}
`;
const createBaseTokenInvoiceQuery = gql`
mutation CreateTokenInvoice($days: Int) {
createTokenInvoice(days: $days) {
request
id
}
}
`;
const createThunderPointsQuery = gql`
mutation CreatePoints(
$id: String!
@ -50,8 +73,55 @@ const createThunderPointsQuery = gql`
}
`;
const createBaseTokenQuery = gql`
mutation CreateBaseToken($id: String!) {
createBaseToken(id: $id)
}
`;
const getBosScoresQuery = gql`
{
getBosScores {
updated
scores {
alias
public_key
score
updated
position
}
}
}
`;
const getBosNodeScoresQuery = gql`
query GetNodeScores($publicKey: String!, $token: String!) {
getNodeScores(publicKey: $publicKey, token: $token) {
alias
public_key
score
updated
position
}
}
`;
export const tbaseResolvers = {
Query: {
getBaseInfo: async (_: undefined, __: undefined, context: ContextType) => {
await requestLimiter(context.ip, 'getBaseInfo');
const [data, error] = await toWithError(
request(appUrls.tbase, getBaseInfoQuery)
);
if (error || !data?.getInfo) {
logger.error('Error getting info: %o', { error });
throw new GraphQLError('ErrorGettingInfo');
}
return data.getInfo;
},
getBaseCanConnect: async (
_: undefined,
__: undefined,
@ -67,6 +137,46 @@ export const tbaseResolvers = {
return true;
},
getBosNodeScores: async (
_: undefined,
{ publicKey }: { publicKey: string },
{ ip, tokenAuth }: ContextType
) => {
if (!tokenAuth) {
logger.error('No ThunderBase auth token available');
throw new GraphQLError('NotAuthenticated');
}
await requestLimiter(ip, 'getBosNodeScores');
const [data, error] = await toWithError(
request(appUrls.tbase, getBosNodeScoresQuery, {
publicKey,
token: tokenAuth,
})
);
if (error) {
logger.error('Error getting BOS scores: %o', { error });
throw new GraphQLError('ErrorGettingBosScores');
}
return data?.getNodeScores || [];
},
getBosScores: async (_: undefined, __: any, context: ContextType) => {
await requestLimiter(context.ip, 'getBosScores');
const [data, error] = await toWithError(
request(appUrls.tbase, getBosScoresQuery)
);
if (error || !data?.getBosScores) {
logger.error('Error getting BOS scores: %o', { error });
throw new GraphQLError('ErrorGettingBosScores');
}
return data.getBosScores;
},
getBaseNodes: async (_: undefined, __: any, context: ContextType) => {
await requestLimiter(context.ip, 'getBaseNodes');
@ -98,7 +208,7 @@ export const tbaseResolvers = {
params: { amount: number },
context: ContextType
) => {
await requestLimiter(context.ip, 'getBaseInvoice');
await requestLimiter(context.ip, 'createBaseInvoice');
if (!params?.amount) return '';
@ -111,12 +221,79 @@ export const tbaseResolvers = {
return null;
},
createBaseToken: async (
_: undefined,
{ id }: { id: string },
{ ip, res }: ContextType
) => {
await requestLimiter(ip, 'createBaseInvoice');
const [data, error] = await toWithError(
request(appUrls.tbase, createBaseTokenQuery, { id })
);
if (error || !data?.createBaseToken) {
logger.debug('Error getting thunderbase token: %o', { error });
throw new Error('ErrorGettingToken');
}
res.setHeader(
'Set-Cookie',
cookieLib.serialize(
appConstants.tokenCookieName,
data.createBaseToken,
{
maxAge: 60 * 60 * 24 * 30, //One month
httpOnly: true,
sameSite: true,
path: '/',
}
)
);
return true;
},
deleteBaseToken: async (
_: undefined,
__: undefined,
{ ip, res }: ContextType
) => {
await requestLimiter(ip, 'deleteBaseToken');
res.setHeader(
'Set-Cookie',
cookieLib.serialize(appConstants.tokenCookieName, '', {
maxAge: -1,
httpOnly: true,
sameSite: true,
path: '/',
})
);
return true;
},
createBaseTokenInvoice: async (
_: undefined,
__: undefined,
context: ContextType
) => {
await requestLimiter(context.ip, 'createBaseTokenInvoice');
const [data, error] = await toWithError(
request(appUrls.tbase, createBaseTokenInvoiceQuery)
);
if (error || !data?.createTokenInvoice) {
logger.error('Error getting invoice for token: %o', error);
throw new Error('ErrorGettingInvoice');
}
return data.createTokenInvoice;
},
createThunderPoints: async (
_: undefined,
params: { id: string; alias: string; uris: string[]; public_key: string },
context: ContextType
): Promise<boolean> => {
await requestLimiter(context.ip, 'getThunderPoints');
await requestLimiter(context.ip, 'createThunderPoints');
const [info, error] = await toWithError(
request(appUrls.tbase, createThunderPointsQuery, params)

View file

@ -17,4 +17,23 @@ export const tbaseTypes = gql`
id: String!
request: String!
}
type BosScore {
alias: String!
public_key: String!
score: Int!
updated: String!
position: Int!
}
type BosScoreResponse {
updated: String!
scores: [BosScore!]!
}
type BaseInfo {
lastBosUpdate: String!
apiTokenSatPrice: Int!
apiTokenOriginalSatPrice: Int!
}
`;

View file

@ -68,7 +68,7 @@ export const toolsResolvers = {
throw new Error(getErrorMsg(error));
}
},
getBackups: async (_: undefined, params: any, context: ContextType) => {
getBackups: async (_: undefined, __: undefined, context: ContextType) => {
await requestLimiter(context.ip, 'getBackups');
const { lnd } = context;
@ -83,7 +83,7 @@ export const toolsResolvers = {
throw new Error(getErrorMsg(error));
}
},
adminCheck: async (_: undefined, params: any, context: ContextType) => {
adminCheck: async (_: undefined, __: undefined, context: ContextType) => {
await requestLimiter(context.ip, 'adminCheck');
const { lnd } = context;

View file

@ -28,6 +28,9 @@ export const generalTypes = gql`
export const queryTypes = gql`
type Query {
getBosNodeScores(publicKey: String!): [BosScore]!
getBosScores: BosScoreResponse!
getBaseInfo: BaseInfo!
getBoltzSwapStatus(ids: [String]!): [BoltzSwap]!
getBoltzInfo: BoltzInfoType!
getLnMarketsStatus: String!
@ -120,6 +123,9 @@ export const mutationTypes = gql`
description: String
): String!
fetchLnUrl(url: String!): LnUrlRequest
createBaseTokenInvoice: baseInvoiceType
createBaseToken(id: String!): Boolean!
deleteBaseToken: Boolean!
createBaseInvoice(amount: Int!): baseInvoiceType
createThunderPoints(
id: String!

View file

@ -5,7 +5,11 @@ import { requestLimiter } from 'server/helpers/rateLimiter';
export const walletResolvers = {
Query: {
getWalletInfo: async (_: undefined, params: any, context: ContextType) => {
getWalletInfo: async (
_: undefined,
__: undefined,
context: ContextType
) => {
await requestLimiter(context.ip, 'getWalletInfo');
const { lnd } = context;

View file

@ -6,7 +6,7 @@ import { GetChannelsType } from 'server/types/ln-service.types';
export const getChannelReport = async (
_: undefined,
params: any,
__: undefined,
context: ContextType
) => {
await requestLimiter(context.ip, 'channelReport');

View file

@ -24,6 +24,7 @@ export const ContextMock: ContextType = {
],
res: {} as ServerResponse,
lnMarketsAuth: 'lnMarketAuth',
tokenAuth: 'tokenAuth',
};
export const ContextMockNoAccounts: ContextType = {
@ -39,6 +40,7 @@ export const ContextMockNoAccounts: ContextType = {
accounts: [],
res: {} as ServerResponse,
lnMarketsAuth: 'lnMarketAuth',
tokenAuth: 'tokenAuth',
};
export const ContextMockNoSSO: ContextType = {
@ -60,4 +62,5 @@ export const ContextMockNoSSO: ContextType = {
],
res: {} as ServerResponse,
lnMarketsAuth: 'lnMarketAuth',
tokenAuth: 'tokenAuth',
};

View file

@ -17,4 +17,5 @@ export type ContextType = {
accounts: ParsedAccount[];
res: ServerResponse;
lnMarketsAuth: string | null;
tokenAuth: string | null;
};

View file

@ -1,4 +1,5 @@
export const appConstants = {
cookieName: 'Thub-Auth',
lnMarketsAuth: 'LnMarkets-Auth',
tokenCookieName: 'Tbase-Auth',
};

View file

@ -0,0 +1,57 @@
import React, { createContext, useContext, useReducer } from 'react';
type State = {
hasToken: boolean;
};
type ActionType = {
type: 'change';
hasToken: boolean;
};
type Dispatch = (action: ActionType) => void;
export const StateContext = createContext<State | undefined>(undefined);
export const DispatchContext = createContext<Dispatch | undefined>(undefined);
const stateReducer = (state: State, action: ActionType): State => {
switch (action.type) {
case 'change':
return { hasToken: action.hasToken };
default:
return state;
}
};
const BaseProvider: React.FC<{ initialHasToken: boolean }> = ({
children,
initialHasToken = false,
}) => {
const [state, dispatch] = useReducer(stateReducer, {
hasToken: initialHasToken,
});
return (
<DispatchContext.Provider value={dispatch}>
<StateContext.Provider value={state}>{children}</StateContext.Provider>
</DispatchContext.Provider>
);
};
const useBaseState = () => {
const context = useContext(StateContext);
if (context === undefined) {
throw new Error('useBaseState must be used within a BaseProvider');
}
return context;
};
const useBaseDispatch = () => {
const context = useContext(DispatchContext);
if (context === undefined) {
throw new Error('useBaseDispatch must be used within a BaseProvider');
}
return context;
};
export { BaseProvider, useBaseState, useBaseDispatch };

View file

@ -0,0 +1,46 @@
/* eslint-disable */
import * as Types from '../../types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
export type CreateBaseTokenMutationVariables = Types.Exact<{
id: Types.Scalars['String'];
}>;
export type CreateBaseTokenMutation = (
{ __typename?: 'Mutation' }
& Pick<Types.Mutation, 'createBaseToken'>
);
export const CreateBaseTokenDocument = gql`
mutation CreateBaseToken($id: String!) {
createBaseToken(id: $id)
}
`;
export type CreateBaseTokenMutationFn = Apollo.MutationFunction<CreateBaseTokenMutation, CreateBaseTokenMutationVariables>;
/**
* __useCreateBaseTokenMutation__
*
* To run a mutation, you first call `useCreateBaseTokenMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useCreateBaseTokenMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [createBaseTokenMutation, { data, loading, error }] = useCreateBaseTokenMutation({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useCreateBaseTokenMutation(baseOptions?: Apollo.MutationHookOptions<CreateBaseTokenMutation, CreateBaseTokenMutationVariables>) {
return Apollo.useMutation<CreateBaseTokenMutation, CreateBaseTokenMutationVariables>(CreateBaseTokenDocument, baseOptions);
}
export type CreateBaseTokenMutationHookResult = ReturnType<typeof useCreateBaseTokenMutation>;
export type CreateBaseTokenMutationResult = Apollo.MutationResult<CreateBaseTokenMutation>;
export type CreateBaseTokenMutationOptions = Apollo.BaseMutationOptions<CreateBaseTokenMutation, CreateBaseTokenMutationVariables>;

View file

@ -0,0 +1,49 @@
/* eslint-disable */
import * as Types from '../../types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
export type CreateBaseTokenInvoiceMutationVariables = Types.Exact<{ [key: string]: never; }>;
export type CreateBaseTokenInvoiceMutation = (
{ __typename?: 'Mutation' }
& { createBaseTokenInvoice?: Types.Maybe<(
{ __typename?: 'baseInvoiceType' }
& Pick<Types.BaseInvoiceType, 'request' | 'id'>
)> }
);
export const CreateBaseTokenInvoiceDocument = gql`
mutation CreateBaseTokenInvoice {
createBaseTokenInvoice {
request
id
}
}
`;
export type CreateBaseTokenInvoiceMutationFn = Apollo.MutationFunction<CreateBaseTokenInvoiceMutation, CreateBaseTokenInvoiceMutationVariables>;
/**
* __useCreateBaseTokenInvoiceMutation__
*
* To run a mutation, you first call `useCreateBaseTokenInvoiceMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useCreateBaseTokenInvoiceMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [createBaseTokenInvoiceMutation, { data, loading, error }] = useCreateBaseTokenInvoiceMutation({
* variables: {
* },
* });
*/
export function useCreateBaseTokenInvoiceMutation(baseOptions?: Apollo.MutationHookOptions<CreateBaseTokenInvoiceMutation, CreateBaseTokenInvoiceMutationVariables>) {
return Apollo.useMutation<CreateBaseTokenInvoiceMutation, CreateBaseTokenInvoiceMutationVariables>(CreateBaseTokenInvoiceDocument, baseOptions);
}
export type CreateBaseTokenInvoiceMutationHookResult = ReturnType<typeof useCreateBaseTokenInvoiceMutation>;
export type CreateBaseTokenInvoiceMutationResult = Apollo.MutationResult<CreateBaseTokenInvoiceMutation>;
export type CreateBaseTokenInvoiceMutationOptions = Apollo.BaseMutationOptions<CreateBaseTokenInvoiceMutation, CreateBaseTokenInvoiceMutationVariables>;

View file

@ -0,0 +1,43 @@
/* eslint-disable */
import * as Types from '../../types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
export type DeleteBaseTokenMutationVariables = Types.Exact<{ [key: string]: never; }>;
export type DeleteBaseTokenMutation = (
{ __typename?: 'Mutation' }
& Pick<Types.Mutation, 'deleteBaseToken'>
);
export const DeleteBaseTokenDocument = gql`
mutation DeleteBaseToken {
deleteBaseToken
}
`;
export type DeleteBaseTokenMutationFn = Apollo.MutationFunction<DeleteBaseTokenMutation, DeleteBaseTokenMutationVariables>;
/**
* __useDeleteBaseTokenMutation__
*
* To run a mutation, you first call `useDeleteBaseTokenMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useDeleteBaseTokenMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [deleteBaseTokenMutation, { data, loading, error }] = useDeleteBaseTokenMutation({
* variables: {
* },
* });
*/
export function useDeleteBaseTokenMutation(baseOptions?: Apollo.MutationHookOptions<DeleteBaseTokenMutation, DeleteBaseTokenMutationVariables>) {
return Apollo.useMutation<DeleteBaseTokenMutation, DeleteBaseTokenMutationVariables>(DeleteBaseTokenDocument, baseOptions);
}
export type DeleteBaseTokenMutationHookResult = ReturnType<typeof useDeleteBaseTokenMutation>;
export type DeleteBaseTokenMutationResult = Apollo.MutationResult<DeleteBaseTokenMutation>;
export type DeleteBaseTokenMutationOptions = Apollo.BaseMutationOptions<DeleteBaseTokenMutation, DeleteBaseTokenMutationVariables>;

View file

@ -0,0 +1,7 @@
import { gql } from '@apollo/client';
export const CREATE_BASE_TOKEN = gql`
mutation CreateBaseToken($id: String!) {
createBaseToken(id: $id)
}
`;

View file

@ -0,0 +1,10 @@
import { gql } from '@apollo/client';
export const CREATE_BASE_TOKEN_INVOICE = gql`
mutation CreateBaseTokenInvoice {
createBaseTokenInvoice {
request
id
}
}
`;

View file

@ -0,0 +1,7 @@
import { gql } from '@apollo/client';
export const DELETE_BASE_TOKEN = gql`
mutation DeleteBaseToken {
deleteBaseToken
}
`;

View file

@ -0,0 +1,51 @@
/* eslint-disable */
import * as Types from '../../types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
export type GetBaseInfoQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type GetBaseInfoQuery = (
{ __typename?: 'Query' }
& { getBaseInfo: (
{ __typename?: 'BaseInfo' }
& Pick<Types.BaseInfo, 'lastBosUpdate' | 'apiTokenSatPrice' | 'apiTokenOriginalSatPrice'>
) }
);
export const GetBaseInfoDocument = gql`
query GetBaseInfo {
getBaseInfo {
lastBosUpdate
apiTokenSatPrice
apiTokenOriginalSatPrice
}
}
`;
/**
* __useGetBaseInfoQuery__
*
* To run a query within a React component, call `useGetBaseInfoQuery` and pass it any options that fit your needs.
* When your component renders, `useGetBaseInfoQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetBaseInfoQuery({
* variables: {
* },
* });
*/
export function useGetBaseInfoQuery(baseOptions?: Apollo.QueryHookOptions<GetBaseInfoQuery, GetBaseInfoQueryVariables>) {
return Apollo.useQuery<GetBaseInfoQuery, GetBaseInfoQueryVariables>(GetBaseInfoDocument, baseOptions);
}
export function useGetBaseInfoLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetBaseInfoQuery, GetBaseInfoQueryVariables>) {
return Apollo.useLazyQuery<GetBaseInfoQuery, GetBaseInfoQueryVariables>(GetBaseInfoDocument, baseOptions);
}
export type GetBaseInfoQueryHookResult = ReturnType<typeof useGetBaseInfoQuery>;
export type GetBaseInfoLazyQueryHookResult = ReturnType<typeof useGetBaseInfoLazyQuery>;
export type GetBaseInfoQueryResult = Apollo.QueryResult<GetBaseInfoQuery, GetBaseInfoQueryVariables>;

View file

@ -0,0 +1,56 @@
/* eslint-disable */
import * as Types from '../../types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
export type GetBosNodeScoresQueryVariables = Types.Exact<{
publicKey: Types.Scalars['String'];
}>;
export type GetBosNodeScoresQuery = (
{ __typename?: 'Query' }
& { getBosNodeScores: Array<Types.Maybe<(
{ __typename?: 'BosScore' }
& Pick<Types.BosScore, 'alias' | 'public_key' | 'score' | 'updated' | 'position'>
)>> }
);
export const GetBosNodeScoresDocument = gql`
query GetBosNodeScores($publicKey: String!) {
getBosNodeScores(publicKey: $publicKey) {
alias
public_key
score
updated
position
}
}
`;
/**
* __useGetBosNodeScoresQuery__
*
* To run a query within a React component, call `useGetBosNodeScoresQuery` and pass it any options that fit your needs.
* When your component renders, `useGetBosNodeScoresQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetBosNodeScoresQuery({
* variables: {
* publicKey: // value for 'publicKey'
* },
* });
*/
export function useGetBosNodeScoresQuery(baseOptions: Apollo.QueryHookOptions<GetBosNodeScoresQuery, GetBosNodeScoresQueryVariables>) {
return Apollo.useQuery<GetBosNodeScoresQuery, GetBosNodeScoresQueryVariables>(GetBosNodeScoresDocument, baseOptions);
}
export function useGetBosNodeScoresLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetBosNodeScoresQuery, GetBosNodeScoresQueryVariables>) {
return Apollo.useLazyQuery<GetBosNodeScoresQuery, GetBosNodeScoresQueryVariables>(GetBosNodeScoresDocument, baseOptions);
}
export type GetBosNodeScoresQueryHookResult = ReturnType<typeof useGetBosNodeScoresQuery>;
export type GetBosNodeScoresLazyQueryHookResult = ReturnType<typeof useGetBosNodeScoresLazyQuery>;
export type GetBosNodeScoresQueryResult = Apollo.QueryResult<GetBosNodeScoresQuery, GetBosNodeScoresQueryVariables>;

View file

@ -0,0 +1,60 @@
/* eslint-disable */
import * as Types from '../../types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
export type GetBosScoresQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type GetBosScoresQuery = (
{ __typename?: 'Query' }
& { getBosScores: (
{ __typename?: 'BosScoreResponse' }
& Pick<Types.BosScoreResponse, 'updated'>
& { scores: Array<(
{ __typename?: 'BosScore' }
& Pick<Types.BosScore, 'alias' | 'public_key' | 'score' | 'updated' | 'position'>
)> }
) }
);
export const GetBosScoresDocument = gql`
query GetBosScores {
getBosScores {
updated
scores {
alias
public_key
score
updated
position
}
}
}
`;
/**
* __useGetBosScoresQuery__
*
* To run a query within a React component, call `useGetBosScoresQuery` and pass it any options that fit your needs.
* When your component renders, `useGetBosScoresQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetBosScoresQuery({
* variables: {
* },
* });
*/
export function useGetBosScoresQuery(baseOptions?: Apollo.QueryHookOptions<GetBosScoresQuery, GetBosScoresQueryVariables>) {
return Apollo.useQuery<GetBosScoresQuery, GetBosScoresQueryVariables>(GetBosScoresDocument, baseOptions);
}
export function useGetBosScoresLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetBosScoresQuery, GetBosScoresQueryVariables>) {
return Apollo.useLazyQuery<GetBosScoresQuery, GetBosScoresQueryVariables>(GetBosScoresDocument, baseOptions);
}
export type GetBosScoresQueryHookResult = ReturnType<typeof useGetBosScoresQuery>;
export type GetBosScoresLazyQueryHookResult = ReturnType<typeof useGetBosScoresLazyQuery>;
export type GetBosScoresQueryResult = Apollo.QueryResult<GetBosScoresQuery, GetBosScoresQueryVariables>;

View file

@ -0,0 +1,11 @@
import { gql } from '@apollo/client';
export const GET_BASE_INFO = gql`
query GetBaseInfo {
getBaseInfo {
lastBosUpdate
apiTokenSatPrice
apiTokenOriginalSatPrice
}
}
`;

View file

@ -0,0 +1,13 @@
import { gql } from '@apollo/client';
export const GET_BOS_NODE_SCORES = gql`
query GetBosNodeScores($publicKey: String!) {
getBosNodeScores(publicKey: $publicKey) {
alias
public_key
score
updated
position
}
}
`;

View file

@ -0,0 +1,16 @@
import { gql } from '@apollo/client';
export const GET_BOS_SCORES = gql`
query GetBosScores {
getBosScores {
updated
scores {
alias
public_key
score
updated
position
}
}
}
`;

View file

@ -1,6 +1,8 @@
/* eslint-disable */
export type Maybe<T> = T | null;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: string;
@ -41,6 +43,9 @@ export type PermissionsType = {
export type Query = {
__typename?: 'Query';
getBosNodeScores: Array<Maybe<BosScore>>;
getBosScores: BosScoreResponse;
getBaseInfo: BaseInfo;
getBoltzSwapStatus: Array<Maybe<BoltzSwap>>;
getBoltzInfo: BoltzInfoType;
getLnMarketsStatus: Scalars['String'];
@ -89,6 +94,11 @@ export type Query = {
};
export type QueryGetBosNodeScoresArgs = {
publicKey: Scalars['String'];
};
export type QueryGetBoltzSwapStatusArgs = {
ids: Array<Maybe<Scalars['String']>>;
};
@ -211,6 +221,9 @@ export type Mutation = {
lnUrlPay: PaySuccess;
lnUrlWithdraw: Scalars['String'];
fetchLnUrl?: Maybe<LnUrlRequest>;
createBaseTokenInvoice?: Maybe<BaseInvoiceType>;
createBaseToken: Scalars['Boolean'];
deleteBaseToken: Scalars['Boolean'];
createBaseInvoice?: Maybe<BaseInvoiceType>;
createThunderPoints: Scalars['Boolean'];
closeChannel?: Maybe<CloseChannelType>;
@ -295,6 +308,11 @@ export type MutationFetchLnUrlArgs = {
};
export type MutationCreateBaseTokenArgs = {
id: Scalars['String'];
};
export type MutationCreateBaseInvoiceArgs = {
amount: Scalars['Int'];
};
@ -1059,6 +1077,28 @@ export type BaseInvoiceType = {
request: Scalars['String'];
};
export type BosScore = {
__typename?: 'BosScore';
alias: Scalars['String'];
public_key: Scalars['String'];
score: Scalars['Int'];
updated: Scalars['String'];
position: Scalars['Int'];
};
export type BosScoreResponse = {
__typename?: 'BosScoreResponse';
updated: Scalars['String'];
scores: Array<BosScore>;
};
export type BaseInfo = {
__typename?: 'BaseInfo';
lastBosUpdate: Scalars['String'];
apiTokenSatPrice: Scalars['Int'];
apiTokenOriginalSatPrice: Scalars['Int'];
};
export type WithdrawRequest = {
__typename?: 'WithdrawRequest';
callback?: Maybe<Scalars['String']>;

View file

@ -4,7 +4,10 @@ import { useGetBaseCanConnectQuery } from 'src/graphql/queries/__generated__/get
export const useBaseConnect = () => {
const [canConnect, setCanConnect] = useState<boolean>(false);
const { loading, error, data } = useGetBaseCanConnectQuery({ ssr: false });
const { loading, error, data } = useGetBaseCanConnectQuery({
ssr: false,
fetchPolicy: 'cache-first',
});
useEffect(() => {
if (loading || !data?.getBaseCanConnect || error) return;

View file

@ -19,6 +19,7 @@ type StatusState = {
pendingChannelCount: number;
closedChannelCount: number;
peersCount: number;
publicKey: string;
};
const initialState = {
@ -37,6 +38,7 @@ const initialState = {
pendingChannelCount: 0,
closedChannelCount: 0,
peersCount: 0,
publicKey: '',
};
export const useNodeInfo = (): StatusState => {
@ -64,6 +66,7 @@ export const useNodeInfo = (): StatusState => {
pending_channels_count,
closed_channels_count,
peers_count,
public_key,
} = getNodeInfo as NodeInfoType;
const {
confirmedBalance,
@ -88,6 +91,7 @@ export const useNodeInfo = (): StatusState => {
pendingChannelCount: pending_channels_count,
closedChannelCount: closed_channels_count,
peersCount: peers_count,
publicKey: public_key,
});
}, [data, error, loading]);

View file

@ -16,6 +16,7 @@ import {
Icon,
Heart,
Shuffle,
Aperture,
} from 'react-feather';
import { useRouter } from 'next/router';
import { useBaseConnect } from 'src/hooks/UseBaseConnect';
@ -128,6 +129,7 @@ const DONATIONS = '/leaderboard';
const CHAT = '/chat';
const SETTINGS = '/settings';
const SWAP = '/swap';
const SCORES = '/scores';
interface NavigationProps {
isBurger?: boolean;
@ -181,6 +183,7 @@ export const Navigation = ({ isBurger, setOpen }: NavigationProps) => {
{renderNavButton('Tools', TOOLS, Shield, sidebar)}
{renderNavButton('Swap', SWAP, Shuffle, sidebar)}
{renderNavButton('Stats', STATS, BarChart2, sidebar)}
{connected && renderNavButton('Scores', SCORES, Aperture)}
</ButtonSection>
);
@ -197,6 +200,7 @@ export const Navigation = ({ isBurger, setOpen }: NavigationProps) => {
{renderBurgerNav('Tools', TOOLS, Shield)}
{renderBurgerNav('Swap', SWAP, Shuffle)}
{renderBurgerNav('Stats', STATS, BarChart2)}
{connected && renderBurgerNav('Scores', SCORES, Aperture)}
{connected && renderBurgerNav('Donations', DONATIONS, Heart)}
{renderBurgerNav('Chat', CHAT, MessageCircle)}
{renderBurgerNav('Settings', SETTINGS, Settings)}

View file

@ -8,22 +8,25 @@ import { GET_AUTH_TOKEN } from 'src/graphql/mutations/getAuthToken';
const cookieProps = (
context: NextPageContext,
noAuth?: boolean
): { theme: string; authenticated: boolean } => {
if (!context?.req) return { theme: 'dark', authenticated: false };
): { theme: string; authenticated: boolean; hasToken: boolean } => {
if (!context?.req)
return { theme: 'dark', authenticated: false, hasToken: false };
const cookies = parseCookies(context.req);
const hasToken = !!cookies[appConstants.tokenCookieName];
if (!cookies[appConstants.cookieName] && !noAuth) {
context.res?.writeHead(302, { Location: '/login' });
context.res?.end();
return { theme: 'dark', authenticated: false };
return { theme: 'dark', authenticated: false, hasToken };
}
if (cookies?.theme) {
return { theme: cookies.theme, authenticated: true };
return { theme: cookies.theme, authenticated: true, hasToken };
}
return { theme: 'dark', authenticated: true };
return { theme: 'dark', authenticated: true, hasToken };
};
type QueryProps = {
@ -54,7 +57,7 @@ export const getProps = async (
});
}
const { theme, authenticated } = cookieProps(context, noAuth);
const { theme, authenticated, hasToken } = cookieProps(context, noAuth);
const apolloClient = initializeApollo(undefined, context);
@ -76,13 +79,14 @@ export const getProps = async (
}
}
} else {
return { props: { initialConfig: { theme } } };
return { props: { initialConfig: { theme }, hasToken } };
}
return {
props: {
initialApolloState: apolloClient.cache.extract(),
initialConfig: { theme },
hasToken,
},
};
};

View file

@ -0,0 +1,202 @@
import React, { useMemo, useCallback } from 'react';
import { AreaClosed, Line, Bar } from '@visx/shape';
import { curveMonotoneX } from '@visx/curve';
import { scaleTime, scaleLinear } from '@visx/scale';
import {
withTooltip,
Tooltip,
TooltipWithBounds,
defaultStyles,
} from '@visx/tooltip';
import { WithTooltipProvidedProps } from '@visx/tooltip/lib/enhancers/withTooltip';
import { localPoint } from '@visx/event';
import { max, extent, bisector } from 'd3-array';
import { chartColors, themeColors } from 'src/styles/Themes';
import format from 'date-fns/format';
const tooltipStyles = {
...defaultStyles,
background: themeColors.blue7,
border: '1px solid white',
color: 'white',
};
const getDate = (d: DataType) => new Date(d.date);
const getValue = (d: DataType) => d.value;
const bisectDate = bisector<DataType, Date>(d => new Date(d.date)).left;
type DataType = {
date: string;
value: number;
};
export type AreaProps = {
data: DataType[];
areaColor?: string;
lineColor?: string;
tooltipText?: string;
clickCallback?: () => void;
width: number;
height: number;
margin?: { top: number; right: number; bottom: number; left: number };
};
export const AreaGraph = withTooltip<AreaProps, DataType>(
({
data,
areaColor = chartColors.orange,
lineColor = themeColors.blue2,
tooltipText,
clickCallback,
width,
height,
margin = { top: 0, right: 0, bottom: 0, left: 0 },
showTooltip,
hideTooltip,
tooltipData,
tooltipTop = 0,
tooltipLeft = 0,
}: AreaProps & WithTooltipProvidedProps<DataType>) => {
if (width < 10) return null;
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
const dateScale = useMemo(
() =>
scaleTime({
range: [margin.left, innerWidth + margin.left],
domain: extent(data, getDate) as [Date, Date],
}),
[innerWidth, margin.left, data]
);
const valueScale = useMemo(
() =>
scaleLinear({
range: [innerHeight + margin.top, margin.top],
domain: [0, max(data, getValue) || 0],
nice: false,
}),
[margin.top, innerHeight, data]
);
const handleTooltip = useCallback(
(
event:
| React.TouchEvent<SVGRectElement>
| React.MouseEvent<SVGRectElement>
) => {
const { x } = localPoint(event) || { x: 0 };
const x0 = dateScale.invert(x);
const index = bisectDate(data, x0, 1);
const d0 = data[index - 1];
const d1 = data[index];
let d = d0;
if (d1 && getDate(d1)) {
const firstPart = x0.valueOf() - getDate(d0).valueOf();
const secondPart = getDate(d1).valueOf() - x0.valueOf();
d = firstPart > secondPart ? d1 : d0;
}
showTooltip({
tooltipData: d,
tooltipLeft: x,
tooltipTop: valueScale(getValue(d)),
});
},
[showTooltip, valueScale, dateScale, data]
);
return (
<div>
<svg width={width} height={height}>
<AreaClosed<DataType>
data={data}
x={d => dateScale(getDate(d)) ?? 0}
y={d => valueScale(getValue(d)) ?? 0}
yScale={valueScale}
strokeWidth={1}
stroke={areaColor}
fill={areaColor}
curve={curveMonotoneX}
/>
<Bar
x={margin.left}
y={margin.top}
width={innerWidth}
height={innerHeight}
fill="transparent"
rx={14}
onTouchStart={handleTooltip}
onTouchMove={handleTooltip}
onMouseMove={handleTooltip}
onMouseLeave={() => hideTooltip()}
onClick={() => {
clickCallback && clickCallback();
}}
/>
{tooltipData && (
<g>
<Line
from={{ x: tooltipLeft, y: margin.top }}
to={{ x: tooltipLeft, y: innerHeight + margin.top }}
stroke={lineColor}
strokeWidth={2}
pointerEvents="none"
/>
<circle
cx={tooltipLeft}
cy={tooltipTop + 1}
r={4}
fill="black"
fillOpacity={0.1}
stroke="black"
strokeOpacity={0.1}
strokeWidth={2}
pointerEvents="none"
/>
<circle
cx={tooltipLeft}
cy={tooltipTop}
r={4}
fill={lineColor}
stroke="white"
strokeWidth={2}
pointerEvents="none"
/>
</g>
)}
</svg>
{tooltipData && (
<div>
<TooltipWithBounds
key={Math.random()}
top={tooltipTop - 12}
left={tooltipLeft + 12}
style={tooltipStyles}
>
{`${tooltipText}${getValue(tooltipData)}`}
</TooltipWithBounds>
<Tooltip
top={innerHeight + margin.top - 14}
left={tooltipLeft}
style={{
...defaultStyles,
minWidth: 72,
textAlign: 'center',
transform: 'translateX(-50%)',
}}
>
{format(getDate(tooltipData), 'LLL d, yyyy')}
</Tooltip>
</div>
)}
</div>
);
}
);

View file

@ -0,0 +1,70 @@
import { ParentSize } from '@visx/responsive';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { LoadingCard } from 'src/components/loading/LoadingCard';
import { useBaseState } from 'src/context/BaseContext';
import { useGetBosNodeScoresQuery } from 'src/graphql/queries/__generated__/getBosNodeScores.generated';
import { chartColors, mediaWidths, themeColors } from 'src/styles/Themes';
import { getErrorContent } from 'src/utils/error';
import styled from 'styled-components';
import { isArray } from 'underscore';
import { AreaGraph } from './AreaGraph';
const Wrapper = styled.div`
height: 160px;
width: 100%;
margin: 32px 0;
@media (${mediaWidths.mobile}) {
height: 80px;
}
`;
export const Graph = () => {
const [isPos, setIsPos] = useState<boolean>(false);
const { hasToken } = useBaseState();
const { query } = useRouter();
const { id } = query;
const toggle = () => setIsPos(p => !p);
const publicKey = (isArray(id) ? id[0] : id) || '';
const { data, loading } = useGetBosNodeScoresQuery({
skip: !hasToken,
variables: { publicKey },
fetchPolicy: 'cache-first',
onError: error => toast.error(getErrorContent(error)),
});
if (loading) {
return <LoadingCard noCard={true} />;
}
const scores = data?.getBosNodeScores || [];
const final = scores
.map(s => ({
date: s?.updated || '',
value: (isPos ? s?.position : s?.score) || 0,
}))
.reverse();
return (
<Wrapper>
<ParentSize>
{parent => (
<AreaGraph
data={final}
width={parent.width}
height={parent.height}
clickCallback={toggle}
areaColor={isPos ? themeColors.blue2 : chartColors.orange}
lineColor={isPos ? chartColors.orange : themeColors.blue2}
tooltipText={isPos ? 'Position:' : 'Score:'}
/>
)}
</ParentSize>
</Wrapper>
);
};

View file

@ -0,0 +1,68 @@
import React from 'react';
import { useRouter } from 'next/router';
import { useGetNodeQuery } from 'src/graphql/queries/__generated__/getNode.generated';
import { isArray } from 'underscore';
import {
Card,
CardWithTitle,
DarkSubTitle,
SubTitle,
} from 'src/components/generic/Styled';
import { LoadingCard } from 'src/components/loading/LoadingCard';
import {
getDateDif,
getFormatDate,
renderLine,
} from 'src/components/generic/helpers';
import { Price } from 'src/components/price/Price';
import { useBaseState } from 'src/context/BaseContext';
import { toast } from 'react-toastify';
import { getErrorContent } from 'src/utils/error';
export const NodeInfo = () => {
const { hasToken } = useBaseState();
const { query } = useRouter();
const { id } = query;
const publicKey = (isArray(id) ? id[0] : id) || '';
const { data, loading } = useGetNodeQuery({
skip: !hasToken,
variables: { publicKey },
onError: error => toast.error(getErrorContent(error)),
});
if (!hasToken) {
return <LoadingCard noTitle={true} />;
}
if (loading) {
return <LoadingCard title={'Node Info'} />;
}
if (!data?.getNode.node || data?.getNode?.node?.alias === 'Node not found') {
return (
<CardWithTitle>
<SubTitle>Node Info</SubTitle>
<Card>
<DarkSubTitle>Node not found</DarkSubTitle>
</Card>
</CardWithTitle>
);
}
const { alias, channel_count, capacity, updated_at } = data.getNode.node;
return (
<CardWithTitle>
<SubTitle>Node Info</SubTitle>
<Card>
<SubTitle>{alias}</SubTitle>
{renderLine('Channel Count', channel_count)}
{renderLine('Capacity', <Price amount={capacity} />)}
{renderLine('Last Update', `${getDateDif(updated_at)} ago`)}
{renderLine('Last Update Date', getFormatDate(updated_at))}
</Card>
</CardWithTitle>
);
};

View file

@ -0,0 +1,66 @@
import React, { FC, useEffect } from 'react';
import { useRouter } from 'next/router';
import { isArray } from 'underscore';
import { Card, CardWithTitle, SubTitle } from 'src/components/generic/Styled';
import { LoadingCard } from 'src/components/loading/LoadingCard';
import { useBaseState } from 'src/context/BaseContext';
import { useGetBosNodeScoresQuery } from 'src/graphql/queries/__generated__/getBosNodeScores.generated';
import { Table } from 'src/components/table';
import { getFormatDate } from 'src/components/generic/helpers';
type NodeScoresProps = {
callback: () => void;
errorCallback: () => void;
};
export const NodeScores: FC<NodeScoresProps> = ({
callback,
errorCallback,
}) => {
const { hasToken } = useBaseState();
const { query } = useRouter();
const { id } = query;
const publicKey = (isArray(id) ? id[0] : id) || '';
const { data, loading, error } = useGetBosNodeScoresQuery({
skip: !hasToken,
variables: { publicKey },
fetchPolicy: 'cache-first',
});
useEffect(() => {
if (!error) return;
errorCallback();
}, [error, errorCallback]);
useEffect(() => {
if (loading || !data?.getBosNodeScores) return;
callback();
}, [loading, data, callback]);
if (loading) {
return <LoadingCard title={'Node Score Info'} />;
}
const columns = [
{ Header: 'Date', accessor: 'date' },
{ Header: 'Score', accessor: 'score' },
{ Header: 'Position', accessor: 'position' },
];
const finalData = data?.getBosNodeScores || [];
const tableData = finalData.map(s => ({
...s,
date: getFormatDate(s?.updated),
}));
return (
<CardWithTitle>
<SubTitle>Historical Scores</SubTitle>
<Card>
<Table withBorder={true} tableData={tableData} tableColumns={columns} />
</Card>
</CardWithTitle>
);
};

View file

@ -0,0 +1,91 @@
import { useRouter } from 'next/router';
import { FC } from 'react';
import { getNodeLink, renderLine } from 'src/components/generic/helpers';
import { Link } from 'src/components/link/Link';
import { BosScore } from 'src/graphql/types';
import { themeColors } from 'src/styles/Themes';
import { appendBasePath } from 'src/utils/basePath';
import styled from 'styled-components';
type ScoreCardProps = {
score: BosScore | null;
};
const getBorderColor = (index: number) => {
switch (index) {
case 1:
return 'gold';
case 2:
return 'orange';
case 3:
return 'white';
default:
return themeColors.blue2;
}
};
const getWidth = (index: number): string => {
switch (index) {
case 1:
case 2:
case 3:
return '2px';
default:
return '1px';
}
};
const S = {
Score: styled.div<{ borderWidth?: string; borderColor?: string }>`
padding: 8px;
border: ${({ borderWidth }) => borderWidth || '2px'} solid
${({ borderColor }) => borderColor || 'gold'};
border-radius: 8px;
text-align: center;
margin: 0 0 16px;
cursor: pointer;
`,
NoScore: styled.div`
padding: 8px;
border-radius: 8px;
text-align: center;
margin: 0 0 16px;
cursor: pointer;
`,
};
export const ScoreCard: FC<ScoreCardProps> = ({ score }) => {
const { push } = useRouter();
if (!score) {
return (
<S.NoScore>
<Link
href={'https://openoms.gitbook.io/lightning-node-management/bosscore'}
newTab={true}
>
This node is not in the BOS list. Read more about these scores here.
</Link>
</S.NoScore>
);
}
const handleClick = () => {
if (score.public_key) {
push(appendBasePath(`/scores/${score.public_key}`));
}
};
return (
<S.Score
borderWidth={getWidth(score.position)}
borderColor={getBorderColor(score.position)}
onClick={handleClick}
>
{score.alias}
{renderLine('Position', score.position)}
{renderLine('Score', score.score)}
{renderLine('Public Key', getNodeLink(score.public_key))}
</S.Score>
);
};

View file

@ -0,0 +1,55 @@
import { ColorButton } from 'src/components/buttons/colorButton/ColorButton';
import { toast } from 'react-toastify';
import Modal from 'src/components/modal/ReactModal';
import { useCreateBaseTokenInvoiceMutation } from 'src/graphql/mutations/__generated__/createBaseTokenInvoice.generated';
import { getErrorContent } from 'src/utils/error';
import { RequestModal } from 'src/views/home/account/pay/RequestModal';
import { chartColors } from 'src/styles/Themes';
import { FC, useEffect, useState } from 'react';
export const BuyButton: FC<{ paidCallback: (id: string) => void }> = ({
children,
paidCallback,
}) => {
const [modalOpen, modalOpenSet] = useState<boolean>(false);
const [invoice, invoiceSet] = useState<string>('');
const [buy, { data, loading }] = useCreateBaseTokenInvoiceMutation({
onError: err => toast.error(getErrorContent(err)),
});
useEffect(() => {
if (loading || !data?.createBaseTokenInvoice) return;
invoiceSet(data.createBaseTokenInvoice.request);
modalOpenSet(true);
}, [loading, data]);
const handleReset = () => {
invoiceSet('');
modalOpenSet(false);
};
const handlePaidReset = () => {
invoiceSet('');
modalOpenSet(false);
paidCallback(data?.createBaseTokenInvoice?.id || '');
};
return (
<>
<ColorButton
loading={loading}
disabled={loading}
withMargin={'24px 0 0'}
fullWidth={true}
color={chartColors.green}
onClick={buy}
>
{children}
</ColorButton>
<Modal isOpen={modalOpen} closeCallback={handleReset}>
<RequestModal request={invoice} handleReset={handlePaidReset} />
</Modal>
</>
);
};

View file

@ -0,0 +1,63 @@
import { FC, useEffect } from 'react';
import { toast } from 'react-toastify';
import { renderLine } from 'src/components/generic/helpers';
import { Card, DarkSubTitle, Separation } from 'src/components/generic/Styled';
import { LoadingCard } from 'src/components/loading/LoadingCard';
import { useBaseDispatch } from 'src/context/BaseContext';
import { useCreateBaseTokenMutation } from 'src/graphql/mutations/__generated__/createBaseToken.generated';
import { chartColors } from 'src/styles/Themes';
import { getErrorContent } from 'src/utils/error';
import styled from 'styled-components';
const S = {
title: styled.div`
color: ${chartColors.green};
width: 100%;
text-align: center;
font-size: 24px;
`,
center: styled.div`
width: 100%;
text-align: center;
`,
};
export const PaidCard: FC<{ id: string }> = ({ id }) => {
const dispatch = useBaseDispatch();
const [getToken, { data, loading }] = useCreateBaseTokenMutation({
onError: err => toast.error(getErrorContent(err)),
variables: { id },
});
useEffect(() => {
if (!id) return;
getToken();
}, [id, getToken]);
useEffect(() => {
if (loading || !data?.createBaseToken) return;
dispatch({ type: 'change', hasToken: true });
}, [loading, data, dispatch]);
if (loading) {
return <LoadingCard noTitle={true} />;
}
return (
<Card>
<S.title>Thank you for the purchase!</S.title>
<Separation />
<S.center>
<DarkSubTitle>
This is your payment backup id you can use to recover the token in
case it gets deleted.
</DarkSubTitle>
<DarkSubTitle>
You can also get it from the transaction in the Transaction View.
</DarkSubTitle>
</S.center>
<Separation />
{renderLine('Backup Id', id)}
</Card>
);
};

View file

@ -0,0 +1,50 @@
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { ColorButton } from 'src/components/buttons/colorButton/ColorButton';
import { Card, CardWithTitle, SubTitle } from 'src/components/generic/Styled';
import { InputWithDeco } from 'src/components/input/InputWithDeco';
import { useBaseDispatch } from 'src/context/BaseContext';
import { useCreateBaseTokenMutation } from 'src/graphql/mutations/__generated__/createBaseToken.generated';
import { appendBasePath } from 'src/utils/basePath';
import { getErrorContent } from 'src/utils/error';
export const RecoverToken = () => {
const { push } = useRouter();
const [id, setId] = useState<string>('');
const dispatch = useBaseDispatch();
const [getToken, { data, loading }] = useCreateBaseTokenMutation({
onError: err => toast.error(getErrorContent(err)),
});
useEffect(() => {
if (loading || !data?.createBaseToken) return;
dispatch({ type: 'change', hasToken: true });
toast.success('Succesfully recovered token');
push(appendBasePath('/scores'));
}, [loading, data, dispatch, push]);
return (
<CardWithTitle>
<SubTitle>Recover a paid token</SubTitle>
<Card>
<InputWithDeco
title={'Backup Id'}
value={id}
placeholder={'Transaction Id'}
inputCallback={setId}
/>
<ColorButton
loading={loading}
disabled={loading || !id}
fullWidth={true}
withMargin={'16px 0 0'}
onClick={() => getToken({ variables: { id } })}
>
Recover Token
</ColorButton>
</Card>
</CardWithTitle>
);
};

View file

@ -0,0 +1,151 @@
import React, { FC } from 'react';
import {
Card,
CardWithTitle,
DarkSubTitle,
} from 'src/components/generic/Styled';
import { useGetBaseInfoQuery } from 'src/graphql/queries/__generated__/getBaseInfo.generated';
import { LifeBuoy } from 'react-feather';
import styled from 'styled-components';
import {
cardColor,
chartColors,
mediaWidths,
themeColors,
} from 'src/styles/Themes';
import { LoadingCard } from 'src/components/loading/LoadingCard';
import { getPrice } from 'src/components/price/Price';
import { usePriceState } from 'src/context/PriceContext';
import { useConfigState } from 'src/context/ConfigContext';
import { BuyButton } from 'src/views/token/BuyButton';
const S = {
Row: styled.div`
display: flex;
position: relative;
@media (${mediaWidths.mobile}) {
flex-direction: column;
align-items: center;
text-align: center;
}
`,
Title: styled.h3`
margin: 0 0 8px;
@media (${mediaWidths.mobile}) {
margin: 0 0 24px;
}
`,
Text: styled.div`
padding-left: 16px;
`,
IconWrapper: styled.div`
width: 120px;
height: auto;
display: flex;
justify-content: center;
align-items: center;
`,
Strike: styled.div`
text-decoration: line-through;
font-size: 14px;
`,
Price: styled.div`
font-weight: bolder;
font-size: 18px;
`,
PriceBox: styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-end;
text-align: right;
width: 100%;
@media (${mediaWidths.mobile}) {
margin: 24px 0 0;
align-items: center;
}
`,
Discount: styled.div`
position: absolute;
top: -36px;
right: -16px;
background: ${cardColor};
border: 1px solid ${chartColors.green};
color: ${chartColors.green};
padding: 4px 8px;
border-radius: 8px;
`,
};
type TokenCardProps = {
paidCallback: (id: string) => void;
};
export const TokenCard: FC<TokenCardProps> = ({ paidCallback }) => {
const { data, loading, error } = useGetBaseInfoQuery();
const { currency, displayValues } = useConfigState();
const priceContext = usePriceState();
const format = getPrice(currency, displayValues, priceContext);
if (loading) {
return <LoadingCard noTitle={true} />;
}
if (error || !data?.getBaseInfo) {
return (
<Card>
<DarkSubTitle>Unable to get token information</DarkSubTitle>
</Card>
);
}
const { apiTokenSatPrice, apiTokenOriginalSatPrice } = data.getBaseInfo;
const monthPrice = apiTokenSatPrice * 30;
const originalMonthPrice = apiTokenOriginalSatPrice * 30;
const formatDayPrice = format({ amount: apiTokenSatPrice });
const formatPrice = format({ amount: monthPrice });
const formatOriginalPrice = format({ amount: originalMonthPrice });
const percent = Math.round(
((originalMonthPrice - monthPrice) / originalMonthPrice) * 100
);
const hasDiscount = percent > 0;
const discount = `-${percent}%`;
return (
<CardWithTitle>
<S.Title>ThunderBase Token</S.Title>
<Card>
<S.Row>
{hasDiscount && <S.Discount>{discount}</S.Discount>}
<S.IconWrapper>
<LifeBuoy size={64} color={themeColors.blue2} />
</S.IconWrapper>
<S.Text>
This token gives you access to the full ThunderBase API.
<DarkSubTitle>
Features: Historical BOS score data, more to come...
</DarkSubTitle>
</S.Text>
<S.PriceBox>
{hasDiscount && (
<S.Strike>{`${formatOriginalPrice}/month`}</S.Strike>
)}
<S.Price>{`${formatPrice}/month`}</S.Price>
<DarkSubTitle>{`${formatDayPrice}/day`}</DarkSubTitle>
</S.PriceBox>
</S.Row>
<BuyButton
paidCallback={paidCallback}
>{`Buy a 1 month token for ${formatPrice}`}</BuyButton>
</Card>
</CardWithTitle>
);
};

View file

@ -5,6 +5,8 @@
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,