diff --git a/.eslintrc.js b/.eslintrc.js
index e7d0dc81..029566dd 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -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',
diff --git a/package-lock.json b/package-lock.json
index b6a29489..3aa338c4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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": {
diff --git a/package.json b/package.json
index 5b446fb1..9763f9cc 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/pages/_app.tsx b/pages/_app.tsx
index 5e29aafa..4cd2b1a5 100644
--- a/pages/_app.tsx
+++ b/pages/_app.tsx
@@ -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) {
ThunderHub - Lightning Node Manager
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/pages/scores/[id].tsx b/pages/scores/[id].tsx
new file mode 100644
index 00000000..f5a91f80
--- /dev/null
+++ b/pages/scores/[id].tsx
@@ -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(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 && (
+ <>
+
+
+ >
+ )}
+ setLoading(false)}
+ errorCallback={handleAuthError}
+ />
+ >
+ );
+};
+
+const Wrapped = () => (
+
+
+
+);
+
+export default Wrapped;
+
+export async function getServerSideProps(context: NextPageContext) {
+ return await getProps(context);
+}
diff --git a/pages/scores/index.tsx b/pages/scores/index.tsx
new file mode 100644
index 00000000..e1771500
--- /dev/null
+++ b/pages/scores/index.tsx
@@ -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 (
+
+
+
+ );
+ }
+
+ 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: (
+
+
+
+
+
+ ),
+ }));
+
+ return (
+
+
+
+ BOS Scores
+ {`Updated: ${getFormatDate(date)}`}
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Wrapped;
+
+export async function getServerSideProps(context: NextPageContext) {
+ return await getProps(context);
+}
diff --git a/pages/token.tsx b/pages/token.tsx
new file mode 100644
index 00000000..098f5d87
--- /dev/null
+++ b/pages/token.tsx
@@ -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();
+
+ if (id) {
+ return ;
+ }
+
+ if (hasToken) {
+ return You already have a token!;
+ }
+
+ return (
+ <>
+ setId(id)} />
+
+ >
+ );
+};
+
+const Wrapped = () => (
+
+
+
+);
+
+export default Wrapped;
+
+export async function getServerSideProps(context: NextPageContext) {
+ return await getProps(context);
+}
diff --git a/server/schema/bitcoin/resolvers.ts b/server/schema/bitcoin/resolvers.ts
index 72a91f42..64b977b6 100644
--- a/server/schema/bitcoin/resolvers.ts
+++ b/server/schema/bitcoin/resolvers.ts
@@ -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 {
diff --git a/server/schema/chain/resolvers.ts b/server/schema/chain/resolvers.ts
index 4d1d3bdd..7f7be488 100644
--- a/server/schema/chain/resolvers.ts
+++ b/server/schema/chain/resolvers.ts
@@ -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(
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;
diff --git a/server/schema/channel/resolvers/query/getChannelBalance.ts b/server/schema/channel/resolvers/query/getChannelBalance.ts
index ca61b30c..dde1f3d1 100644
--- a/server/schema/channel/resolvers/query/getChannelBalance.ts
+++ b/server/schema/channel/resolvers/query/getChannelBalance.ts
@@ -11,7 +11,7 @@ interface ChannelBalanceProps {
export const getChannelBalance = async (
_: undefined,
- params: any,
+ __: undefined,
context: ContextType
) => {
await requestLimiter(context.ip, 'channelBalance');
diff --git a/server/schema/channel/resolvers/query/getPendingChannels.ts b/server/schema/channel/resolvers/query/getPendingChannels.ts
index b3456101..b250c825 100644
--- a/server/schema/channel/resolvers/query/getPendingChannels.ts
+++ b/server/schema/channel/resolvers/query/getPendingChannels.ts
@@ -26,7 +26,7 @@ interface PendingChannelProps {
export const getPendingChannels = async (
_: undefined,
- params: any,
+ __: undefined,
context: ContextType
) => {
await requestLimiter(context.ip, 'pendingChannels');
diff --git a/server/schema/context.ts b/server/schema/context.ts
index 282669c4..bebb6fce 100644
--- a/server/schema/context.ts
+++ b/server/schema/context.ts
@@ -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;
diff --git a/server/schema/github/resolvers.ts b/server/schema/github/resolvers.ts
index 5347e948..b2bd5634 100644
--- a/server/schema/github/resolvers.ts
+++ b/server/schema/github/resolvers.ts
@@ -8,7 +8,7 @@ export const githubResolvers = {
Query: {
getLatestVersion: async (
_: undefined,
- params: any,
+ __: undefined,
context: ContextType
) => {
await requestLimiter(context.ip, 'getLnPay');
diff --git a/server/schema/health/resolvers/getFeeHealth.ts b/server/schema/health/resolvers/getFeeHealth.ts
index 7686c4e3..bc093af0 100644
--- a/server/schema/health/resolvers/getFeeHealth.ts
+++ b/server/schema/health/resolvers/getFeeHealth.ts
@@ -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;
diff --git a/server/schema/health/resolvers/getVolumeHealth.ts b/server/schema/health/resolvers/getVolumeHealth.ts
index 300977b4..01f57920 100644
--- a/server/schema/health/resolvers/getVolumeHealth.ts
+++ b/server/schema/health/resolvers/getVolumeHealth.ts
@@ -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;
diff --git a/server/schema/network/resolvers.ts b/server/schema/network/resolvers.ts
index 299d13ee..3442cd2f 100644
--- a/server/schema/network/resolvers.ts
+++ b/server/schema/network/resolvers.ts
@@ -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;
diff --git a/server/schema/node/resolvers.ts b/server/schema/node/resolvers.ts
index f30b2d5d..0ccac6a6 100644
--- a/server/schema/node/resolvers.ts
+++ b/server/schema/node/resolvers.ts
@@ -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;
diff --git a/server/schema/peer/resolvers.ts b/server/schema/peer/resolvers.ts
index b4ee247d..293b078a 100644
--- a/server/schema/peer/resolvers.ts
+++ b/server/schema/peer/resolvers.ts
@@ -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;
diff --git a/server/schema/tbase/resolvers.ts b/server/schema/tbase/resolvers.ts
index 83f22f53..720e05a2 100644
--- a/server/schema/tbase/resolvers.ts
+++ b/server/schema/tbase/resolvers.ts
@@ -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 => {
- await requestLimiter(context.ip, 'getThunderPoints');
+ await requestLimiter(context.ip, 'createThunderPoints');
const [info, error] = await toWithError(
request(appUrls.tbase, createThunderPointsQuery, params)
diff --git a/server/schema/tbase/types.ts b/server/schema/tbase/types.ts
index 19fa85a7..6da2ea10 100644
--- a/server/schema/tbase/types.ts
+++ b/server/schema/tbase/types.ts
@@ -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!
+ }
`;
diff --git a/server/schema/tools/resolvers.ts b/server/schema/tools/resolvers.ts
index 5b5c3dbd..5063172e 100644
--- a/server/schema/tools/resolvers.ts
+++ b/server/schema/tools/resolvers.ts
@@ -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;
diff --git a/server/schema/types.ts b/server/schema/types.ts
index 2e71aa67..a07437d4 100644
--- a/server/schema/types.ts
+++ b/server/schema/types.ts
@@ -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!
diff --git a/server/schema/wallet/resolvers.ts b/server/schema/wallet/resolvers.ts
index e1a21f8f..df04844c 100644
--- a/server/schema/wallet/resolvers.ts
+++ b/server/schema/wallet/resolvers.ts
@@ -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;
diff --git a/server/schema/widgets/resolvers/getChannelReport.ts b/server/schema/widgets/resolvers/getChannelReport.ts
index 3a099368..399eed67 100644
--- a/server/schema/widgets/resolvers/getChannelReport.ts
+++ b/server/schema/widgets/resolvers/getChannelReport.ts
@@ -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');
diff --git a/server/tests/testMocks.ts b/server/tests/testMocks.ts
index b5c909c5..c5867e98 100644
--- a/server/tests/testMocks.ts
+++ b/server/tests/testMocks.ts
@@ -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',
};
diff --git a/server/types/apiTypes.ts b/server/types/apiTypes.ts
index fc83ee85..37bc2c06 100644
--- a/server/types/apiTypes.ts
+++ b/server/types/apiTypes.ts
@@ -17,4 +17,5 @@ export type ContextType = {
accounts: ParsedAccount[];
res: ServerResponse;
lnMarketsAuth: string | null;
+ tokenAuth: string | null;
};
diff --git a/server/utils/appConstants.ts b/server/utils/appConstants.ts
index cd7d103e..286c58d4 100644
--- a/server/utils/appConstants.ts
+++ b/server/utils/appConstants.ts
@@ -1,4 +1,5 @@
export const appConstants = {
cookieName: 'Thub-Auth',
lnMarketsAuth: 'LnMarkets-Auth',
+ tokenCookieName: 'Tbase-Auth',
};
diff --git a/src/context/BaseContext.tsx b/src/context/BaseContext.tsx
new file mode 100644
index 00000000..eea3f3e7
--- /dev/null
+++ b/src/context/BaseContext.tsx
@@ -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(undefined);
+export const DispatchContext = createContext(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 (
+
+ {children}
+
+ );
+};
+
+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 };
diff --git a/src/graphql/mutations/__generated__/createBaseToken.generated.tsx b/src/graphql/mutations/__generated__/createBaseToken.generated.tsx
new file mode 100644
index 00000000..a3b90b65
--- /dev/null
+++ b/src/graphql/mutations/__generated__/createBaseToken.generated.tsx
@@ -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
+);
+
+
+export const CreateBaseTokenDocument = gql`
+ mutation CreateBaseToken($id: String!) {
+ createBaseToken(id: $id)
+}
+ `;
+export type CreateBaseTokenMutationFn = Apollo.MutationFunction;
+
+/**
+ * __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) {
+ return Apollo.useMutation(CreateBaseTokenDocument, baseOptions);
+ }
+export type CreateBaseTokenMutationHookResult = ReturnType;
+export type CreateBaseTokenMutationResult = Apollo.MutationResult;
+export type CreateBaseTokenMutationOptions = Apollo.BaseMutationOptions;
\ No newline at end of file
diff --git a/src/graphql/mutations/__generated__/createBaseTokenInvoice.generated.tsx b/src/graphql/mutations/__generated__/createBaseTokenInvoice.generated.tsx
new file mode 100644
index 00000000..f103e2eb
--- /dev/null
+++ b/src/graphql/mutations/__generated__/createBaseTokenInvoice.generated.tsx
@@ -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
+ )> }
+);
+
+
+export const CreateBaseTokenInvoiceDocument = gql`
+ mutation CreateBaseTokenInvoice {
+ createBaseTokenInvoice {
+ request
+ id
+ }
+}
+ `;
+export type CreateBaseTokenInvoiceMutationFn = Apollo.MutationFunction;
+
+/**
+ * __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) {
+ return Apollo.useMutation(CreateBaseTokenInvoiceDocument, baseOptions);
+ }
+export type CreateBaseTokenInvoiceMutationHookResult = ReturnType;
+export type CreateBaseTokenInvoiceMutationResult = Apollo.MutationResult;
+export type CreateBaseTokenInvoiceMutationOptions = Apollo.BaseMutationOptions;
\ No newline at end of file
diff --git a/src/graphql/mutations/__generated__/deleteBaseToken.generated.tsx b/src/graphql/mutations/__generated__/deleteBaseToken.generated.tsx
new file mode 100644
index 00000000..639fd5f4
--- /dev/null
+++ b/src/graphql/mutations/__generated__/deleteBaseToken.generated.tsx
@@ -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
+);
+
+
+export const DeleteBaseTokenDocument = gql`
+ mutation DeleteBaseToken {
+ deleteBaseToken
+}
+ `;
+export type DeleteBaseTokenMutationFn = Apollo.MutationFunction;
+
+/**
+ * __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) {
+ return Apollo.useMutation(DeleteBaseTokenDocument, baseOptions);
+ }
+export type DeleteBaseTokenMutationHookResult = ReturnType;
+export type DeleteBaseTokenMutationResult = Apollo.MutationResult;
+export type DeleteBaseTokenMutationOptions = Apollo.BaseMutationOptions;
\ No newline at end of file
diff --git a/src/graphql/mutations/createBaseToken.ts b/src/graphql/mutations/createBaseToken.ts
new file mode 100644
index 00000000..0ccf3559
--- /dev/null
+++ b/src/graphql/mutations/createBaseToken.ts
@@ -0,0 +1,7 @@
+import { gql } from '@apollo/client';
+
+export const CREATE_BASE_TOKEN = gql`
+ mutation CreateBaseToken($id: String!) {
+ createBaseToken(id: $id)
+ }
+`;
diff --git a/src/graphql/mutations/createBaseTokenInvoice.ts b/src/graphql/mutations/createBaseTokenInvoice.ts
new file mode 100644
index 00000000..6407de55
--- /dev/null
+++ b/src/graphql/mutations/createBaseTokenInvoice.ts
@@ -0,0 +1,10 @@
+import { gql } from '@apollo/client';
+
+export const CREATE_BASE_TOKEN_INVOICE = gql`
+ mutation CreateBaseTokenInvoice {
+ createBaseTokenInvoice {
+ request
+ id
+ }
+ }
+`;
diff --git a/src/graphql/mutations/deleteBaseToken.ts b/src/graphql/mutations/deleteBaseToken.ts
new file mode 100644
index 00000000..6d07b3e9
--- /dev/null
+++ b/src/graphql/mutations/deleteBaseToken.ts
@@ -0,0 +1,7 @@
+import { gql } from '@apollo/client';
+
+export const DELETE_BASE_TOKEN = gql`
+ mutation DeleteBaseToken {
+ deleteBaseToken
+ }
+`;
diff --git a/src/graphql/queries/__generated__/getBaseInfo.generated.tsx b/src/graphql/queries/__generated__/getBaseInfo.generated.tsx
new file mode 100644
index 00000000..59b92100
--- /dev/null
+++ b/src/graphql/queries/__generated__/getBaseInfo.generated.tsx
@@ -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
+ ) }
+);
+
+
+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) {
+ return Apollo.useQuery(GetBaseInfoDocument, baseOptions);
+ }
+export function useGetBaseInfoLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) {
+ return Apollo.useLazyQuery(GetBaseInfoDocument, baseOptions);
+ }
+export type GetBaseInfoQueryHookResult = ReturnType;
+export type GetBaseInfoLazyQueryHookResult = ReturnType;
+export type GetBaseInfoQueryResult = Apollo.QueryResult;
\ No newline at end of file
diff --git a/src/graphql/queries/__generated__/getBosNodeScores.generated.tsx b/src/graphql/queries/__generated__/getBosNodeScores.generated.tsx
new file mode 100644
index 00000000..f460d060
--- /dev/null
+++ b/src/graphql/queries/__generated__/getBosNodeScores.generated.tsx
@@ -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
+ )>> }
+);
+
+
+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) {
+ return Apollo.useQuery(GetBosNodeScoresDocument, baseOptions);
+ }
+export function useGetBosNodeScoresLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) {
+ return Apollo.useLazyQuery(GetBosNodeScoresDocument, baseOptions);
+ }
+export type GetBosNodeScoresQueryHookResult = ReturnType;
+export type GetBosNodeScoresLazyQueryHookResult = ReturnType;
+export type GetBosNodeScoresQueryResult = Apollo.QueryResult;
\ No newline at end of file
diff --git a/src/graphql/queries/__generated__/getBosScores.generated.tsx b/src/graphql/queries/__generated__/getBosScores.generated.tsx
new file mode 100644
index 00000000..2574ed68
--- /dev/null
+++ b/src/graphql/queries/__generated__/getBosScores.generated.tsx
@@ -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
+ & { scores: Array<(
+ { __typename?: 'BosScore' }
+ & Pick
+ )> }
+ ) }
+);
+
+
+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) {
+ return Apollo.useQuery(GetBosScoresDocument, baseOptions);
+ }
+export function useGetBosScoresLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) {
+ return Apollo.useLazyQuery(GetBosScoresDocument, baseOptions);
+ }
+export type GetBosScoresQueryHookResult = ReturnType;
+export type GetBosScoresLazyQueryHookResult = ReturnType;
+export type GetBosScoresQueryResult = Apollo.QueryResult;
\ No newline at end of file
diff --git a/src/graphql/queries/getBaseInfo.ts b/src/graphql/queries/getBaseInfo.ts
new file mode 100644
index 00000000..2fc21002
--- /dev/null
+++ b/src/graphql/queries/getBaseInfo.ts
@@ -0,0 +1,11 @@
+import { gql } from '@apollo/client';
+
+export const GET_BASE_INFO = gql`
+ query GetBaseInfo {
+ getBaseInfo {
+ lastBosUpdate
+ apiTokenSatPrice
+ apiTokenOriginalSatPrice
+ }
+ }
+`;
diff --git a/src/graphql/queries/getBosNodeScores.ts b/src/graphql/queries/getBosNodeScores.ts
new file mode 100644
index 00000000..b65be571
--- /dev/null
+++ b/src/graphql/queries/getBosNodeScores.ts
@@ -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
+ }
+ }
+`;
diff --git a/src/graphql/queries/getBosScores.ts b/src/graphql/queries/getBosScores.ts
new file mode 100644
index 00000000..05818001
--- /dev/null
+++ b/src/graphql/queries/getBosScores.ts
@@ -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
+ }
+ }
+ }
+`;
diff --git a/src/graphql/types.ts b/src/graphql/types.ts
index a6754be7..f3558dd2 100644
--- a/src/graphql/types.ts
+++ b/src/graphql/types.ts
@@ -1,6 +1,8 @@
/* eslint-disable */
export type Maybe = T | null;
export type Exact = { [K in keyof T]: T[K] };
+export type MakeOptional = Omit & { [SubKey in K]?: Maybe };
+export type MakeMaybe = Omit & { [SubKey in K]: Maybe };
/** 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>;
+ getBosScores: BosScoreResponse;
+ getBaseInfo: BaseInfo;
getBoltzSwapStatus: Array>;
getBoltzInfo: BoltzInfoType;
getLnMarketsStatus: Scalars['String'];
@@ -89,6 +94,11 @@ export type Query = {
};
+export type QueryGetBosNodeScoresArgs = {
+ publicKey: Scalars['String'];
+};
+
+
export type QueryGetBoltzSwapStatusArgs = {
ids: Array>;
};
@@ -211,6 +221,9 @@ export type Mutation = {
lnUrlPay: PaySuccess;
lnUrlWithdraw: Scalars['String'];
fetchLnUrl?: Maybe;
+ createBaseTokenInvoice?: Maybe;
+ createBaseToken: Scalars['Boolean'];
+ deleteBaseToken: Scalars['Boolean'];
createBaseInvoice?: Maybe;
createThunderPoints: Scalars['Boolean'];
closeChannel?: Maybe;
@@ -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;
+};
+
+export type BaseInfo = {
+ __typename?: 'BaseInfo';
+ lastBosUpdate: Scalars['String'];
+ apiTokenSatPrice: Scalars['Int'];
+ apiTokenOriginalSatPrice: Scalars['Int'];
+};
+
export type WithdrawRequest = {
__typename?: 'WithdrawRequest';
callback?: Maybe;
diff --git a/src/hooks/UseBaseConnect.tsx b/src/hooks/UseBaseConnect.tsx
index 78025fd0..30a00635 100644
--- a/src/hooks/UseBaseConnect.tsx
+++ b/src/hooks/UseBaseConnect.tsx
@@ -4,7 +4,10 @@ import { useGetBaseCanConnectQuery } from 'src/graphql/queries/__generated__/get
export const useBaseConnect = () => {
const [canConnect, setCanConnect] = useState(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;
diff --git a/src/hooks/UseNodeInfo.tsx b/src/hooks/UseNodeInfo.tsx
index dde9a453..bec8f189 100644
--- a/src/hooks/UseNodeInfo.tsx
+++ b/src/hooks/UseNodeInfo.tsx
@@ -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]);
diff --git a/src/layouts/navigation/Navigation.tsx b/src/layouts/navigation/Navigation.tsx
index 861ecb39..a3a108c7 100644
--- a/src/layouts/navigation/Navigation.tsx
+++ b/src/layouts/navigation/Navigation.tsx
@@ -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)}
);
@@ -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)}
diff --git a/src/utils/ssr.ts b/src/utils/ssr.ts
index 0856e7df..7891719d 100644
--- a/src/utils/ssr.ts
+++ b/src/utils/ssr.ts
@@ -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,
},
};
};
diff --git a/src/views/scores/AreaGraph.tsx b/src/views/scores/AreaGraph.tsx
new file mode 100644
index 00000000..5ec8df25
--- /dev/null
+++ b/src/views/scores/AreaGraph.tsx
@@ -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(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(
+ ({
+ 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) => {
+ 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
+ | React.MouseEvent
+ ) => {
+ 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 (
+
+
+ {tooltipData && (
+
+
+ {`${tooltipText}${getValue(tooltipData)}`}
+
+
+ {format(getDate(tooltipData), 'LLL d, yyyy')}
+
+
+ )}
+
+ );
+ }
+);
diff --git a/src/views/scores/NodeGraph.tsx b/src/views/scores/NodeGraph.tsx
new file mode 100644
index 00000000..0b9b9491
--- /dev/null
+++ b/src/views/scores/NodeGraph.tsx
@@ -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(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 ;
+ }
+
+ const scores = data?.getBosNodeScores || [];
+ const final = scores
+ .map(s => ({
+ date: s?.updated || '',
+ value: (isPos ? s?.position : s?.score) || 0,
+ }))
+ .reverse();
+
+ return (
+
+
+ {parent => (
+
+ )}
+
+
+ );
+};
diff --git a/src/views/scores/NodeInfo.tsx b/src/views/scores/NodeInfo.tsx
new file mode 100644
index 00000000..3300507d
--- /dev/null
+++ b/src/views/scores/NodeInfo.tsx
@@ -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 ;
+ }
+
+ if (loading) {
+ return ;
+ }
+
+ if (!data?.getNode.node || data?.getNode?.node?.alias === 'Node not found') {
+ return (
+
+ Node Info
+
+ Node not found
+
+
+ );
+ }
+
+ const { alias, channel_count, capacity, updated_at } = data.getNode.node;
+
+ return (
+
+ Node Info
+
+ {alias}
+ {renderLine('Channel Count', channel_count)}
+ {renderLine('Capacity', )}
+ {renderLine('Last Update', `${getDateDif(updated_at)} ago`)}
+ {renderLine('Last Update Date', getFormatDate(updated_at))}
+
+
+ );
+};
diff --git a/src/views/scores/NodeScores.tsx b/src/views/scores/NodeScores.tsx
new file mode 100644
index 00000000..4d31a2da
--- /dev/null
+++ b/src/views/scores/NodeScores.tsx
@@ -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 = ({
+ 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 ;
+ }
+
+ 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 (
+
+ Historical Scores
+
+
+
+
+ );
+};
diff --git a/src/views/scores/ScoreCard.tsx b/src/views/scores/ScoreCard.tsx
new file mode 100644
index 00000000..4e1c4e64
--- /dev/null
+++ b/src/views/scores/ScoreCard.tsx
@@ -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 = ({ score }) => {
+ const { push } = useRouter();
+
+ if (!score) {
+ return (
+
+
+ This node is not in the BOS list. Read more about these scores here.
+
+
+ );
+ }
+
+ const handleClick = () => {
+ if (score.public_key) {
+ push(appendBasePath(`/scores/${score.public_key}`));
+ }
+ };
+
+ return (
+
+ {score.alias}
+ {renderLine('Position', score.position)}
+ {renderLine('Score', score.score)}
+ {renderLine('Public Key', getNodeLink(score.public_key))}
+
+ );
+};
diff --git a/src/views/token/BuyButton.tsx b/src/views/token/BuyButton.tsx
new file mode 100644
index 00000000..9f1ae0cd
--- /dev/null
+++ b/src/views/token/BuyButton.tsx
@@ -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(false);
+ const [invoice, invoiceSet] = useState('');
+
+ 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 (
+ <>
+
+ {children}
+
+
+
+
+ >
+ );
+};
diff --git a/src/views/token/PaidCard.tsx b/src/views/token/PaidCard.tsx
new file mode 100644
index 00000000..4a57b302
--- /dev/null
+++ b/src/views/token/PaidCard.tsx
@@ -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 ;
+ }
+
+ return (
+
+ Thank you for the purchase!
+
+
+
+ This is your payment backup id you can use to recover the token in
+ case it gets deleted.
+
+
+ You can also get it from the transaction in the Transaction View.
+
+
+
+ {renderLine('Backup Id', id)}
+
+ );
+};
diff --git a/src/views/token/RecoverToken.tsx b/src/views/token/RecoverToken.tsx
new file mode 100644
index 00000000..5f7abf05
--- /dev/null
+++ b/src/views/token/RecoverToken.tsx
@@ -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('');
+
+ 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 (
+
+ Recover a paid token
+
+
+ getToken({ variables: { id } })}
+ >
+ Recover Token
+
+
+
+ );
+};
diff --git a/src/views/token/TokenCard.tsx b/src/views/token/TokenCard.tsx
new file mode 100644
index 00000000..ba136971
--- /dev/null
+++ b/src/views/token/TokenCard.tsx
@@ -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 = ({ paidCallback }) => {
+ const { data, loading, error } = useGetBaseInfoQuery();
+
+ const { currency, displayValues } = useConfigState();
+ const priceContext = usePriceState();
+ const format = getPrice(currency, displayValues, priceContext);
+
+ if (loading) {
+ return ;
+ }
+
+ if (error || !data?.getBaseInfo) {
+ return (
+
+ Unable to get token information
+
+ );
+ }
+
+ 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 (
+
+ ThunderBase Token
+
+
+ {hasDiscount && {discount}}
+
+
+
+
+ This token gives you access to the full ThunderBase API.
+
+ Features: Historical BOS score data, more to come...
+
+
+
+ {hasDiscount && (
+ {`${formatOriginalPrice}/month`}
+ )}
+ {`${formatPrice}/month`}
+ {`${formatDayPrice}/day`}
+
+
+ {`Buy a 1 month token for ${formatPrice}`}
+
+
+ );
+};
diff --git a/tsconfig.json b/tsconfig.json
index 13b8e8de..1f547a8d 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -5,6 +5,8 @@
"allowJs": true,
"skipLibCheck": true,
"strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,