From 7fa7cfc3c27fd25e749ab885d99d7bd9786e8854 Mon Sep 17 00:00:00 2001 From: Anthony Potdevin Date: Mon, 14 Dec 2020 14:16:31 +0100 Subject: [PATCH] =?UTF-8?q?chore:=20=F0=9F=94=A7=20add=20score=20query?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.js | 3 +- package-lock.json | 110 ++++++++-- package.json | 5 + pages/_app.tsx | 13 +- pages/scores/[id].tsx | 61 ++++++ pages/scores/index.tsx | 104 +++++++++ pages/token.tsx | 41 ++++ server/schema/bitcoin/resolvers.ts | 6 +- server/schema/chain/resolvers.ts | 12 +- .../resolvers/query/getChannelBalance.ts | 2 +- .../resolvers/query/getPendingChannels.ts | 2 +- server/schema/context.ts | 2 + server/schema/github/resolvers.ts | 2 +- .../schema/health/resolvers/getFeeHealth.ts | 2 +- .../health/resolvers/getVolumeHealth.ts | 2 +- server/schema/network/resolvers.ts | 6 +- server/schema/node/resolvers.ts | 2 +- server/schema/peer/resolvers.ts | 2 +- server/schema/tbase/resolvers.ts | 181 +++++++++++++++- server/schema/tbase/types.ts | 19 ++ server/schema/tools/resolvers.ts | 4 +- server/schema/types.ts | 6 + server/schema/wallet/resolvers.ts | 6 +- .../widgets/resolvers/getChannelReport.ts | 2 +- server/tests/testMocks.ts | 3 + server/types/apiTypes.ts | 1 + server/utils/appConstants.ts | 1 + src/context/BaseContext.tsx | 57 +++++ .../createBaseToken.generated.tsx | 46 ++++ .../createBaseTokenInvoice.generated.tsx | 49 +++++ .../deleteBaseToken.generated.tsx | 43 ++++ src/graphql/mutations/createBaseToken.ts | 7 + .../mutations/createBaseTokenInvoice.ts | 10 + src/graphql/mutations/deleteBaseToken.ts | 7 + .../__generated__/getBaseInfo.generated.tsx | 51 +++++ .../getBosNodeScores.generated.tsx | 56 +++++ .../__generated__/getBosScores.generated.tsx | 60 ++++++ src/graphql/queries/getBaseInfo.ts | 11 + src/graphql/queries/getBosNodeScores.ts | 13 ++ src/graphql/queries/getBosScores.ts | 16 ++ src/graphql/types.ts | 40 ++++ src/hooks/UseBaseConnect.tsx | 5 +- src/hooks/UseNodeInfo.tsx | 4 + src/layouts/navigation/Navigation.tsx | 4 + src/utils/ssr.ts | 18 +- src/views/scores/AreaGraph.tsx | 202 ++++++++++++++++++ src/views/scores/NodeGraph.tsx | 70 ++++++ src/views/scores/NodeInfo.tsx | 68 ++++++ src/views/scores/NodeScores.tsx | 66 ++++++ src/views/scores/ScoreCard.tsx | 91 ++++++++ src/views/token/BuyButton.tsx | 55 +++++ src/views/token/PaidCard.tsx | 63 ++++++ src/views/token/RecoverToken.tsx | 50 +++++ src/views/token/TokenCard.tsx | 151 +++++++++++++ tsconfig.json | 2 + 55 files changed, 1863 insertions(+), 52 deletions(-) create mode 100644 pages/scores/[id].tsx create mode 100644 pages/scores/index.tsx create mode 100644 pages/token.tsx create mode 100644 src/context/BaseContext.tsx create mode 100644 src/graphql/mutations/__generated__/createBaseToken.generated.tsx create mode 100644 src/graphql/mutations/__generated__/createBaseTokenInvoice.generated.tsx create mode 100644 src/graphql/mutations/__generated__/deleteBaseToken.generated.tsx create mode 100644 src/graphql/mutations/createBaseToken.ts create mode 100644 src/graphql/mutations/createBaseTokenInvoice.ts create mode 100644 src/graphql/mutations/deleteBaseToken.ts create mode 100644 src/graphql/queries/__generated__/getBaseInfo.generated.tsx create mode 100644 src/graphql/queries/__generated__/getBosNodeScores.generated.tsx create mode 100644 src/graphql/queries/__generated__/getBosScores.generated.tsx create mode 100644 src/graphql/queries/getBaseInfo.ts create mode 100644 src/graphql/queries/getBosNodeScores.ts create mode 100644 src/graphql/queries/getBosScores.ts create mode 100644 src/views/scores/AreaGraph.tsx create mode 100644 src/views/scores/NodeGraph.tsx create mode 100644 src/views/scores/NodeInfo.tsx create mode 100644 src/views/scores/NodeScores.tsx create mode 100644 src/views/scores/ScoreCard.tsx create mode 100644 src/views/token/BuyButton.tsx create mode 100644 src/views/token/PaidCard.tsx create mode 100644 src/views/token/RecoverToken.tsx create mode 100644 src/views/token/TokenCard.tsx 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 ( +
+ + + data={data} + x={d => dateScale(getDate(d)) ?? 0} + y={d => valueScale(getValue(d)) ?? 0} + yScale={valueScale} + strokeWidth={1} + stroke={areaColor} + fill={areaColor} + curve={curveMonotoneX} + /> + hideTooltip()} + onClick={() => { + clickCallback && clickCallback(); + }} + /> + {tooltipData && ( + + + + + + )} + + {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,