From 009195c86f8e377a5679f2d4c875f230086c1bc7 Mon Sep 17 00:00:00 2001 From: Anthony Potdevin <31413433+apotdevin@users.noreply.github.com> Date: Fri, 5 Jun 2020 18:50:10 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20=E2=9C=A8=20stats=20view=20(#54)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: ✨ channel stats * chore: 🔧 add node resolver * chore: 🔧 add monitored time * chore: 🔧 and queries to front * fix: 🐛 floats to ints * chore: 🔧 add progress bars * chore: 🔧 add channel resolver * chore: 🔧 refactor forwards frontend * refactor: ♻️ channel resolvers * chore: 🔧 refactor channel queries * refactor: ♻️ peer resolver * refactor: ♻️ peer query * fix: 🐛 small changes * fix: 🐛 typo * chore: 🔧 stats view wip * chore: 🔧 add update script * chore: 🔧 improve ui * chore: 🔧 move buttons * fix: 🐛 home path * chore: 🔧 add public key to node resolver * refactor: ♻️ resume resolver * chore: 🔧 remove test account * chore: 🔧 change logger for lnpay * feat: ✨ github version Co-authored-by: apotdevin --- README.md | 52 +- codegen.yml | 3 + config/client.tsx | 12 +- package-lock.json | 41 + package.json | 5 +- pages/_app.tsx | 1 + pages/forwards.tsx | 2 +- pages/home.tsx | 2 + pages/settings.tsx | 2 +- pages/stats.tsx | 40 + pages/transactions.tsx | 13 +- scripts/updateToLatest.sh | 26 + server/helpers/helpers.ts | 7 + server/schema/channel/resolvers.ts | 43 + .../channel/resolvers/query/getChannels.ts | 85 +- .../resolvers/query/getClosedChannels.ts | 39 +- .../resolvers/query/getPendingChannels.ts | 40 +- server/schema/channel/types.ts | 33 +- server/schema/github/resolvers.ts | 28 + server/schema/health/helpers.ts | 72 ++ server/schema/health/resolvers.ts | 11 + .../schema/health/resolvers/getFeeHealth.ts | 132 +++ .../schema/health/resolvers/getTimeHealth.ts | 51 + .../health/resolvers/getVolumeHealth.ts | 68 ++ server/schema/health/types.ts | 53 ++ server/schema/index.ts | 8 +- server/schema/invoice/types.ts | 2 +- server/schema/lnpay/resolvers.ts | 2 +- server/schema/node/resolvers.ts | 34 +- server/schema/node/types.ts | 16 + server/schema/peer/resolvers.ts | 42 +- server/schema/peer/types.ts | 2 +- server/schema/transactions/resolvers.ts | 180 ++-- server/schema/transactions/types.ts | 53 +- server/schema/types.ts | 17 +- server/utils/appUrls.ts | 4 +- src/components/generic/Styled.tsx | 13 + src/components/generic/helpers.tsx | 30 +- src/components/gridWrapper/GridWrapper.tsx | 24 +- src/components/link/Link.tsx | 9 +- src/components/version/Version.tsx | 49 + src/context/AccountContext.tsx | 2 +- src/graphql/fragmentTypes.json | 18 + .../__generated__/createInvoice.generated.tsx | 2 +- .../__generated__/getChannels.generated.tsx | 73 +- .../getClosedChannels.generated.tsx | 28 +- .../__generated__/getFeeHealth.generated.tsx | 146 +++ .../__generated__/getForwards.generated.tsx | 79 +- .../getLatestVersion.generated.tsx | 65 ++ .../__generated__/getNode.generated.tsx | 4 +- .../__generated__/getPeers.generated.tsx | 28 +- .../getPendingChannels.generated.tsx | 28 +- .../__generated__/getResume.generated.tsx | 130 ++- .../__generated__/getTimeHealth.generated.tsx | 114 +++ .../getVolumeHealth.generated.tsx | 104 ++ src/graphql/queries/getChannels.ts | 28 +- src/graphql/queries/getClosedChannels.ts | 12 +- src/graphql/queries/getFeeHealth.ts | 35 + src/graphql/queries/getForwards.ts | 28 +- src/graphql/queries/getLatestVersion.ts | 7 + src/graphql/queries/getPeers.ts | 12 +- src/graphql/queries/getPendingChannels.ts | 12 +- src/graphql/queries/getResume.ts | 52 +- src/graphql/queries/getTimeHealth.ts | 22 + src/graphql/queries/getVolumeHealth.ts | 20 + src/graphql/types.ts | 899 ++++++++++-------- src/layouts/header/Header.styled.ts | 36 +- src/layouts/header/Header.tsx | 40 +- src/layouts/navigation/Navigation.tsx | 9 +- src/views/channels/channels/ChannelCard.tsx | 2 +- src/views/channels/channels/Channels.tsx | 14 +- .../channels/closedChannels/ClosedCard.tsx | 2 +- .../channels/pendingChannels/PendingCard.tsx | 9 +- src/views/forwards/ForwardsCard.tsx | 17 +- src/views/peers/PeersCard.tsx | 12 +- src/views/stats/FeeStats.tsx | 139 +++ src/views/stats/StatResume.tsx | 85 ++ src/views/stats/TimeStats.tsx | 103 ++ src/views/stats/VolumeStats.tsx | 100 ++ src/views/stats/Wrapper.tsx | 25 + src/views/stats/context/index.tsx | 66 ++ src/views/stats/helpers.tsx | 129 +++ src/views/stats/styles.tsx | 48 + src/views/transactions/InvoiceCard.tsx | 19 +- src/views/transactions/PaymentsCards.tsx | 22 +- 85 files changed, 3320 insertions(+), 851 deletions(-) create mode 100644 pages/stats.tsx create mode 100644 scripts/updateToLatest.sh create mode 100644 server/schema/github/resolvers.ts create mode 100644 server/schema/health/helpers.ts create mode 100644 server/schema/health/resolvers.ts create mode 100644 server/schema/health/resolvers/getFeeHealth.ts create mode 100644 server/schema/health/resolvers/getTimeHealth.ts create mode 100644 server/schema/health/resolvers/getVolumeHealth.ts create mode 100644 server/schema/health/types.ts create mode 100644 src/components/version/Version.tsx create mode 100644 src/graphql/fragmentTypes.json create mode 100644 src/graphql/queries/__generated__/getFeeHealth.generated.tsx create mode 100644 src/graphql/queries/__generated__/getLatestVersion.generated.tsx create mode 100644 src/graphql/queries/__generated__/getTimeHealth.generated.tsx create mode 100644 src/graphql/queries/__generated__/getVolumeHealth.generated.tsx create mode 100644 src/graphql/queries/getFeeHealth.ts create mode 100644 src/graphql/queries/getLatestVersion.ts create mode 100644 src/graphql/queries/getTimeHealth.ts create mode 100644 src/graphql/queries/getVolumeHealth.ts create mode 100644 src/views/stats/FeeStats.tsx create mode 100644 src/views/stats/StatResume.tsx create mode 100644 src/views/stats/TimeStats.tsx create mode 100644 src/views/stats/VolumeStats.tsx create mode 100644 src/views/stats/Wrapper.tsx create mode 100644 src/views/stats/context/index.tsx create mode 100644 src/views/stats/helpers.tsx create mode 100644 src/views/stats/styles.tsx diff --git a/README.md b/README.md index 9c06a1ba..9ebc495b 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,14 @@ - [Development](#development) - [Docker deployment](#docker) +--- + ## Introduction ThunderHub is an **open-source** LND node manager where you can manage and monitor your node on any device or browser. It allows you to take control of the lightning network with a simple and intuitive UX and the most up-to-date tech stack. +--- + ### Integrations **BTCPay Server** @@ -25,6 +29,8 @@ ThunderHub is currently integrated into BTCPay for easier deployment. If you alr **Raspiblitz** For Raspiblitz users you can also get ThunderHub running by following this [gist](https://gist.github.com/openoms/8ba963915c786ce01892f2c9fa2707bc) +--- + ### Tech Stack This repository consists of a **NextJS** server that handles both the backend **Graphql Server** and the frontend **React App**. ThunderHub connects to your Lightning Network node by using the gRPC ports. @@ -38,6 +44,8 @@ This repository consists of a **NextJS** server that handles both the backend ** - GraphQL - Ln-Service +--- + ## Features ### Monitoring @@ -89,6 +97,8 @@ This repository consists of a **NextJS** server that handles both the backend ** - Loop In and Out to provide liquidity or remove it from your channels. - Storefront interface +--- + ## **Requirements** - Yarn/npm installed @@ -109,6 +119,8 @@ npm run dev -> npm run dev:compatible **HodlHodl integration will not work with older versions of Node!** +--- + ## Config You can define some environment variables that ThunderHub can start with. To do this create a `.env` file in the root directory with the following parameters: @@ -190,6 +202,8 @@ location /thub/ { } ``` +--- + ## Installation To run ThunderHub you first need to clone this repository. @@ -224,26 +238,56 @@ yarn start -p 4000 npm start -- -p 4000 ``` +--- + ## Updating -To update ThunderHub to the latest version follow these commands. +There are multiple ways to update ThunderHub to it's latest version. _Commands have to be called inside the thunderhub repository folder._ -```js +**1. Script Shortcut** + +```sh +// Yarn +yarn update + +// NPM +npm run update +``` + +**2. Script** + +```sh +sh ./scripts/updateToLatest.sh +``` + +**3. Step by Step** + +```sh // Yarn git pull yarn yarn build -yarn start // NPM git pull npm install npm run build +``` + +**Then you can start your server:** + +```sh +// Yarn +yarn start + +// NPM npm run start ``` +--- + ## Development If you want to develop on ThunderHub and want hot reloading when you do changes, use the following commands: @@ -256,6 +300,8 @@ yarn dev npm run dev ``` +--- + ## Docker ThunderHub also provides docker images for easier deployment. [Docker Hub](https://hub.docker.com/repository/docker/apotdevin/thunderhub) diff --git a/codegen.yml b/codegen.yml index 3bca8c79..80367995 100644 --- a/codegen.yml +++ b/codegen.yml @@ -2,6 +2,9 @@ overwrite: true schema: 'http://localhost:3000/api/v1' documents: 'src/graphql/**/*.ts' generates: + src/graphql/fragmentTypes.json: + plugins: + - fragment-matcher src/graphql/types.ts: - typescript src/graphql/: diff --git a/config/client.tsx b/config/client.tsx index 6ec4c862..4ddb19c9 100644 --- a/config/client.tsx +++ b/config/client.tsx @@ -3,8 +3,16 @@ import * as React from 'react'; import Head from 'next/head'; import { ApolloProvider } from '@apollo/react-hooks'; import { ApolloClient } from 'apollo-client'; -import { InMemoryCache } from 'apollo-cache-inmemory'; +import { + InMemoryCache, + IntrospectionFragmentMatcher, +} from 'apollo-cache-inmemory'; import getConfig from 'next/config'; +import introspectionQueryResultData from 'src/graphql/fragmentTypes.json'; + +const fragmentMatcher = new IntrospectionFragmentMatcher({ + introspectionQueryResultData, +}); let globalApolloClient = null; @@ -32,7 +40,7 @@ function createIsomorphLink(ctx) { */ function createApolloClient(ctx = {}, initialState = {}) { const ssrMode = typeof window === 'undefined'; - const cache = new InMemoryCache().restore(initialState); + const cache = new InMemoryCache({ fragmentMatcher }).restore(initialState); // Check out https://github.com/zeit/next.js/pull/4611 if you want to use the AWSAppSyncClient return new ApolloClient({ diff --git a/package-lock.json b/package-lock.json index 53f4a05f..813ab093 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2105,6 +2105,42 @@ } } }, + "@graphql-codegen/fragment-matcher": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@graphql-codegen/fragment-matcher/-/fragment-matcher-1.15.1.tgz", + "integrity": "sha512-30oLLPRYLuAo+OvJLPBsBEYEswAaXddOGDYL4QxGQsYnml0IhQMj//24rnLEUmYEV6zy2BAXCPK5whmV/DOnNw==", + "dev": true, + "requires": { + "@graphql-codegen/plugin-helpers": "1.15.1", + "tslib": "~2.0.0" + }, + "dependencies": { + "@graphql-codegen/plugin-helpers": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-1.15.1.tgz", + "integrity": "sha512-DnLD+s4ng+rqbqrcHtV0/jtn/bYSUTqL3tpqPDeIhsqmdDSAtOtelVCeTtPHAJGOO7RI6BQB6rXm/ZgaCObIAg==", + "dev": true, + "requires": { + "@graphql-tools/utils": "^6.0.0", + "camel-case": "4.1.1", + "common-tags": "1.8.0", + "constant-case": "3.0.3", + "import-from": "3.0.0", + "lower-case": "2.0.1", + "param-case": "3.0.3", + "pascal-case": "3.1.1", + "tslib": "~2.0.0", + "upper-case": "2.0.1" + } + }, + "tslib": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz", + "integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g==", + "dev": true + } + } + }, "@graphql-codegen/introspection": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/@graphql-codegen/introspection/-/introspection-1.15.0.tgz", @@ -20094,6 +20130,11 @@ "prop-types": "^15.6.2" } }, + "react-circular-progressbar": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/react-circular-progressbar/-/react-circular-progressbar-2.0.3.tgz", + "integrity": "sha512-YKN+xAShXA3gYihevbQZbavfiJxo83Dt1cUxqg/cltj4VVsRQpDr7Fg1mvjDG3x1KHGtd9NmYKvJ2mMrPwbKyw==" + }, "react-copy-to-clipboard": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.2.tgz", diff --git a/package.json b/package.json index 30e5e1a6..f52b9abe 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,8 @@ "build:64": "docker build -f arm64v8.Dockerfile -t apotdevin/thunderhub:test-arm64v8 .", "build:manifest": "docker manifest create apotdevin/thunderhub:test apotdevin/thunderhub:test-amd64 apotdevin/thunderhub:test-arm32v7 apotdevin/thunderhub:test-arm64v8", "upgrade-latest": "npx npm-check -u", - "tsc": "tsc" + "tsc": "tsc", + "update": "sh ./scripts/updateToLatest.sh" }, "keywords": [], "author": "", @@ -63,6 +64,7 @@ "numeral": "^2.0.6", "qrcode.react": "^1.0.0", "react": "^16.13.1", + "react-circular-progressbar": "^2.0.3", "react-copy-to-clipboard": "^5.0.2", "react-dom": "^16.13.1", "react-feather": "^2.0.8", @@ -86,6 +88,7 @@ "@commitlint/cli": "^8.3.5", "@commitlint/config-conventional": "^8.3.4", "@graphql-codegen/cli": "^1.15.0", + "@graphql-codegen/fragment-matcher": "^1.15.1", "@graphql-codegen/introspection": "^1.15.0", "@graphql-codegen/near-operation-file-preset": "^1.15.0", "@graphql-codegen/typescript": "^1.15.0", diff --git a/pages/_app.tsx b/pages/_app.tsx index c1bfaa5a..20399dda 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -12,6 +12,7 @@ import { Footer } from '../src/layouts/footer/Footer'; import 'react-toastify/dist/ReactToastify.css'; import { PageWrapper, HeaderBodyWrapper } from '../src/layouts/Layout.styled'; import { parseCookies } from '../src/utils/cookies'; +import 'react-circular-progressbar/dist/styles.css'; toast.configure({ draggable: false, pauseOnFocusLoss: false }); diff --git a/pages/forwards.tsx b/pages/forwards.tsx index c4412c86..567046b6 100644 --- a/pages/forwards.tsx +++ b/pages/forwards.tsx @@ -65,7 +65,7 @@ const ForwardsView = () => { {data.getForwards.forwards.length <= 0 && renderNoForwards()} - {data.getForwards.forwards.map((forward: any, index: number) => ( + {data.getForwards.forwards.map((forward, index: number) => ( { return ( <> + diff --git a/pages/settings.tsx b/pages/settings.tsx index 7e2d4d1b..6d406de4 100644 --- a/pages/settings.tsx +++ b/pages/settings.tsx @@ -35,7 +35,7 @@ const SettingsView = () => { }; const Wrapped = () => ( - + ); diff --git a/pages/stats.tsx b/pages/stats.tsx new file mode 100644 index 00000000..3737a69c --- /dev/null +++ b/pages/stats.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import styled from 'styled-components'; +import { GridWrapper } from 'src/components/gridWrapper/GridWrapper'; +import { withApollo } from 'config/client'; +import { VolumeStats } from 'src/views/stats/VolumeStats'; +import { TimeStats } from 'src/views/stats/TimeStats'; +import { FeeStats } from 'src/views/stats/FeeStats'; +import { StatResume } from 'src/views/stats/StatResume'; +import { StatsProvider } from 'src/views/stats/context'; +import { SingleLine } from '../src/components/generic/Styled'; + +export const ButtonRow = styled.div` + width: auto; + display: flex; +`; + +export const SettingsLine = styled(SingleLine)` + margin: 8px 0; +`; + +const StatsView = () => { + return ( + <> + + + + + + ); +}; + +const Wrapped = () => ( + + + + + +); + +export default withApollo(Wrapped); diff --git a/pages/transactions.tsx b/pages/transactions.tsx index 208e724f..689543f0 100644 --- a/pages/transactions.tsx +++ b/pages/transactions.tsx @@ -22,7 +22,6 @@ import { FlowBox } from '../src/views/home/reports/flow'; const TransactionsView = () => { const [indexOpen, setIndexOpen] = useState(0); const [token, setToken] = useState(''); - const [fetching, setFetching] = useState(false); const { auth } = useAccountState(); @@ -42,7 +41,7 @@ const TransactionsView = () => { return ; } - const resumeList = JSON.parse(data.getResume.resume); + const resumeList = data.getResume.resume; return ( <> @@ -75,10 +74,7 @@ const TransactionsView = () => { { - setFetching(true); fetchMore({ variables: { auth, token }, updateQuery: ( @@ -89,18 +85,17 @@ const TransactionsView = () => { ) => { if (!result) return prev; const newToken = result.getResume.token || ''; - const prevEntries = JSON.parse(prev.getResume.resume); - const newEntries = JSON.parse(result.getResume.resume); + const prevEntries = prev.getResume.resume; + const newEntries = result.getResume.resume; const allTransactions = newToken ? [...prevEntries, ...newEntries] : prevEntries; - setFetching(false); return { getResume: { token: newToken, - resume: JSON.stringify(allTransactions), + resume: allTransactions, __typename: 'getResumeType', }, }; diff --git a/scripts/updateToLatest.sh b/scripts/updateToLatest.sh new file mode 100644 index 00000000..22ec2a58 --- /dev/null +++ b/scripts/updateToLatest.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +UPSTREAM=${1:-'@{u}'} +LOCAL=$(git rev-parse @) +REMOTE=$(git rev-parse "$UPSTREAM") + +if [ $LOCAL = $REMOTE ]; then + TAG=$(git tag | sort -V | tail -1) + echo "You are up-to-date on version" $TAG +else + # fetch latest master + echo "Pulling latest changes..." + git fetch + git pull -p + + # install deps + echo "Installing dependencies..." + npm install --quiet + + # build nextjs + echo "Building application..." + npm run build + + TAG=$(git tag | sort -V | tail -1) + echo "Updated to version" $TAG +fi diff --git a/server/helpers/helpers.ts b/server/helpers/helpers.ts index 314fa8df..55418eb4 100644 --- a/server/helpers/helpers.ts +++ b/server/helpers/helpers.ts @@ -36,6 +36,13 @@ export const getCorrectAuth = ( auth: AuthType, context: ContextType ): LndAuthType => { + if (auth.type === 'test' && nodeEnv === 'development') { + return { + host: process.env.TEST_HOST, + macaroon: process.env.TEST_MACAROON, + cert: process.env.TEST_CERT, + }; + } if (auth.type === SERVER_ACCOUNT) { const { account, accounts } = context; if (!account) { diff --git a/server/schema/channel/resolvers.ts b/server/schema/channel/resolvers.ts index be2450c0..9861edbd 100644 --- a/server/schema/channel/resolvers.ts +++ b/server/schema/channel/resolvers.ts @@ -1,3 +1,6 @@ +import { logger } from 'server/helpers/logger'; +import { toWithError } from 'server/helpers/async'; +import { getChannel } from 'ln-service'; import { openChannel } from './resolvers/mutation/openChannel'; import { closeChannel } from './resolvers/mutation/closeChannel'; import { updateFees } from './resolvers/mutation/updateFees'; @@ -20,4 +23,44 @@ export const channelResolvers = { closeChannel, updateFees, }, + Channel: { + channel: async parent => { + const { lnd, id, withNodes = true, localKey, dontResolveKey } = parent; + + if (!lnd) { + logger.debug('ExpectedLNDToGetChannel'); + return null; + } + + if (!id) { + logger.debug('ExpectedIdToGetChannel'); + return null; + } + + const [channel, error] = await toWithError(getChannel({ lnd, id })); + + if (error) { + logger.debug(`Error getting channel with id ${id}: %o`, error); + return null; + } + + const nodeProps = (publicKey: string) => + withNodes ? { node: { lnd, publicKey } } : {}; + + const policiesWithNodes = channel.policies + .map(policy => { + if (dontResolveKey && dontResolveKey === policy.public_key) { + return null; + } + return { + ...policy, + ...nodeProps(policy.public_key), + ...(localKey ? { my_node: policy.public_key === localKey } : {}), + }; + }) + .filter(Boolean); + + return { ...channel, policies: policiesWithNodes }; + }, + }, }; diff --git a/server/schema/channel/resolvers/query/getChannels.ts b/server/schema/channel/resolvers/query/getChannels.ts index a1d34c6a..256abecc 100644 --- a/server/schema/channel/resolvers/query/getChannels.ts +++ b/server/schema/channel/resolvers/query/getChannels.ts @@ -1,12 +1,6 @@ -import { - getChannels as getLnChannels, - getNode, - getChannel, - getWalletInfo, -} from 'ln-service'; +import { getChannels as getLnChannels, getWalletInfo } from 'ln-service'; import { ContextType } from 'server/types/apiTypes'; -import { toWithError, to } from 'server/helpers/async'; -import { logger } from 'server/helpers/logger'; +import { to } from 'server/helpers/async'; import { requestLimiter } from 'server/helpers/rateLimiter'; import { getAuthLnd, getCorrectAuth } from 'server/helpers/helpers'; @@ -20,77 +14,20 @@ export const getChannels = async ( const auth = getCorrectAuth(params.auth, context); const lnd = getAuthLnd(auth); - const [walletInfo, walletError] = await toWithError(getWalletInfo({ lnd })); - const publicKey = walletInfo?.public_key; + const { public_key } = await to(getWalletInfo({ lnd })); - walletError && - logger.debug('Error getting wallet info in getChannels: %o', walletError); - - const channelList = await to( + const { channels } = await to( getLnChannels({ lnd, is_active: params.active, }) ); - const getChannelList = () => - Promise.all( - channelList.channels.map(async channel => { - const [nodeInfo, nodeError] = await toWithError( - getNode({ - lnd, - is_omitting_channels: true, - public_key: channel.partner_public_key, - }) - ); - - const [channelInfo, channelError] = await toWithError( - getChannel({ - lnd, - id: channel.id, - }) - ); - - nodeError && - logger.debug( - `Error getting node with public key ${channel.partner_public_key}: %o`, - nodeError - ); - - channelError && - logger.debug( - `Error getting channel with id ${channel.id}: %o`, - channelError - ); - - let partnerFees = {}; - if (!channelError && publicKey) { - const partnerPolicy = channelInfo.policies.filter( - policy => policy.public_key !== publicKey - ); - if (partnerPolicy && partnerPolicy.length >= 1) { - partnerFees = { - base_fee: partnerPolicy[0].base_fee_mtokens || 0, - fee_rate: partnerPolicy[0].fee_rate || 0, - cltv_delta: partnerPolicy[0].cltv_delta || 0, - }; - } - } - - const partner_node_info = { - ...(!nodeError && nodeInfo), - ...partnerFees, - }; - - return { - ...channel, - time_offline: Math.round((channel.time_offline || 0) / 1000), - time_online: Math.round((channel.time_online || 0) / 1000), - partner_node_info, - }; - }) - ); - - const channels = await getChannelList(); - return channels; + return channels.map(channel => ({ + ...channel, + time_offline: Math.round((channel.time_offline || 0) / 1000), + time_online: Math.round((channel.time_online || 0) / 1000), + partner_node_info: { lnd, publicKey: channel.partner_public_key }, + partner_fee_info: { lnd, id: channel.id, dontResolve: public_key }, + })); }; diff --git a/server/schema/channel/resolvers/query/getClosedChannels.ts b/server/schema/channel/resolvers/query/getClosedChannels.ts index bee81099..e5e31ece 100644 --- a/server/schema/channel/resolvers/query/getClosedChannels.ts +++ b/server/schema/channel/resolvers/query/getClosedChannels.ts @@ -1,7 +1,6 @@ -import { getClosedChannels as getLnClosedChannels, getNode } from 'ln-service'; +import { getClosedChannels as getLnClosedChannels } from 'ln-service'; import { ContextType } from 'server/types/apiTypes'; -import { to, toWithError } from 'server/helpers/async'; -import { logger } from 'server/helpers/logger'; +import { to } from 'server/helpers/async'; import { requestLimiter } from 'server/helpers/rateLimiter'; import { getAuthLnd, getCorrectAuth } from 'server/helpers/helpers'; @@ -36,31 +35,13 @@ export const getClosedChannels = async ( const auth = getCorrectAuth(params.auth, context); const lnd = getAuthLnd(auth); - const closedChannels: ChannelListProps = await to( - getLnClosedChannels({ lnd }) - ); + const { channels }: ChannelListProps = await to(getLnClosedChannels({ lnd })); - const channels = closedChannels.channels.map(async channel => { - const [nodeInfo, nodeError] = await toWithError( - getNode({ - lnd, - is_omitting_channels: true, - public_key: channel.partner_public_key, - }) - ); - - nodeError && - logger.debug( - `Error getting node with public key ${channel.partner_public_key}: %o`, - nodeError - ); - - return { - ...channel, - partner_node_info: { - ...(!nodeError && nodeInfo), - }, - }; - }); - return channels; + return channels.map(channel => ({ + ...channel, + partner_node_info: { + lnd, + publicKey: channel.partner_public_key, + }, + })); }; diff --git a/server/schema/channel/resolvers/query/getPendingChannels.ts b/server/schema/channel/resolvers/query/getPendingChannels.ts index 638b34f2..81d9bb7c 100644 --- a/server/schema/channel/resolvers/query/getPendingChannels.ts +++ b/server/schema/channel/resolvers/query/getPendingChannels.ts @@ -1,10 +1,6 @@ -import { - getPendingChannels as getLnPendingChannels, - getNode, -} from 'ln-service'; +import { getPendingChannels as getLnPendingChannels } from 'ln-service'; import { ContextType } from 'server/types/apiTypes'; -import { to, toWithError } from 'server/helpers/async'; -import { logger } from 'server/helpers/logger'; +import { to } from 'server/helpers/async'; import { requestLimiter } from 'server/helpers/rateLimiter'; import { getAuthLnd, getCorrectAuth } from 'server/helpers/helpers'; @@ -39,31 +35,15 @@ export const getPendingChannels = async ( const auth = getCorrectAuth(params.auth, context); const lnd = getAuthLnd(auth); - const pendingChannels: PendingChannelListProps = await to( + const { pending_channels }: PendingChannelListProps = await to( getLnPendingChannels({ lnd }) ); - const channels = pendingChannels.pending_channels.map(async channel => { - const [nodeInfo, nodeError] = await toWithError( - getNode({ - lnd, - is_omitting_channels: true, - public_key: channel.partner_public_key, - }) - ); - - nodeError && - logger.debug( - `Error getting node with public key ${channel.partner_public_key}: %o`, - nodeError - ); - - return { - ...channel, - partner_node_info: { - ...(!nodeError && nodeInfo), - }, - }; - }); - return channels; + return pending_channels.map(channel => ({ + ...channel, + partner_node_info: { + lnd, + publicKey: channel.partner_public_key, + }, + })); }; diff --git a/server/schema/channel/types.ts b/server/schema/channel/types.ts index b87c2ba6..1a63bd48 100644 --- a/server/schema/channel/types.ts +++ b/server/schema/channel/types.ts @@ -1,6 +1,32 @@ import { gql } from 'apollo-server-micro'; export const channelTypes = gql` + type policyType { + base_fee_mtokens: String + cltv_delta: Int + fee_rate: Int + is_disabled: Boolean + max_htlc_mtokens: String + min_htlc_mtokens: String + public_key: String! + updated_at: String + my_node: Boolean + node: Node + } + + type singleChannelType { + capacity: Int! + id: String! + policies: [policyType!]! + transaction_id: String! + transaction_vout: Int! + updated_at: String + } + + type Channel { + channel: singleChannelType + } + type channelFeeType { alias: String color: String @@ -46,7 +72,8 @@ export const channelTypes = gql` transaction_id: String transaction_vout: Int unsettled_balance: Int - partner_node_info: nodeType + partner_node_info: Node + partner_fee_info: Channel } type closeChannelType { @@ -69,7 +96,7 @@ export const channelTypes = gql` partner_public_key: String transaction_id: String transaction_vout: Int - partner_node_info: nodeType + partner_node_info: Node } type openChannelType { @@ -92,6 +119,6 @@ export const channelTypes = gql` transaction_fee: Int transaction_id: String transaction_vout: Int - partner_node_info: nodeType + partner_node_info: Node } `; diff --git a/server/schema/github/resolvers.ts b/server/schema/github/resolvers.ts new file mode 100644 index 00000000..1239e19a --- /dev/null +++ b/server/schema/github/resolvers.ts @@ -0,0 +1,28 @@ +import { requestLimiter } from 'server/helpers/rateLimiter'; +import { ContextType } from 'server/types/apiTypes'; +import { toWithError } from 'server/helpers/async'; +import { appUrls } from 'server/utils/appUrls'; +import { logger } from 'server/helpers/logger'; + +export const githubResolvers = { + Query: { + getLatestVersion: async ( + _: undefined, + params: any, + context: ContextType + ) => { + await requestLimiter(context.ip, 'getLnPay'); + + const [response, error] = await toWithError(fetch(appUrls.github)); + + if (error) { + logger.debug('Unable to get latest github version'); + throw new Error('NoGithubVersion'); + } + + const json = await response.json(); + + return json.tag_name; + }, + }, +}; diff --git a/server/schema/health/helpers.ts b/server/schema/health/helpers.ts new file mode 100644 index 00000000..238c50d3 --- /dev/null +++ b/server/schema/health/helpers.ts @@ -0,0 +1,72 @@ +import { groupBy } from 'underscore'; + +export const getChannelVolume = forwards => { + const orderedIncoming = groupBy(forwards, f => f.incoming_channel); + const orderedOutgoing = groupBy(forwards, f => f.outgoing_channel); + + const reducedIncoming = reduceTokens(orderedIncoming); + const reducedOutgoing = reduceTokens(orderedOutgoing); + + const together = groupBy( + [...reducedIncoming, ...reducedOutgoing], + c => c.channel + ); + return reduceTokens(together); +}; + +const reduceTokens = array => { + const reducedArray = []; + for (const key in array) { + if (Object.prototype.hasOwnProperty.call(array, key)) { + const channel = array[key]; + const reduced = channel.reduce((a, b) => a + b.tokens, 0); + reducedArray.push({ channel: key, tokens: reduced }); + } + } + return reducedArray; +}; + +export const getChannelIdInfo = ( + id: string +): { blockHeight: number; transaction: number; output: number } | null => { + const format = /^\d*x\d*x\d*$/; + + if (!format.test(id)) return null; + + const separate = id.split('x'); + + return { + blockHeight: Number(separate[0]), + transaction: Number(separate[1]), + output: Number(separate[2]), + }; +}; + +export const getAverage = (array: number[]): number => { + const sum = array.reduce((a, b) => a + b, 0); + return sum / array.length || 0; +}; + +export const getFeeScore = (max: number, current: number): number => { + const score = Math.round(((max - current) / max) * 100); + return Math.max(0, Math.min(100, score)); +}; + +export const getMyFeeScore = ( + max: number, + current: number, + min: number +): { over: boolean; score: number } => { + if (current === min) { + return { over: false, score: 100 }; + } + if (current < min) { + const score = Math.round(((min - current) / min) * 100); + return { over: false, score: 100 - Math.max(0, Math.min(100, score)) }; + } + const minimum = current - min; + const maximum = max - min; + const score = Math.round(((maximum - minimum) / maximum) * 100); + + return { over: true, score: Math.max(0, Math.min(100, score)) }; +}; diff --git a/server/schema/health/resolvers.ts b/server/schema/health/resolvers.ts new file mode 100644 index 00000000..a7334c63 --- /dev/null +++ b/server/schema/health/resolvers.ts @@ -0,0 +1,11 @@ +import getVolumeHealth from './resolvers/getVolumeHealth'; +import getTimeHealth from './resolvers/getTimeHealth'; +import getFeeHealth from './resolvers/getFeeHealth'; + +export const healthResolvers = { + Query: { + getVolumeHealth, + getTimeHealth, + getFeeHealth, + }, +}; diff --git a/server/schema/health/resolvers/getFeeHealth.ts b/server/schema/health/resolvers/getFeeHealth.ts new file mode 100644 index 00000000..bb656be7 --- /dev/null +++ b/server/schema/health/resolvers/getFeeHealth.ts @@ -0,0 +1,132 @@ +import { getChannels, getChannel, getWalletInfo } from 'ln-service'; +import { getCorrectAuth, getAuthLnd } from 'server/helpers/helpers'; +import { requestLimiter } from 'server/helpers/rateLimiter'; +import { to, toWithError } from 'server/helpers/async'; +import { logger } from 'server/helpers/logger'; +import { ContextType } from 'server/types/apiTypes'; +import { getFeeScore, getAverage, getMyFeeScore } from '../helpers'; + +type ChannelFeesType = { + id: string; + publicKey: string; + partnerBaseFee: number; + partnerFeeRate: number; + myBaseFee: number; + myFeeRate: number; +}; + +export default async (_: undefined, params: any, context: ContextType) => { + await requestLimiter(context.ip, 'getFeeHealth'); + + const auth = getCorrectAuth(params.auth, context); + const lnd = getAuthLnd(auth); + + const { public_key } = await to(getWalletInfo({ lnd })); + const { channels } = await to(getChannels({ lnd })); + + const getChannelList = () => + Promise.all( + channels + .map(async channel => { + const { id, partner_public_key: publicKey } = channel; + const [{ policies }, channelError] = await toWithError( + getChannel({ + lnd, + id, + }) + ); + + if (channelError) { + logger.debug( + `Error getting channel with id ${id}: %o`, + channelError + ); + return; + } + + let partnerBaseFee = 0; + let partnerFeeRate = 0; + let myBaseFee = 0; + let myFeeRate = 0; + + if (!channelError && policies) { + for (let i = 0; i < policies.length; i++) { + const policy = policies[i]; + + if (policy.public_key === public_key) { + myBaseFee = Number(policy.base_fee_mtokens); + myFeeRate = policy.fee_rate; + } else { + partnerBaseFee = Number(policy.base_fee_mtokens); + partnerFeeRate = policy.fee_rate; + } + } + } + + return { + id, + publicKey, + partnerBaseFee, + partnerFeeRate, + myBaseFee, + myFeeRate, + }; + }) + .filter(Boolean) + ); + + const list = await getChannelList(); + + const health = list.map((channel: ChannelFeesType) => { + const partnerRateScore = getFeeScore(2000, channel.partnerFeeRate); + const partnerBaseScore = getFeeScore(100000, channel.partnerBaseFee); + const myRateScore = getMyFeeScore(2000, channel.myFeeRate, 200); + const myBaseScore = getMyFeeScore(100000, channel.myBaseFee, 1000); + + const partnerScore = Math.round( + getAverage([partnerBaseScore, partnerRateScore]) + ); + const myScore = Math.round( + getAverage([myRateScore.score, myBaseScore.score]) + ); + + const mySide = { + score: myScore, + rate: channel.myFeeRate, + base: Math.round(channel.myBaseFee / 1000), + rateScore: myRateScore.score, + baseScore: myBaseScore.score, + rateOver: myRateScore.over, + baseOver: myBaseScore.over, + }; + + const partnerSide = { + score: partnerScore, + rate: channel.partnerFeeRate, + base: Math.round(channel.partnerBaseFee / 1000), + rateScore: partnerRateScore, + baseScore: partnerBaseScore, + rateOver: true, + baseOver: true, + }; + + return { + id: channel.id, + partnerSide, + mySide, + partner: { publicKey: channel.publicKey, lnd }, + }; + }); + + const score = Math.round( + getAverage([ + ...health.map(c => c.partnerSide.score), + ...health.map(c => c.mySide.score), + ]) + ); + + return { + score, + channels: health, + }; +}; diff --git a/server/schema/health/resolvers/getTimeHealth.ts b/server/schema/health/resolvers/getTimeHealth.ts new file mode 100644 index 00000000..a58417f1 --- /dev/null +++ b/server/schema/health/resolvers/getTimeHealth.ts @@ -0,0 +1,51 @@ +import { getChannels } from 'ln-service'; +import { getCorrectAuth, getAuthLnd } from 'server/helpers/helpers'; +import { requestLimiter } from 'server/helpers/rateLimiter'; +import { to } from 'server/helpers/async'; +import { ContextType } from 'server/types/apiTypes'; +import { getAverage } from '../helpers'; + +const halfMonthInMilliSeconds = 1296000000; + +export default async (_: undefined, params: any, context: ContextType) => { + await requestLimiter(context.ip, 'getTimeHealth'); + + const auth = getCorrectAuth(params.auth, context); + const lnd = getAuthLnd(auth); + + const { channels } = await to(getChannels({ lnd })); + + const health = channels.map(channel => { + const { + time_offline = 1, + time_online = 1, + id, + partner_public_key, + } = channel; + + const significant = time_offline + time_online > halfMonthInMilliSeconds; + + const defaultProps = { + id, + significant, + monitoredTime: Math.round((time_online + time_offline) / 1000), + monitoredUptime: Math.round(time_online / 1000), + monitoredDowntime: Math.round(time_offline / 1000), + partner: { publicKey: partner_public_key, lnd }, + }; + + const percentOnline = time_online / (time_online + time_offline); + + return { + score: Math.round(percentOnline * 100), + ...defaultProps, + }; + }); + + const average = Math.round(getAverage(health.map(c => c.score))); + + return { + score: average, + channels: health, + }; +}; diff --git a/server/schema/health/resolvers/getVolumeHealth.ts b/server/schema/health/resolvers/getVolumeHealth.ts new file mode 100644 index 00000000..e41aeeb4 --- /dev/null +++ b/server/schema/health/resolvers/getVolumeHealth.ts @@ -0,0 +1,68 @@ +import { getForwards, getChannels, getWalletInfo } from 'ln-service'; +import { requestLimiter } from 'server/helpers/rateLimiter'; +import { getCorrectAuth, getAuthLnd } from 'server/helpers/helpers'; +import { to } from 'server/helpers/async'; +import { subMonths } from 'date-fns'; +import { ContextType } from 'server/types/apiTypes'; +import { getChannelVolume, getChannelIdInfo, getAverage } from '../helpers'; + +const monthInBlocks = 4380; + +export default async (_: undefined, params: any, context: ContextType) => { + await requestLimiter(context.ip, 'getVolumeHealth'); + + const auth = getCorrectAuth(params.auth, context); + const lnd = getAuthLnd(auth); + + const before = new Date().toISOString(); + const after = subMonths(new Date(), 1).toISOString(); + + const { current_block_height } = await to(getWalletInfo({ lnd })); + const { channels } = await to(getChannels({ lnd })); + const { forwards } = await to(getForwards({ lnd, after, before })); + + const channelVolume = getChannelVolume(forwards); + + const channelDetails = channels + .map(channel => { + const { tokens } = + channelVolume.find(c => c.channel === channel.id) || {}; + const info = getChannelIdInfo(channel.id); + + if (!info) return; + + const age = Math.min( + current_block_height - info.blockHeight, + monthInBlocks + ); + + return { + id: channel.id, + volume: tokens, + volumeNormalized: Math.round(tokens / age) || 0, + publicKey: channel.partner_public_key, + }; + }) + .filter(Boolean); + + const average = getAverage(channelDetails.map(c => c.volumeNormalized)); + + const health = channelDetails.map(channel => { + const diff = (channel.volumeNormalized - average) / average || -1; + const score = Math.round((diff + 1) * 100); + + return { + id: channel.id, + score, + volumeNormalized: channel.volumeNormalized, + averageVolumeNormalized: average, + partner: { publicKey: channel.publicKey, lnd }, + }; + }); + + const globalAverage = Math.round( + getAverage(health.map(c => Math.min(c.score, 100))) + ); + + return { score: globalAverage, channels: health }; +}; diff --git a/server/schema/health/types.ts b/server/schema/health/types.ts new file mode 100644 index 00000000..5030a752 --- /dev/null +++ b/server/schema/health/types.ts @@ -0,0 +1,53 @@ +import { gql } from 'apollo-server-micro'; + +export const healthTypes = gql` + type channelHealth { + id: String + score: Int + volumeNormalized: String + averageVolumeNormalized: String + partner: Node + } + + type channelsHealth { + score: Int + channels: [channelHealth] + } + + type channelTimeHealth { + id: String + score: Int + significant: Boolean + monitoredTime: Int + monitoredUptime: Int + monitoredDowntime: Int + partner: Node + } + + type channelsTimeHealth { + score: Int + channels: [channelTimeHealth] + } + + type feeHealth { + score: Int + rate: Int + base: String + rateScore: Int + baseScore: Int + rateOver: Boolean + baseOver: Boolean + } + + type channelFeeHealth { + id: String + partnerSide: feeHealth + mySide: feeHealth + partner: Node + } + + type channelsFeeHealth { + score: Int + channels: [channelFeeHealth] + } +`; diff --git a/server/schema/index.ts b/server/schema/index.ts index ca256411..c5f82a83 100644 --- a/server/schema/index.ts +++ b/server/schema/index.ts @@ -32,6 +32,9 @@ import { walletTypes } from './wallet/types'; import { invoiceTypes } from './invoice/types'; import { networkTypes } from './network/types'; import { transactionTypes } from './transactions/types'; +import { healthResolvers } from './health/resolvers'; +import { healthTypes } from './health/types'; +import { githubResolvers } from './github/resolvers'; const typeDefs = [ generalTypes, @@ -52,6 +55,7 @@ const typeDefs = [ invoiceTypes, networkTypes, transactionTypes, + healthTypes, ]; const resolvers = merge( @@ -70,7 +74,9 @@ const resolvers = merge( invoiceResolvers, channelResolvers, walletResolvers, - transactionResolvers + transactionResolvers, + healthResolvers, + githubResolvers ); export default makeExecutableSchema({ typeDefs, resolvers }); diff --git a/server/schema/invoice/types.ts b/server/schema/invoice/types.ts index 17303828..9c7c4304 100644 --- a/server/schema/invoice/types.ts +++ b/server/schema/invoice/types.ts @@ -47,7 +47,7 @@ export const invoiceTypes = gql` timeout: Int } - type invoiceType { + type newInvoiceType { chainAddress: String createdAt: DateTime description: String diff --git a/server/schema/lnpay/resolvers.ts b/server/schema/lnpay/resolvers.ts index 4806f270..478ad43b 100644 --- a/server/schema/lnpay/resolvers.ts +++ b/server/schema/lnpay/resolvers.ts @@ -27,7 +27,7 @@ export const lnpayResolvers = { const [response, error] = await toWithError(fetch(appUrls.lnpay)); if (error) { - logger.debug('Unable to get lnpay: %o', error); + logger.debug('Unable to connect to ThunderHub LNPAY'); throw new Error('NoLnPay'); } diff --git a/server/schema/node/resolvers.ts b/server/schema/node/resolvers.ts index d785b266..6e5ba2b9 100644 --- a/server/schema/node/resolvers.ts +++ b/server/schema/node/resolvers.ts @@ -3,12 +3,14 @@ import { getWalletInfo, getClosedChannels, } from 'ln-service'; -import { to } from 'server/helpers/async'; +import { to, toWithError } from 'server/helpers/async'; import { requestLimiter } from 'server/helpers/rateLimiter'; import { getAuthLnd, getErrorMsg, getCorrectAuth } from '../../helpers/helpers'; import { ContextType } from '../../types/apiTypes'; import { logger } from '../../helpers/logger'; +const errorNode = { alias: 'Node not found' }; + export const nodeResolvers = { Query: { getNode: async (_: undefined, params: any, context: ContextType) => { @@ -53,4 +55,34 @@ export const nodeResolvers = { }; }, }, + Node: { + node: async parent => { + const { lnd, withChannels, publicKey } = parent; + + if (!lnd) { + logger.debug('ExpectedLNDToGetNode'); + return errorNode; + } + + if (!publicKey) { + logger.debug('ExpectedPublicKeyToGetNode'); + return errorNode; + } + + const [info, error] = await toWithError( + getLnNode({ + lnd, + is_omitting_channels: !withChannels, + public_key: publicKey, + }) + ); + + if (error) { + logger.debug(`Error getting node with key: ${publicKey}`); + return errorNode; + } + + return { ...info, public_key: publicKey }; + }, + }, }; diff --git a/server/schema/node/types.ts b/server/schema/node/types.ts index 9ebe1d73..c84fbb44 100644 --- a/server/schema/node/types.ts +++ b/server/schema/node/types.ts @@ -1,6 +1,22 @@ import { gql } from 'apollo-server-micro'; export const nodeTypes = gql` + type nodeType { + alias: String + capacity: String + channel_count: Int + color: String + updated_at: String + base_fee: Int + fee_rate: Int + cltv_delta: Int + public_key: String + } + + type Node { + node: nodeType + } + type nodeInfoType { chains: [String] color: String diff --git a/server/schema/peer/resolvers.ts b/server/schema/peer/resolvers.ts index b316a69b..63474f44 100644 --- a/server/schema/peer/resolvers.ts +++ b/server/schema/peer/resolvers.ts @@ -1,4 +1,4 @@ -import { getPeers, getNode, removePeer, addPeer } from 'ln-service'; +import { getPeers, removePeer, addPeer } from 'ln-service'; import { ContextType } from 'server/types/apiTypes'; import { logger } from 'server/helpers/logger'; import { requestLimiter } from 'server/helpers/rateLimiter'; @@ -7,6 +7,7 @@ import { getErrorMsg, getCorrectAuth, } from 'server/helpers/helpers'; +import { to } from 'server/helpers/async'; interface PeerProps { bytes_received: number; @@ -28,39 +29,16 @@ export const peerResolvers = { const auth = getCorrectAuth(params.auth, context); const lnd = getAuthLnd(auth); - try { - const { peers }: { peers: PeerProps[] } = await getPeers({ + const { peers }: { peers: PeerProps[] } = await to( + getPeers({ lnd, - }); + }) + ); - const getPeerList = () => - Promise.all( - peers.map(async peer => { - try { - const nodeInfo = await getNode({ - lnd, - is_omitting_channels: true, - public_key: peer.public_key, - }); - - return { - ...peer, - partner_node_info: { - ...nodeInfo, - }, - }; - } catch (error) { - return { ...peer, partner_node_info: {} }; - } - }) - ); - - const peerList = await getPeerList(); - return peerList; - } catch (error) { - logger.error('Error getting peers: %o', error); - throw new Error(getErrorMsg(error)); - } + return peers.map(peer => ({ + ...peer, + partner_node_info: { lnd, publicKey: peer.public_key }, + })); }, }, Mutation: { diff --git a/server/schema/peer/types.ts b/server/schema/peer/types.ts index 1494ff06..baff53b6 100644 --- a/server/schema/peer/types.ts +++ b/server/schema/peer/types.ts @@ -11,6 +11,6 @@ export const peerTypes = gql` socket: String tokens_received: Int tokens_sent: Int - partner_node_info: nodeType + partner_node_info: Node } `; diff --git a/server/schema/transactions/resolvers.ts b/server/schema/transactions/resolvers.ts index 26f4cc5b..965d049e 100644 --- a/server/schema/transactions/resolvers.ts +++ b/server/schema/transactions/resolvers.ts @@ -1,23 +1,17 @@ -import { getNodeFromChannel } from 'server/helpers/getNodeFromChannel'; import { getPayments, getInvoices, - getNode, getForwards as getLnForwards, getWalletInfo, } from 'ln-service'; import { compareDesc, subHours, subDays, subMonths, subYears } from 'date-fns'; import { sortBy } from 'underscore'; import { ContextType } from 'server/types/apiTypes'; -import { logger } from 'server/helpers/logger'; import { requestLimiter } from 'server/helpers/rateLimiter'; -import { - getAuthLnd, - getErrorMsg, - getCorrectAuth, -} from 'server/helpers/helpers'; +import { getAuthLnd, getCorrectAuth } from 'server/helpers/helpers'; +import { to } from 'server/helpers/async'; import { ForwardCompleteProps } from '../widgets/resolvers/interface'; -import { PaymentsProps, InvoicesProps, NodeProps } from './interface'; +import { PaymentsProps, InvoicesProps } from './interface'; export const transactionResolvers = { Query: { @@ -27,41 +21,6 @@ export const transactionResolvers = { const auth = getCorrectAuth(params.auth, context); const lnd = getAuthLnd(auth); - let payments; - let invoices; - - try { - const paymentList: PaymentsProps = await getPayments({ - lnd, - }); - - const getMappedPayments = () => - Promise.all( - paymentList.payments.map(async payment => { - let nodeInfo: NodeProps; - try { - nodeInfo = await getNode({ - lnd, - is_omitting_channels: true, - public_key: payment.destination, - }); - } catch (error) { - nodeInfo = { alias: payment.destination?.substring(0, 6) }; - } - return { - type: 'payment', - alias: nodeInfo.alias, - date: payment.created_at, - ...payment, - }; - }) - ); - payments = await getMappedPayments(); - } catch (error) { - logger.error('Error getting payments: %o', error); - throw new Error(getErrorMsg(error)); - } - const invoiceProps = params.token ? { token: params.token } : { limit: 25 }; @@ -71,33 +30,46 @@ export const transactionResolvers = { let token = ''; let withInvoices = true; - try { - const invoiceList: InvoicesProps = await getInvoices({ + const invoiceList: InvoicesProps = await to( + getInvoices({ lnd, ...invoiceProps, - }); + }) + ); - invoices = invoiceList.invoices.map(invoice => { - return { - type: 'invoice', - date: invoice.confirmed_at || invoice.created_at, - ...invoice, - }; - }); + const invoices = invoiceList.invoices.map(invoice => { + return { + type: 'invoice', + date: invoice.confirmed_at || invoice.created_at, + ...invoice, + isTypeOf: 'InvoiceType', + }; + }); - if (invoices.length <= 0) { - withInvoices = false; - } else { - const { date } = invoices[invoices.length - 1]; - firstInvoiceDate = invoices[0].date; - lastInvoiceDate = date; - token = invoiceList.next; - } - } catch (error) { - logger.error('Error getting invoices: %o', error); - throw new Error(getErrorMsg(error)); + if (invoices.length <= 0) { + withInvoices = false; + } else { + const { date } = invoices[invoices.length - 1]; + firstInvoiceDate = invoices[0].date; + lastInvoiceDate = date; + token = invoiceList.next; } + const paymentList: PaymentsProps = await to( + getPayments({ + lnd, + }) + ); + + const payments = paymentList.payments.map(payment => ({ + ...payment, + type: 'payment', + date: payment.created_at, + destination_node: { lnd, publicKey: payment.destination }, + hops: [...payment.hops.map(hop => ({ lnd, publicKey: hop }))], + isTypeOf: 'PaymentType', + })); + const filterArray = payment => { const last = compareDesc(new Date(lastInvoiceDate), new Date(payment.date)) === 1; @@ -112,14 +84,14 @@ export const transactionResolvers = { ? payments.filter(filterArray) : payments; - const resumeArray = sortBy( + const resume = sortBy( [...invoices, ...filteredPayments], 'date' ).reverse(); return { token, - resume: JSON.stringify(resumeArray), + resume, }; }, getForwards: async (_: undefined, params: any, context: ContextType) => { @@ -145,54 +117,44 @@ export const transactionResolvers = { startDate = subHours(endDate, 24); } - const walletInfo: { public_key: string } = await getWalletInfo({ - lnd, - }); + const { public_key } = await to( + getWalletInfo({ + lnd, + }) + ); - const getAlias = (array: any[], publicKey: string) => - Promise.all( - array.map(async forward => { - const inNodeAlias = await getNodeFromChannel( - forward.incoming_channel, - publicKey, - lnd - ); - const outNodeAlias = await getNodeFromChannel( - forward.outgoing_channel, - publicKey, - lnd - ); - return { - incoming_alias: inNodeAlias.alias, - incoming_color: inNodeAlias.color, - outgoing_alias: outNodeAlias.alias, - outgoing_color: outNodeAlias.color, - ...forward, - }; - }) - ); - - try { - const forwardsList: ForwardCompleteProps = await getLnForwards({ + const forwardsList: ForwardCompleteProps = await to( + getLnForwards({ lnd, after: startDate, before: endDate, - }); + }) + ); - const list = await getAlias( - forwardsList.forwards, - walletInfo.public_key - ); + const list = forwardsList.forwards.map(forward => ({ + ...forward, + incoming_channel_info: { + lnd, + id: forward.incoming_channel, + dontResolveKey: public_key, + }, + outgoing_channel_info: { + lnd, + id: forward.outgoing_channel, + dontResolveKey: public_key, + }, + })); - const forwards = sortBy(list, 'created_at').reverse(); - return { - token: forwardsList.next, - forwards, - }; - } catch (error) { - logger.error('Error getting forwards: %o', error); - throw new Error(getErrorMsg(error)); - } + const forwards = sortBy(list, 'created_at').reverse(); + return { + token: forwardsList.next, + forwards, + }; + }, + }, + Transaction: { + __resolveType(parent) { + return parent.isTypeOf; }, }, }; diff --git a/server/schema/transactions/types.ts b/server/schema/transactions/types.ts index b09c6175..474ddb81 100644 --- a/server/schema/transactions/types.ts +++ b/server/schema/transactions/types.ts @@ -11,17 +11,60 @@ export const transactionTypes = gql` fee: Int fee_mtokens: String incoming_channel: String - incoming_alias: String - incoming_color: String mtokens: String outgoing_channel: String - outgoing_alias: String - outgoing_color: String tokens: Int + incoming_channel_info: Channel + outgoing_channel_info: Channel } + type PaymentType { + created_at: String! + destination: String! + destination_node: Node + fee: Int! + fee_mtokens: String! + hops: [Node] + id: String! + index: Int + is_confirmed: Boolean! + is_outgoing: Boolean! + mtokens: String! + request: String + safe_fee: Int! + safe_tokens: Int + secret: String! + tokens: Int! + type: String! + date: String! + } + + type InvoiceType { + chain_address: String + confirmed_at: String + created_at: String! + description: String! + description_hash: String + expires_at: String! + id: String! + is_canceled: Boolean + is_confirmed: Boolean! + is_held: Boolean + is_private: Boolean! + is_push: Boolean + received: Int! + received_mtokens: String! + request: String + secret: String! + tokens: Int! + type: String! + date: String! + } + + union Transaction = InvoiceType | PaymentType + type getResumeType { token: String - resume: String + resume: [Transaction] } `; diff --git a/server/schema/types.ts b/server/schema/types.ts index 4f63df93..9437934a 100644 --- a/server/schema/types.ts +++ b/server/schema/types.ts @@ -9,17 +9,6 @@ export const generalTypes = gql` cert: String } - type nodeType { - alias: String - capacity: String - channel_count: Int - color: String - updated_at: String - base_fee: Int - fee_rate: Int - cltv_delta: Int - } - # A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the # date-time format outlined in section 5.6 of the RFC 3339 profile of the ISO # 8601 standard for representation of dates and times using the Gregorian calendar. @@ -28,6 +17,9 @@ export const generalTypes = gql` export const queryTypes = gql` type Query { + getVolumeHealth(auth: authType!): channelsHealth + getTimeHealth(auth: authType!): channelsTimeHealth + getFeeHealth(auth: authType!): channelsFeeHealth getChannelBalance(auth: authType!): channelBalanceType getChannels(auth: authType!, active: Boolean): [channelType] getClosedChannels(auth: authType!, type: String): [closedChannelType] @@ -87,6 +79,7 @@ export const queryTypes = gql` getServerAccounts: [serverAccountType] getLnPayInfo: lnPayInfoType getLnPay(amount: Int): String + getLatestVersion: String } `; @@ -115,7 +108,7 @@ export const mutationTypes = gql` ): Boolean parsePayment(auth: authType!, request: String!): parsePaymentType pay(auth: authType!, request: String!, tokens: Int): payType - createInvoice(auth: authType!, amount: Int!): invoiceType + createInvoice(auth: authType!, amount: Int!): newInvoiceType payViaRoute(auth: authType!, route: String!): Boolean createAddress(auth: authType!, nested: Boolean): String sendToAddress( diff --git a/server/utils/appUrls.ts b/server/utils/appUrls.ts index 8d87a62a..8d55eafd 100644 --- a/server/utils/appUrls.ts +++ b/server/utils/appUrls.ts @@ -4,8 +4,10 @@ const lnpay = : 'https://thunderhub.io/api/lnpay'; export const appUrls = { + lnpay, fees: 'https://bitcoinfees.earn.com/api/v1/fees/recommended', ticker: 'https://blockchain.info/ticker', hodlhodl: 'https://hodlhodl.com/api', - lnpay, + github: 'https://api.github.com/repos/apotdevin/thunderhub/releases/latest', + update: 'https://github.com/apotdevin/thunderhub#updating', }; diff --git a/src/components/generic/Styled.tsx b/src/components/generic/Styled.tsx index f05c04f2..c38a0678 100644 --- a/src/components/generic/Styled.tsx +++ b/src/components/generic/Styled.tsx @@ -14,6 +14,7 @@ import { colorButtonBackground, colorButtonBorder, hoverTextColor, + themeColors, } from '../../styles/Themes'; export const CardWithTitle = styled.div` @@ -238,3 +239,15 @@ export const ResponsiveSingle = styled(SingleLine)` width: 100%; } `; + +export const CopyIcon = styled.span` + cursor: pointer; + margin-left: 4px; + padding: 0 4px; + border-radius: 2px; + + &:hover { + background-color: ${themeColors.blue2}; + color: white; + } +`; diff --git a/src/components/generic/helpers.tsx b/src/components/generic/helpers.tsx index 823c3452..20c4c999 100644 --- a/src/components/generic/helpers.tsx +++ b/src/components/generic/helpers.tsx @@ -5,8 +5,16 @@ import { differenceInCalendarDays, isToday, } from 'date-fns'; -import { X } from 'react-feather'; -import { SmallLink, DarkSubTitle, OverflowText, SingleLine } from './Styled'; +import { X, Copy } from 'react-feather'; +import CopyToClipboard from 'react-copy-to-clipboard'; +import { toast } from 'react-toastify'; +import { + SmallLink, + DarkSubTitle, + OverflowText, + SingleLine, + CopyIcon, +} from './Styled'; import { StatusDot, DetailLine } from './CardGeneric'; const shorten = (text: string): string => { @@ -26,12 +34,22 @@ export const getTransactionLink = (transaction: string) => { ); }; -export const getNodeLink = (publicKey: string) => { +export const getNodeLink = (publicKey: string, alias?: string) => { + if (alias && alias === 'Node not found') { + return 'Node not found'; + } const link = `https://1ml.com/node/${publicKey}`; return ( - - {shorten(publicKey)} - + <> + + {alias ? alias : shorten(publicKey)} + + toast.success('Copied')}> + + + + + ); }; diff --git a/src/components/gridWrapper/GridWrapper.tsx b/src/components/gridWrapper/GridWrapper.tsx index e100cc5a..c890cd42 100644 --- a/src/components/gridWrapper/GridWrapper.tsx +++ b/src/components/gridWrapper/GridWrapper.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; import { BitcoinFees } from 'src/components/bitcoinInfo/BitcoinFees'; import { BitcoinPrice } from 'src/components/bitcoinInfo/BitcoinPrice'; import { useAccountState } from 'src/context/AccountContext'; @@ -10,11 +10,20 @@ import { StatusCheck } from '../statusCheck/StatusCheck'; import { LoadingCard } from '../loading/LoadingCard'; import { ServerAccounts } from '../accounts/ServerAccounts'; -const Container = styled.div` +type GridProps = { + noNavigation?: boolean; +}; + +const Container = styled.div` display: grid; grid-template-areas: 'nav content content'; grid-template-columns: auto 1fr 200px; - gap: 16px; + + ${({ noNavigation }) => + !noNavigation && + css` + gap: 16px; + `} @media (${mediaWidths.mobile}) { display: flex; @@ -26,7 +35,10 @@ const ContentStyle = styled.div` grid-area: content; `; -export const GridWrapper: React.FC = ({ children }) => { +export const GridWrapper: React.FC = ({ + children, + noNavigation, +}) => { const { hasAccount, auth } = useAccountState(); const renderContent = () => { if (hasAccount === 'false') { @@ -36,12 +48,12 @@ export const GridWrapper: React.FC = ({ children }) => { }; return (
- + {auth && } - + {!noNavigation && } {renderContent()}
diff --git a/src/components/link/Link.tsx b/src/components/link/Link.tsx index ad200486..46116974 100644 --- a/src/components/link/Link.tsx +++ b/src/components/link/Link.tsx @@ -36,6 +36,7 @@ const StyledLink = styled.a` `; const NoStyling = styled.a` + cursor: pointer; text-decoration: none; `; @@ -48,6 +49,7 @@ interface LinkProps { inheritColor?: boolean; fullWidth?: boolean; noStyling?: boolean; + newTab?: boolean; } const { publicRuntimeConfig } = getConfig(); @@ -62,6 +64,7 @@ export const Link = ({ inheritColor, fullWidth, noStyling, + newTab, }: LinkProps) => { const props = { fontColor: color, underline, inheritColor, fullWidth }; @@ -71,7 +74,11 @@ export const Link = ({ if (href) { return ( - + {children} ); diff --git a/src/components/version/Version.tsx b/src/components/version/Version.tsx new file mode 100644 index 00000000..9c686884 --- /dev/null +++ b/src/components/version/Version.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import { useGetLatestVersionQuery } from 'src/graphql/queries/__generated__/getLatestVersion.generated'; +import getConfig from 'next/config'; +import styled from 'styled-components'; +import { appUrls } from 'server/utils/appUrls'; +import { Link } from '../link/Link'; + +const VersionBox = styled.div` + width: 100%; + text-align: center; + font-size: 14px; + opacity: 0.3; + cursor: pointer; + + &:hover { + opacity: 1; + color: white; + } +`; + +const { publicRuntimeConfig } = getConfig(); +const { npmVersion } = publicRuntimeConfig; + +export const Version = () => { + const { data, loading, error } = useGetLatestVersionQuery(); + + if (error || !data || loading || !data?.getLatestVersion) { + return null; + } + + const githubVersion = data.getLatestVersion.replace('v', ''); + const version = githubVersion.split('.'); + const localVersion = npmVersion.split('.').map(Number); + + const newVersionAvailable = + version[0] > localVersion[0] || + version[1] > localVersion[1] || + version[2] > localVersion[2]; + + if (!newVersionAvailable) { + return null; + } + + return ( + + {`Version ${githubVersion} is available. You are on version ${npmVersion}`} + + ); +}; diff --git a/src/context/AccountContext.tsx b/src/context/AccountContext.tsx index 7e22fbdf..ea5a0851 100644 --- a/src/context/AccountContext.tsx +++ b/src/context/AccountContext.tsx @@ -7,7 +7,7 @@ import { getAuthFromAccount, } from './helpers/context'; -export type SERVER_ACCOUNT_TYPE = 'sso' | 'server'; +export type SERVER_ACCOUNT_TYPE = 'sso' | 'server' | 'test'; export type ACCOUNT_TYPE = 'client'; export const CLIENT_ACCOUNT: ACCOUNT_TYPE = 'client'; diff --git a/src/graphql/fragmentTypes.json b/src/graphql/fragmentTypes.json new file mode 100644 index 00000000..af149eb7 --- /dev/null +++ b/src/graphql/fragmentTypes.json @@ -0,0 +1,18 @@ +{ + "__schema": { + "types": [ + { + "kind": "UNION", + "name": "Transaction", + "possibleTypes": [ + { + "name": "InvoiceType" + }, + { + "name": "PaymentType" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/src/graphql/mutations/__generated__/createInvoice.generated.tsx b/src/graphql/mutations/__generated__/createInvoice.generated.tsx index db5bf6fd..d1fc686a 100644 --- a/src/graphql/mutations/__generated__/createInvoice.generated.tsx +++ b/src/graphql/mutations/__generated__/createInvoice.generated.tsx @@ -10,7 +10,7 @@ export type CreateInvoiceMutationVariables = { export type CreateInvoiceMutation = { __typename?: 'Mutation' } & { createInvoice?: Types.Maybe< - { __typename?: 'invoiceType' } & Pick + { __typename?: 'newInvoiceType' } & Pick >; }; diff --git a/src/graphql/queries/__generated__/getChannels.generated.tsx b/src/graphql/queries/__generated__/getChannels.generated.tsx index e9617cfd..5bf99530 100644 --- a/src/graphql/queries/__generated__/getChannels.generated.tsx +++ b/src/graphql/queries/__generated__/getChannels.generated.tsx @@ -38,17 +38,40 @@ export type GetChannelsQuery = { __typename?: 'Query' } & { | 'unsettled_balance' > & { partner_node_info?: Types.Maybe< - { __typename?: 'partnerNodeType' } & Pick< - Types.PartnerNodeType, - | 'alias' - | 'capacity' - | 'channel_count' - | 'color' - | 'updated_at' - | 'base_fee' - | 'fee_rate' - | 'cltv_delta' - > + { __typename?: 'Node' } & { + node?: Types.Maybe< + { __typename?: 'nodeType' } & Pick< + Types.NodeType, + | 'alias' + | 'capacity' + | 'channel_count' + | 'color' + | 'updated_at' + > + >; + } + >; + partner_fee_info?: Types.Maybe< + { __typename?: 'Channel' } & { + channel?: Types.Maybe< + { __typename?: 'singleChannelType' } & { + policies: Array< + { __typename?: 'policyType' } & { + node?: Types.Maybe< + { __typename?: 'Node' } & { + node?: Types.Maybe< + { __typename?: 'nodeType' } & Pick< + Types.NodeType, + 'base_fee' | 'fee_rate' | 'cltv_delta' + > + >; + } + >; + } + >; + } + >; + } >; } > @@ -82,14 +105,26 @@ export const GetChannelsDocument = gql` transaction_vout unsettled_balance partner_node_info { - alias - capacity - channel_count - color - updated_at - base_fee - fee_rate - cltv_delta + node { + alias + capacity + channel_count + color + updated_at + } + } + partner_fee_info { + channel { + policies { + node { + node { + base_fee + fee_rate + cltv_delta + } + } + } + } } } } diff --git a/src/graphql/queries/__generated__/getClosedChannels.generated.tsx b/src/graphql/queries/__generated__/getClosedChannels.generated.tsx index 92731d10..b2359f6a 100644 --- a/src/graphql/queries/__generated__/getClosedChannels.generated.tsx +++ b/src/graphql/queries/__generated__/getClosedChannels.generated.tsx @@ -29,10 +29,18 @@ export type GetClosedChannelsQuery = { __typename?: 'Query' } & { | 'transaction_vout' > & { partner_node_info?: Types.Maybe< - { __typename?: 'partnerNodeType' } & Pick< - Types.PartnerNodeType, - 'alias' | 'capacity' | 'channel_count' | 'color' | 'updated_at' - > + { __typename?: 'Node' } & { + node?: Types.Maybe< + { __typename?: 'nodeType' } & Pick< + Types.NodeType, + | 'alias' + | 'capacity' + | 'channel_count' + | 'color' + | 'updated_at' + > + >; + } >; } > @@ -58,11 +66,13 @@ export const GetClosedChannelsDocument = gql` transaction_id transaction_vout partner_node_info { - alias - capacity - channel_count - color - updated_at + node { + alias + capacity + channel_count + color + updated_at + } } } } diff --git a/src/graphql/queries/__generated__/getFeeHealth.generated.tsx b/src/graphql/queries/__generated__/getFeeHealth.generated.tsx new file mode 100644 index 00000000..e54a4420 --- /dev/null +++ b/src/graphql/queries/__generated__/getFeeHealth.generated.tsx @@ -0,0 +1,146 @@ +import gql from 'graphql-tag'; +import * as ApolloReactCommon from '@apollo/react-common'; +import * as ApolloReactHooks from '@apollo/react-hooks'; +import * as Types from '../../types'; + +export type GetFeeHealthQueryVariables = { + auth: Types.AuthType; +}; + +export type GetFeeHealthQuery = { __typename?: 'Query' } & { + getFeeHealth?: Types.Maybe< + { __typename?: 'channelsFeeHealth' } & Pick< + Types.ChannelsFeeHealth, + 'score' + > & { + channels?: Types.Maybe< + Array< + Types.Maybe< + { __typename?: 'channelFeeHealth' } & Pick< + Types.ChannelFeeHealth, + 'id' + > & { + partnerSide?: Types.Maybe< + { __typename?: 'feeHealth' } & Pick< + Types.FeeHealth, + | 'score' + | 'rate' + | 'base' + | 'rateScore' + | 'baseScore' + | 'rateOver' + | 'baseOver' + > + >; + mySide?: Types.Maybe< + { __typename?: 'feeHealth' } & Pick< + Types.FeeHealth, + | 'score' + | 'rate' + | 'base' + | 'rateScore' + | 'baseScore' + | 'rateOver' + | 'baseOver' + > + >; + partner?: Types.Maybe< + { __typename?: 'Node' } & { + node?: Types.Maybe< + { __typename?: 'nodeType' } & Pick< + Types.NodeType, + 'alias' + > + >; + } + >; + } + > + > + >; + } + >; +}; + +export const GetFeeHealthDocument = gql` + query GetFeeHealth($auth: authType!) { + getFeeHealth(auth: $auth) { + score + channels { + id + partnerSide { + score + rate + base + rateScore + baseScore + rateOver + baseOver + } + mySide { + score + rate + base + rateScore + baseScore + rateOver + baseOver + } + partner { + node { + alias + } + } + } + } + } +`; + +/** + * __useGetFeeHealthQuery__ + * + * To run a query within a React component, call `useGetFeeHealthQuery` and pass it any options that fit your needs. + * When your component renders, `useGetFeeHealthQuery` 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 } = useGetFeeHealthQuery({ + * variables: { + * auth: // value for 'auth' + * }, + * }); + */ +export function useGetFeeHealthQuery( + baseOptions?: ApolloReactHooks.QueryHookOptions< + GetFeeHealthQuery, + GetFeeHealthQueryVariables + > +) { + return ApolloReactHooks.useQuery< + GetFeeHealthQuery, + GetFeeHealthQueryVariables + >(GetFeeHealthDocument, baseOptions); +} +export function useGetFeeHealthLazyQuery( + baseOptions?: ApolloReactHooks.LazyQueryHookOptions< + GetFeeHealthQuery, + GetFeeHealthQueryVariables + > +) { + return ApolloReactHooks.useLazyQuery< + GetFeeHealthQuery, + GetFeeHealthQueryVariables + >(GetFeeHealthDocument, baseOptions); +} +export type GetFeeHealthQueryHookResult = ReturnType< + typeof useGetFeeHealthQuery +>; +export type GetFeeHealthLazyQueryHookResult = ReturnType< + typeof useGetFeeHealthLazyQuery +>; +export type GetFeeHealthQueryResult = ApolloReactCommon.QueryResult< + GetFeeHealthQuery, + GetFeeHealthQueryVariables +>; diff --git a/src/graphql/queries/__generated__/getForwards.generated.tsx b/src/graphql/queries/__generated__/getForwards.generated.tsx index 523f44db..db63073b 100644 --- a/src/graphql/queries/__generated__/getForwards.generated.tsx +++ b/src/graphql/queries/__generated__/getForwards.generated.tsx @@ -20,14 +20,55 @@ export type GetForwardsQuery = { __typename?: 'Query' } & { | 'fee' | 'fee_mtokens' | 'incoming_channel' - | 'incoming_alias' - | 'incoming_color' | 'mtokens' | 'outgoing_channel' - | 'outgoing_alias' - | 'outgoing_color' | 'tokens' - > + > & { + incoming_channel_info?: Types.Maybe< + { __typename?: 'Channel' } & { + channel?: Types.Maybe< + { __typename?: 'singleChannelType' } & { + policies: Array< + { __typename?: 'policyType' } & { + node?: Types.Maybe< + { __typename?: 'Node' } & { + node?: Types.Maybe< + { __typename?: 'nodeType' } & Pick< + Types.NodeType, + 'alias' | 'color' + > + >; + } + >; + } + >; + } + >; + } + >; + outgoing_channel_info?: Types.Maybe< + { __typename?: 'Channel' } & { + channel?: Types.Maybe< + { __typename?: 'singleChannelType' } & { + policies: Array< + { __typename?: 'policyType' } & { + node?: Types.Maybe< + { __typename?: 'Node' } & { + node?: Types.Maybe< + { __typename?: 'nodeType' } & Pick< + Types.NodeType, + 'alias' | 'color' + > + >; + } + >; + } + >; + } + >; + } + >; + } > > >; @@ -43,13 +84,33 @@ export const GetForwardsDocument = gql` fee fee_mtokens incoming_channel - incoming_alias - incoming_color mtokens outgoing_channel - outgoing_alias - outgoing_color tokens + incoming_channel_info { + channel { + policies { + node { + node { + alias + color + } + } + } + } + } + outgoing_channel_info { + channel { + policies { + node { + node { + alias + color + } + } + } + } + } } token } diff --git a/src/graphql/queries/__generated__/getLatestVersion.generated.tsx b/src/graphql/queries/__generated__/getLatestVersion.generated.tsx new file mode 100644 index 00000000..ab6d70a2 --- /dev/null +++ b/src/graphql/queries/__generated__/getLatestVersion.generated.tsx @@ -0,0 +1,65 @@ +import gql from 'graphql-tag'; +import * as ApolloReactCommon from '@apollo/react-common'; +import * as ApolloReactHooks from '@apollo/react-hooks'; +import * as Types from '../../types'; + +export type GetLatestVersionQueryVariables = {}; + +export type GetLatestVersionQuery = { __typename?: 'Query' } & Pick< + Types.Query, + 'getLatestVersion' +>; + +export const GetLatestVersionDocument = gql` + query GetLatestVersion { + getLatestVersion + } +`; + +/** + * __useGetLatestVersionQuery__ + * + * To run a query within a React component, call `useGetLatestVersionQuery` and pass it any options that fit your needs. + * When your component renders, `useGetLatestVersionQuery` 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 } = useGetLatestVersionQuery({ + * variables: { + * }, + * }); + */ +export function useGetLatestVersionQuery( + baseOptions?: ApolloReactHooks.QueryHookOptions< + GetLatestVersionQuery, + GetLatestVersionQueryVariables + > +) { + return ApolloReactHooks.useQuery< + GetLatestVersionQuery, + GetLatestVersionQueryVariables + >(GetLatestVersionDocument, baseOptions); +} +export function useGetLatestVersionLazyQuery( + baseOptions?: ApolloReactHooks.LazyQueryHookOptions< + GetLatestVersionQuery, + GetLatestVersionQueryVariables + > +) { + return ApolloReactHooks.useLazyQuery< + GetLatestVersionQuery, + GetLatestVersionQueryVariables + >(GetLatestVersionDocument, baseOptions); +} +export type GetLatestVersionQueryHookResult = ReturnType< + typeof useGetLatestVersionQuery +>; +export type GetLatestVersionLazyQueryHookResult = ReturnType< + typeof useGetLatestVersionLazyQuery +>; +export type GetLatestVersionQueryResult = ApolloReactCommon.QueryResult< + GetLatestVersionQuery, + GetLatestVersionQueryVariables +>; diff --git a/src/graphql/queries/__generated__/getNode.generated.tsx b/src/graphql/queries/__generated__/getNode.generated.tsx index db71d769..b67bc198 100644 --- a/src/graphql/queries/__generated__/getNode.generated.tsx +++ b/src/graphql/queries/__generated__/getNode.generated.tsx @@ -11,8 +11,8 @@ export type GetNodeQueryVariables = { export type GetNodeQuery = { __typename?: 'Query' } & { getNode?: Types.Maybe< - { __typename?: 'partnerNodeType' } & Pick< - Types.PartnerNodeType, + { __typename?: 'nodeType' } & Pick< + Types.NodeType, 'alias' | 'capacity' | 'channel_count' | 'color' | 'updated_at' > >; diff --git a/src/graphql/queries/__generated__/getPeers.generated.tsx b/src/graphql/queries/__generated__/getPeers.generated.tsx index f76b5dbc..9557028c 100644 --- a/src/graphql/queries/__generated__/getPeers.generated.tsx +++ b/src/graphql/queries/__generated__/getPeers.generated.tsx @@ -24,10 +24,18 @@ export type GetPeersQuery = { __typename?: 'Query' } & { | 'tokens_sent' > & { partner_node_info?: Types.Maybe< - { __typename?: 'partnerNodeType' } & Pick< - Types.PartnerNodeType, - 'alias' | 'capacity' | 'channel_count' | 'color' | 'updated_at' - > + { __typename?: 'Node' } & { + node?: Types.Maybe< + { __typename?: 'nodeType' } & Pick< + Types.NodeType, + | 'alias' + | 'capacity' + | 'channel_count' + | 'color' + | 'updated_at' + > + >; + } >; } > @@ -48,11 +56,13 @@ export const GetPeersDocument = gql` tokens_received tokens_sent partner_node_info { - alias - capacity - channel_count - color - updated_at + node { + alias + capacity + channel_count + color + updated_at + } } } } diff --git a/src/graphql/queries/__generated__/getPendingChannels.generated.tsx b/src/graphql/queries/__generated__/getPendingChannels.generated.tsx index cdad0d18..6a0ce18e 100644 --- a/src/graphql/queries/__generated__/getPendingChannels.generated.tsx +++ b/src/graphql/queries/__generated__/getPendingChannels.generated.tsx @@ -29,10 +29,18 @@ export type GetPendingChannelsQuery = { __typename?: 'Query' } & { | 'transaction_vout' > & { partner_node_info?: Types.Maybe< - { __typename?: 'partnerNodeType' } & Pick< - Types.PartnerNodeType, - 'alias' | 'capacity' | 'channel_count' | 'color' | 'updated_at' - > + { __typename?: 'Node' } & { + node?: Types.Maybe< + { __typename?: 'nodeType' } & Pick< + Types.NodeType, + | 'alias' + | 'capacity' + | 'channel_count' + | 'color' + | 'updated_at' + > + >; + } >; } > @@ -58,11 +66,13 @@ export const GetPendingChannelsDocument = gql` transaction_id transaction_vout partner_node_info { - alias - capacity - channel_count - color - updated_at + node { + alias + capacity + channel_count + color + updated_at + } } } } diff --git a/src/graphql/queries/__generated__/getResume.generated.tsx b/src/graphql/queries/__generated__/getResume.generated.tsx index c99058be..b4848bf0 100644 --- a/src/graphql/queries/__generated__/getResume.generated.tsx +++ b/src/graphql/queries/__generated__/getResume.generated.tsx @@ -10,10 +10,80 @@ export type GetResumeQueryVariables = { export type GetResumeQuery = { __typename?: 'Query' } & { getResume?: Types.Maybe< - { __typename?: 'getResumeType' } & Pick< - Types.GetResumeType, - 'token' | 'resume' - > + { __typename?: 'getResumeType' } & Pick & { + resume?: Types.Maybe< + Array< + Types.Maybe< + | ({ __typename?: 'InvoiceType' } & Pick< + Types.InvoiceType, + | 'chain_address' + | 'confirmed_at' + | 'created_at' + | 'description' + | 'description_hash' + | 'expires_at' + | 'id' + | 'is_canceled' + | 'is_confirmed' + | 'is_held' + | 'is_private' + | 'is_push' + | 'received' + | 'received_mtokens' + | 'request' + | 'secret' + | 'tokens' + | 'type' + | 'date' + >) + | ({ __typename?: 'PaymentType' } & Pick< + Types.PaymentType, + | 'created_at' + | 'destination' + | 'fee' + | 'fee_mtokens' + | 'id' + | 'index' + | 'is_confirmed' + | 'is_outgoing' + | 'mtokens' + | 'request' + | 'safe_fee' + | 'safe_tokens' + | 'secret' + | 'tokens' + | 'type' + | 'date' + > & { + destination_node?: Types.Maybe< + { __typename?: 'Node' } & { + node?: Types.Maybe< + { __typename?: 'nodeType' } & Pick< + Types.NodeType, + 'alias' + > + >; + } + >; + hops?: Types.Maybe< + Array< + Types.Maybe< + { __typename?: 'Node' } & { + node?: Types.Maybe< + { __typename?: 'nodeType' } & Pick< + Types.NodeType, + 'alias' + > + >; + } + > + > + >; + }) + > + > + >; + } >; }; @@ -21,7 +91,57 @@ export const GetResumeDocument = gql` query GetResume($auth: authType!, $token: String) { getResume(auth: $auth, token: $token) { token - resume + resume { + ... on InvoiceType { + chain_address + confirmed_at + created_at + description + description_hash + expires_at + id + is_canceled + is_confirmed + is_held + is_private + is_push + received + received_mtokens + request + secret + tokens + type + date + } + ... on PaymentType { + created_at + destination + destination_node { + node { + alias + } + } + fee + fee_mtokens + hops { + node { + alias + } + } + id + index + is_confirmed + is_outgoing + mtokens + request + safe_fee + safe_tokens + secret + tokens + type + date + } + } } } `; diff --git a/src/graphql/queries/__generated__/getTimeHealth.generated.tsx b/src/graphql/queries/__generated__/getTimeHealth.generated.tsx new file mode 100644 index 00000000..e11855be --- /dev/null +++ b/src/graphql/queries/__generated__/getTimeHealth.generated.tsx @@ -0,0 +1,114 @@ +import gql from 'graphql-tag'; +import * as ApolloReactCommon from '@apollo/react-common'; +import * as ApolloReactHooks from '@apollo/react-hooks'; +import * as Types from '../../types'; + +export type GetTimeHealthQueryVariables = { + auth: Types.AuthType; +}; + +export type GetTimeHealthQuery = { __typename?: 'Query' } & { + getTimeHealth?: Types.Maybe< + { __typename?: 'channelsTimeHealth' } & Pick< + Types.ChannelsTimeHealth, + 'score' + > & { + channels?: Types.Maybe< + Array< + Types.Maybe< + { __typename?: 'channelTimeHealth' } & Pick< + Types.ChannelTimeHealth, + | 'id' + | 'score' + | 'significant' + | 'monitoredTime' + | 'monitoredUptime' + | 'monitoredDowntime' + > & { + partner?: Types.Maybe< + { __typename?: 'Node' } & { + node?: Types.Maybe< + { __typename?: 'nodeType' } & Pick< + Types.NodeType, + 'alias' + > + >; + } + >; + } + > + > + >; + } + >; +}; + +export const GetTimeHealthDocument = gql` + query GetTimeHealth($auth: authType!) { + getTimeHealth(auth: $auth) { + score + channels { + id + score + significant + monitoredTime + monitoredUptime + monitoredDowntime + partner { + node { + alias + } + } + } + } + } +`; + +/** + * __useGetTimeHealthQuery__ + * + * To run a query within a React component, call `useGetTimeHealthQuery` and pass it any options that fit your needs. + * When your component renders, `useGetTimeHealthQuery` 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 } = useGetTimeHealthQuery({ + * variables: { + * auth: // value for 'auth' + * }, + * }); + */ +export function useGetTimeHealthQuery( + baseOptions?: ApolloReactHooks.QueryHookOptions< + GetTimeHealthQuery, + GetTimeHealthQueryVariables + > +) { + return ApolloReactHooks.useQuery< + GetTimeHealthQuery, + GetTimeHealthQueryVariables + >(GetTimeHealthDocument, baseOptions); +} +export function useGetTimeHealthLazyQuery( + baseOptions?: ApolloReactHooks.LazyQueryHookOptions< + GetTimeHealthQuery, + GetTimeHealthQueryVariables + > +) { + return ApolloReactHooks.useLazyQuery< + GetTimeHealthQuery, + GetTimeHealthQueryVariables + >(GetTimeHealthDocument, baseOptions); +} +export type GetTimeHealthQueryHookResult = ReturnType< + typeof useGetTimeHealthQuery +>; +export type GetTimeHealthLazyQueryHookResult = ReturnType< + typeof useGetTimeHealthLazyQuery +>; +export type GetTimeHealthQueryResult = ApolloReactCommon.QueryResult< + GetTimeHealthQuery, + GetTimeHealthQueryVariables +>; diff --git a/src/graphql/queries/__generated__/getVolumeHealth.generated.tsx b/src/graphql/queries/__generated__/getVolumeHealth.generated.tsx new file mode 100644 index 00000000..cadc00f5 --- /dev/null +++ b/src/graphql/queries/__generated__/getVolumeHealth.generated.tsx @@ -0,0 +1,104 @@ +import gql from 'graphql-tag'; +import * as ApolloReactCommon from '@apollo/react-common'; +import * as ApolloReactHooks from '@apollo/react-hooks'; +import * as Types from '../../types'; + +export type GetVolumeHealthQueryVariables = { + auth: Types.AuthType; +}; + +export type GetVolumeHealthQuery = { __typename?: 'Query' } & { + getVolumeHealth?: Types.Maybe< + { __typename?: 'channelsHealth' } & Pick & { + channels?: Types.Maybe< + Array< + Types.Maybe< + { __typename?: 'channelHealth' } & Pick< + Types.ChannelHealth, + 'id' | 'score' | 'volumeNormalized' | 'averageVolumeNormalized' + > & { + partner?: Types.Maybe< + { __typename?: 'Node' } & { + node?: Types.Maybe< + { __typename?: 'nodeType' } & Pick< + Types.NodeType, + 'alias' + > + >; + } + >; + } + > + > + >; + } + >; +}; + +export const GetVolumeHealthDocument = gql` + query GetVolumeHealth($auth: authType!) { + getVolumeHealth(auth: $auth) { + score + channels { + id + score + volumeNormalized + averageVolumeNormalized + partner { + node { + alias + } + } + } + } + } +`; + +/** + * __useGetVolumeHealthQuery__ + * + * To run a query within a React component, call `useGetVolumeHealthQuery` and pass it any options that fit your needs. + * When your component renders, `useGetVolumeHealthQuery` 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 } = useGetVolumeHealthQuery({ + * variables: { + * auth: // value for 'auth' + * }, + * }); + */ +export function useGetVolumeHealthQuery( + baseOptions?: ApolloReactHooks.QueryHookOptions< + GetVolumeHealthQuery, + GetVolumeHealthQueryVariables + > +) { + return ApolloReactHooks.useQuery< + GetVolumeHealthQuery, + GetVolumeHealthQueryVariables + >(GetVolumeHealthDocument, baseOptions); +} +export function useGetVolumeHealthLazyQuery( + baseOptions?: ApolloReactHooks.LazyQueryHookOptions< + GetVolumeHealthQuery, + GetVolumeHealthQueryVariables + > +) { + return ApolloReactHooks.useLazyQuery< + GetVolumeHealthQuery, + GetVolumeHealthQueryVariables + >(GetVolumeHealthDocument, baseOptions); +} +export type GetVolumeHealthQueryHookResult = ReturnType< + typeof useGetVolumeHealthQuery +>; +export type GetVolumeHealthLazyQueryHookResult = ReturnType< + typeof useGetVolumeHealthLazyQuery +>; +export type GetVolumeHealthQueryResult = ApolloReactCommon.QueryResult< + GetVolumeHealthQuery, + GetVolumeHealthQueryVariables +>; diff --git a/src/graphql/queries/getChannels.ts b/src/graphql/queries/getChannels.ts index b7be6415..60ccb794 100644 --- a/src/graphql/queries/getChannels.ts +++ b/src/graphql/queries/getChannels.ts @@ -26,14 +26,26 @@ export const GET_CHANNELS = gql` transaction_vout unsettled_balance partner_node_info { - alias - capacity - channel_count - color - updated_at - base_fee - fee_rate - cltv_delta + node { + alias + capacity + channel_count + color + updated_at + } + } + partner_fee_info { + channel { + policies { + node { + node { + base_fee + fee_rate + cltv_delta + } + } + } + } } } } diff --git a/src/graphql/queries/getClosedChannels.ts b/src/graphql/queries/getClosedChannels.ts index cd495c14..7ba7842c 100644 --- a/src/graphql/queries/getClosedChannels.ts +++ b/src/graphql/queries/getClosedChannels.ts @@ -18,11 +18,13 @@ export const GET_CLOSED_CHANNELS = gql` transaction_id transaction_vout partner_node_info { - alias - capacity - channel_count - color - updated_at + node { + alias + capacity + channel_count + color + updated_at + } } } } diff --git a/src/graphql/queries/getFeeHealth.ts b/src/graphql/queries/getFeeHealth.ts new file mode 100644 index 00000000..e9fb70fa --- /dev/null +++ b/src/graphql/queries/getFeeHealth.ts @@ -0,0 +1,35 @@ +import { gql } from 'apollo-server-micro'; + +export const GET_FEE_HEALTH = gql` + query GetFeeHealth($auth: authType!) { + getFeeHealth(auth: $auth) { + score + channels { + id + partnerSide { + score + rate + base + rateScore + baseScore + rateOver + baseOver + } + mySide { + score + rate + base + rateScore + baseScore + rateOver + baseOver + } + partner { + node { + alias + } + } + } + } + } +`; diff --git a/src/graphql/queries/getForwards.ts b/src/graphql/queries/getForwards.ts index 5b5c9969..c9167cff 100644 --- a/src/graphql/queries/getForwards.ts +++ b/src/graphql/queries/getForwards.ts @@ -8,13 +8,33 @@ export const GET_FORWARDS = gql` fee fee_mtokens incoming_channel - incoming_alias - incoming_color mtokens outgoing_channel - outgoing_alias - outgoing_color tokens + incoming_channel_info { + channel { + policies { + node { + node { + alias + color + } + } + } + } + } + outgoing_channel_info { + channel { + policies { + node { + node { + alias + color + } + } + } + } + } } token } diff --git a/src/graphql/queries/getLatestVersion.ts b/src/graphql/queries/getLatestVersion.ts new file mode 100644 index 00000000..e5fb801d --- /dev/null +++ b/src/graphql/queries/getLatestVersion.ts @@ -0,0 +1,7 @@ +import gql from 'graphql-tag'; + +export const GET_LATEST_VERSION = gql` + query GetLatestVersion { + getLatestVersion + } +`; diff --git a/src/graphql/queries/getPeers.ts b/src/graphql/queries/getPeers.ts index b7137213..ccc5d2ce 100644 --- a/src/graphql/queries/getPeers.ts +++ b/src/graphql/queries/getPeers.ts @@ -13,11 +13,13 @@ export const GET_PEERS = gql` tokens_received tokens_sent partner_node_info { - alias - capacity - channel_count - color - updated_at + node { + alias + capacity + channel_count + color + updated_at + } } } } diff --git a/src/graphql/queries/getPendingChannels.ts b/src/graphql/queries/getPendingChannels.ts index 10aea19f..0f003508 100644 --- a/src/graphql/queries/getPendingChannels.ts +++ b/src/graphql/queries/getPendingChannels.ts @@ -18,11 +18,13 @@ export const GET_PENDING_CHANNELS = gql` transaction_id transaction_vout partner_node_info { - alias - capacity - channel_count - color - updated_at + node { + alias + capacity + channel_count + color + updated_at + } } } } diff --git a/src/graphql/queries/getResume.ts b/src/graphql/queries/getResume.ts index 5cf659bc..6de4f5f5 100644 --- a/src/graphql/queries/getResume.ts +++ b/src/graphql/queries/getResume.ts @@ -4,7 +4,57 @@ export const GET_RESUME = gql` query GetResume($auth: authType!, $token: String) { getResume(auth: $auth, token: $token) { token - resume + resume { + ... on InvoiceType { + chain_address + confirmed_at + created_at + description + description_hash + expires_at + id + is_canceled + is_confirmed + is_held + is_private + is_push + received + received_mtokens + request + secret + tokens + type + date + } + ... on PaymentType { + created_at + destination + destination_node { + node { + alias + } + } + fee + fee_mtokens + hops { + node { + alias + } + } + id + index + is_confirmed + is_outgoing + mtokens + request + safe_fee + safe_tokens + secret + tokens + type + date + } + } } } `; diff --git a/src/graphql/queries/getTimeHealth.ts b/src/graphql/queries/getTimeHealth.ts new file mode 100644 index 00000000..7eee7451 --- /dev/null +++ b/src/graphql/queries/getTimeHealth.ts @@ -0,0 +1,22 @@ +import { gql } from 'apollo-server-micro'; + +export const GET_TIME_HEALTH = gql` + query GetTimeHealth($auth: authType!) { + getTimeHealth(auth: $auth) { + score + channels { + id + score + significant + monitoredTime + monitoredUptime + monitoredDowntime + partner { + node { + alias + } + } + } + } + } +`; diff --git a/src/graphql/queries/getVolumeHealth.ts b/src/graphql/queries/getVolumeHealth.ts new file mode 100644 index 00000000..074926d0 --- /dev/null +++ b/src/graphql/queries/getVolumeHealth.ts @@ -0,0 +1,20 @@ +import { gql } from 'apollo-server-micro'; + +export const GET_VOLUME_HEALTH = gql` + query GetVolumeHealth($auth: authType!) { + getVolumeHealth(auth: $auth) { + score + channels { + id + score + volumeNormalized + averageVolumeNormalized + partner { + node { + alias + } + } + } + } + } +`; diff --git a/src/graphql/types.ts b/src/graphql/types.ts index bf287503..0ca4aefe 100644 --- a/src/graphql/types.ts +++ b/src/graphql/types.ts @@ -6,12 +6,22 @@ export type Scalars = { Boolean: boolean; Int: number; Float: number; - /** A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. */ DateTime: any; }; +export type AuthType = { + type: Scalars['String']; + id?: Maybe; + host?: Maybe; + macaroon?: Maybe; + cert?: Maybe; +}; + export type Query = { __typename?: 'Query'; + getVolumeHealth?: Maybe; + getTimeHealth?: Maybe; + getFeeHealth?: Maybe; getChannelBalance?: Maybe; getChannels?: Maybe>>; getClosedChannels?: Maybe>>; @@ -21,7 +31,7 @@ export type Query = { getNetworkInfo?: Maybe; getNodeInfo?: Maybe; adminCheck?: Maybe; - getNode?: Maybe; + getNode?: Maybe; decodeRequest?: Maybe; getWalletInfo?: Maybe; getResume?: Maybe; @@ -51,6 +61,19 @@ export type Query = { getServerAccounts?: Maybe>>; getLnPayInfo?: Maybe; getLnPay?: Maybe; + getLatestVersion?: Maybe; +}; + +export type QueryGetVolumeHealthArgs = { + auth: AuthType; +}; + +export type QueryGetTimeHealthArgs = { + auth: AuthType; +}; + +export type QueryGetFeeHealthArgs = { + auth: AuthType; }; export type QueryGetChannelBalanceArgs = { @@ -219,367 +242,6 @@ export type QueryGetLnPayArgs = { amount?: Maybe; }; -export type ChannelBalanceType = { - __typename?: 'channelBalanceType'; - confirmedBalance?: Maybe; - pendingBalance?: Maybe; -}; - -export type AuthType = { - type: Scalars['String']; - id?: Maybe; - host?: Maybe; - macaroon?: Maybe; - cert?: Maybe; -}; - -export type ChannelType = { - __typename?: 'channelType'; - capacity?: Maybe; - commit_transaction_fee?: Maybe; - commit_transaction_weight?: Maybe; - id?: Maybe; - is_active?: Maybe; - is_closing?: Maybe; - is_opening?: Maybe; - is_partner_initiated?: Maybe; - is_private?: Maybe; - is_static_remote_key?: Maybe; - local_balance?: Maybe; - local_reserve?: Maybe; - partner_public_key?: Maybe; - received?: Maybe; - remote_balance?: Maybe; - remote_reserve?: Maybe; - sent?: Maybe; - time_offline?: Maybe; - time_online?: Maybe; - transaction_id?: Maybe; - transaction_vout?: Maybe; - unsettled_balance?: Maybe; - partner_node_info?: Maybe; -}; - -export type PartnerNodeType = { - __typename?: 'partnerNodeType'; - alias?: Maybe; - capacity?: Maybe; - channel_count?: Maybe; - color?: Maybe; - updated_at?: Maybe; - base_fee?: Maybe; - fee_rate?: Maybe; - cltv_delta?: Maybe; -}; - -export type ClosedChannelType = { - __typename?: 'closedChannelType'; - capacity?: Maybe; - close_confirm_height?: Maybe; - close_transaction_id?: Maybe; - final_local_balance?: Maybe; - final_time_locked_balance?: Maybe; - id?: Maybe; - is_breach_close?: Maybe; - is_cooperative_close?: Maybe; - is_funding_cancel?: Maybe; - is_local_force_close?: Maybe; - is_remote_force_close?: Maybe; - partner_public_key?: Maybe; - transaction_id?: Maybe; - transaction_vout?: Maybe; - partner_node_info?: Maybe; -}; - -export type PendingChannelType = { - __typename?: 'pendingChannelType'; - close_transaction_id?: Maybe; - is_active?: Maybe; - is_closing?: Maybe; - is_opening?: Maybe; - local_balance?: Maybe; - local_reserve?: Maybe; - partner_public_key?: Maybe; - received?: Maybe; - remote_balance?: Maybe; - remote_reserve?: Maybe; - sent?: Maybe; - transaction_fee?: Maybe; - transaction_id?: Maybe; - transaction_vout?: Maybe; - partner_node_info?: Maybe; -}; - -export type ChannelFeeType = { - __typename?: 'channelFeeType'; - alias?: Maybe; - color?: Maybe; - baseFee?: Maybe; - feeRate?: Maybe; - transactionId?: Maybe; - transactionVout?: Maybe; - public_key?: Maybe; -}; - -export type ChannelReportType = { - __typename?: 'channelReportType'; - local?: Maybe; - remote?: Maybe; - maxIn?: Maybe; - maxOut?: Maybe; -}; - -export type NetworkInfoType = { - __typename?: 'networkInfoType'; - averageChannelSize?: Maybe; - channelCount?: Maybe; - maxChannelSize?: Maybe; - medianChannelSize?: Maybe; - minChannelSize?: Maybe; - nodeCount?: Maybe; - notRecentlyUpdatedPolicyCount?: Maybe; - totalCapacity?: Maybe; -}; - -export type NodeInfoType = { - __typename?: 'nodeInfoType'; - chains?: Maybe>>; - color?: Maybe; - active_channels_count?: Maybe; - closed_channels_count?: Maybe; - alias?: Maybe; - current_block_hash?: Maybe; - current_block_height?: Maybe; - is_synced_to_chain?: Maybe; - is_synced_to_graph?: Maybe; - latest_block_at?: Maybe; - peers_count?: Maybe; - pending_channels_count?: Maybe; - public_key?: Maybe; - uris?: Maybe>>; - version?: Maybe; -}; - -export type DecodeType = { - __typename?: 'decodeType'; - chain_address?: Maybe; - cltv_delta?: Maybe; - description?: Maybe; - description_hash?: Maybe; - destination?: Maybe; - expires_at?: Maybe; - id?: Maybe; - mtokens?: Maybe; - routes?: Maybe>>; - safe_tokens?: Maybe; - tokens?: Maybe; -}; - -export type DecodeRoutesType = { - __typename?: 'DecodeRoutesType'; - base_fee_mtokens?: Maybe; - channel?: Maybe; - cltv_delta?: Maybe; - fee_rate?: Maybe; - public_key?: Maybe; -}; - -export type WalletInfoType = { - __typename?: 'walletInfoType'; - build_tags?: Maybe>>; - commit_hash?: Maybe; - is_autopilotrpc_enabled?: Maybe; - is_chainrpc_enabled?: Maybe; - is_invoicesrpc_enabled?: Maybe; - is_signrpc_enabled?: Maybe; - is_walletrpc_enabled?: Maybe; - is_watchtowerrpc_enabled?: Maybe; - is_wtclientrpc_enabled?: Maybe; -}; - -export type GetResumeType = { - __typename?: 'getResumeType'; - token?: Maybe; - resume?: Maybe; -}; - -export type GetForwardType = { - __typename?: 'getForwardType'; - token?: Maybe; - forwards?: Maybe>>; -}; - -export type ForwardType = { - __typename?: 'forwardType'; - created_at?: Maybe; - fee?: Maybe; - fee_mtokens?: Maybe; - incoming_channel?: Maybe; - incoming_alias?: Maybe; - incoming_color?: Maybe; - mtokens?: Maybe; - outgoing_channel?: Maybe; - outgoing_alias?: Maybe; - outgoing_color?: Maybe; - tokens?: Maybe; -}; - -export type BitcoinFeeType = { - __typename?: 'bitcoinFeeType'; - fast?: Maybe; - halfHour?: Maybe; - hour?: Maybe; -}; - -export type InOutType = { - __typename?: 'InOutType'; - invoices?: Maybe; - payments?: Maybe; - confirmedInvoices?: Maybe; - unConfirmedInvoices?: Maybe; -}; - -export type PeerType = { - __typename?: 'peerType'; - bytes_received?: Maybe; - bytes_sent?: Maybe; - is_inbound?: Maybe; - is_sync_peer?: Maybe; - ping_time?: Maybe; - public_key?: Maybe; - socket?: Maybe; - tokens_received?: Maybe; - tokens_sent?: Maybe; - partner_node_info?: Maybe; -}; - -export type GetTransactionsType = { - __typename?: 'getTransactionsType'; - block_id?: Maybe; - confirmation_count?: Maybe; - confirmation_height?: Maybe; - created_at?: Maybe; - fee?: Maybe; - id?: Maybe; - output_addresses?: Maybe>>; - tokens?: Maybe; -}; - -export type GetUtxosType = { - __typename?: 'getUtxosType'; - address?: Maybe; - address_format?: Maybe; - confirmation_count?: Maybe; - output_script?: Maybe; - tokens?: Maybe; - transaction_id?: Maybe; - transaction_vout?: Maybe; -}; - -export type HodlOfferType = { - __typename?: 'hodlOfferType'; - id?: Maybe; - version?: Maybe; - asset_code?: Maybe; - searchable?: Maybe; - country?: Maybe; - country_code?: Maybe; - working_now?: Maybe; - side?: Maybe; - title?: Maybe; - description?: Maybe; - currency_code?: Maybe; - price?: Maybe; - min_amount?: Maybe; - max_amount?: Maybe; - first_trade_limit?: Maybe; - fee?: Maybe; - balance?: Maybe; - payment_window_minutes?: Maybe; - confirmations?: Maybe; - payment_method_instructions?: Maybe>>; - trader?: Maybe; -}; - -export type HodlOfferFeeType = { - __typename?: 'hodlOfferFeeType'; - author_fee_rate?: Maybe; -}; - -export type HodlOfferPaymentType = { - __typename?: 'hodlOfferPaymentType'; - id?: Maybe; - version?: Maybe; - payment_method_id?: Maybe; - payment_method_type?: Maybe; - payment_method_name?: Maybe; -}; - -export type HodlOfferTraderType = { - __typename?: 'hodlOfferTraderType'; - login?: Maybe; - online_status?: Maybe; - rating?: Maybe; - trades_count?: Maybe; - url?: Maybe; - verified?: Maybe; - verified_by?: Maybe; - strong_hodler?: Maybe; - country?: Maybe; - country_code?: Maybe; - average_payment_time_minutes?: Maybe; - average_release_time_minutes?: Maybe; - days_since_last_trade?: Maybe; -}; - -export type HodlCountryType = { - __typename?: 'hodlCountryType'; - code?: Maybe; - name?: Maybe; - native_name?: Maybe; - currency_code?: Maybe; - currency_name?: Maybe; -}; - -export type HodlCurrencyType = { - __typename?: 'hodlCurrencyType'; - code?: Maybe; - name?: Maybe; - type?: Maybe; -}; - -export type GetMessagesType = { - __typename?: 'getMessagesType'; - token?: Maybe; - messages?: Maybe>>; -}; - -export type MessagesType = { - __typename?: 'messagesType'; - date?: Maybe; - id?: Maybe; - verified?: Maybe; - contentType?: Maybe; - sender?: Maybe; - alias?: Maybe; - message?: Maybe; - tokens?: Maybe; -}; - -export type ServerAccountType = { - __typename?: 'serverAccountType'; - name: Scalars['String']; - id: Scalars['String']; - type: Scalars['String']; - loggedIn: Scalars['Boolean']; -}; - -export type LnPayInfoType = { - __typename?: 'lnPayInfoType'; - max?: Maybe; - min?: Maybe; -}; - export type Mutation = { __typename?: 'Mutation'; closeChannel?: Maybe; @@ -587,7 +249,7 @@ export type Mutation = { updateFees?: Maybe; parsePayment?: Maybe; pay?: Maybe; - createInvoice?: Maybe; + createInvoice?: Maybe; payViaRoute?: Maybe; createAddress?: Maybe; sendToAddress?: Maybe; @@ -681,18 +343,377 @@ export type MutationLogoutArgs = { type: Scalars['String']; }; +export type NodeType = { + __typename?: 'nodeType'; + alias?: Maybe; + capacity?: Maybe; + channel_count?: Maybe; + color?: Maybe; + updated_at?: Maybe; + base_fee?: Maybe; + fee_rate?: Maybe; + cltv_delta?: Maybe; + public_key?: Maybe; +}; + +export type Node = { + __typename?: 'Node'; + node?: Maybe; +}; + +export type NodeInfoType = { + __typename?: 'nodeInfoType'; + chains?: Maybe>>; + color?: Maybe; + active_channels_count?: Maybe; + closed_channels_count?: Maybe; + alias?: Maybe; + current_block_hash?: Maybe; + current_block_height?: Maybe; + is_synced_to_chain?: Maybe; + is_synced_to_graph?: Maybe; + latest_block_at?: Maybe; + peers_count?: Maybe; + pending_channels_count?: Maybe; + public_key?: Maybe; + uris?: Maybe>>; + version?: Maybe; +}; + +export type ServerAccountType = { + __typename?: 'serverAccountType'; + name: Scalars['String']; + id: Scalars['String']; + type: Scalars['String']; + loggedIn: Scalars['Boolean']; +}; + +export type HodlCountryType = { + __typename?: 'hodlCountryType'; + code?: Maybe; + name?: Maybe; + native_name?: Maybe; + currency_code?: Maybe; + currency_name?: Maybe; +}; + +export type HodlCurrencyType = { + __typename?: 'hodlCurrencyType'; + code?: Maybe; + name?: Maybe; + type?: Maybe; +}; + +export type HodlOfferFeeType = { + __typename?: 'hodlOfferFeeType'; + author_fee_rate?: Maybe; +}; + +export type HodlOfferPaymentType = { + __typename?: 'hodlOfferPaymentType'; + id?: Maybe; + version?: Maybe; + payment_method_id?: Maybe; + payment_method_type?: Maybe; + payment_method_name?: Maybe; +}; + +export type HodlOfferTraderType = { + __typename?: 'hodlOfferTraderType'; + login?: Maybe; + online_status?: Maybe; + rating?: Maybe; + trades_count?: Maybe; + url?: Maybe; + verified?: Maybe; + verified_by?: Maybe; + strong_hodler?: Maybe; + country?: Maybe; + country_code?: Maybe; + average_payment_time_minutes?: Maybe; + average_release_time_minutes?: Maybe; + days_since_last_trade?: Maybe; +}; + +export type HodlOfferType = { + __typename?: 'hodlOfferType'; + id?: Maybe; + version?: Maybe; + asset_code?: Maybe; + searchable?: Maybe; + country?: Maybe; + country_code?: Maybe; + working_now?: Maybe; + side?: Maybe; + title?: Maybe; + description?: Maybe; + currency_code?: Maybe; + price?: Maybe; + min_amount?: Maybe; + max_amount?: Maybe; + first_trade_limit?: Maybe; + fee?: Maybe; + balance?: Maybe; + payment_window_minutes?: Maybe; + confirmations?: Maybe; + payment_method_instructions?: Maybe>>; + trader?: Maybe; +}; + +export type LnPayInfoType = { + __typename?: 'lnPayInfoType'; + max?: Maybe; + min?: Maybe; +}; + +export type BitcoinFeeType = { + __typename?: 'bitcoinFeeType'; + fast?: Maybe; + halfHour?: Maybe; + hour?: Maybe; +}; + +export type PeerType = { + __typename?: 'peerType'; + bytes_received?: Maybe; + bytes_sent?: Maybe; + is_inbound?: Maybe; + is_sync_peer?: Maybe; + ping_time?: Maybe; + public_key?: Maybe; + socket?: Maybe; + tokens_received?: Maybe; + tokens_sent?: Maybe; + partner_node_info?: Maybe; +}; + +export type GetUtxosType = { + __typename?: 'getUtxosType'; + address?: Maybe; + address_format?: Maybe; + confirmation_count?: Maybe; + output_script?: Maybe; + tokens?: Maybe; + transaction_id?: Maybe; + transaction_vout?: Maybe; +}; + +export type SendToType = { + __typename?: 'sendToType'; + confirmationCount?: Maybe; + id?: Maybe; + isConfirmed?: Maybe; + isOutgoing?: Maybe; + tokens?: Maybe; +}; + +export type GetTransactionsType = { + __typename?: 'getTransactionsType'; + block_id?: Maybe; + confirmation_count?: Maybe; + confirmation_height?: Maybe; + created_at?: Maybe; + fee?: Maybe; + id?: Maybe; + output_addresses?: Maybe>>; + tokens?: Maybe; +}; + +export type DecodeType = { + __typename?: 'decodeType'; + chain_address?: Maybe; + cltv_delta?: Maybe; + description?: Maybe; + description_hash?: Maybe; + destination?: Maybe; + expires_at?: Maybe; + id?: Maybe; + mtokens?: Maybe; + routes?: Maybe>>; + safe_tokens?: Maybe; + tokens?: Maybe; +}; + +export type DecodeRoutesType = { + __typename?: 'DecodeRoutesType'; + base_fee_mtokens?: Maybe; + channel?: Maybe; + cltv_delta?: Maybe; + fee_rate?: Maybe; + public_key?: Maybe; +}; + +export type GetMessagesType = { + __typename?: 'getMessagesType'; + token?: Maybe; + messages?: Maybe>>; +}; + +export type MessagesType = { + __typename?: 'messagesType'; + date?: Maybe; + id?: Maybe; + verified?: Maybe; + contentType?: Maybe; + sender?: Maybe; + alias?: Maybe; + message?: Maybe; + tokens?: Maybe; +}; + +export type InOutType = { + __typename?: 'InOutType'; + invoices?: Maybe; + payments?: Maybe; + confirmedInvoices?: Maybe; + unConfirmedInvoices?: Maybe; +}; + +export type PolicyType = { + __typename?: 'policyType'; + base_fee_mtokens?: Maybe; + cltv_delta?: Maybe; + fee_rate?: Maybe; + is_disabled?: Maybe; + max_htlc_mtokens?: Maybe; + min_htlc_mtokens?: Maybe; + public_key: Scalars['String']; + updated_at?: Maybe; + my_node?: Maybe; + node?: Maybe; +}; + +export type SingleChannelType = { + __typename?: 'singleChannelType'; + capacity: Scalars['Int']; + id: Scalars['String']; + policies: Array; + transaction_id: Scalars['String']; + transaction_vout: Scalars['Int']; + updated_at?: Maybe; +}; + +export type Channel = { + __typename?: 'Channel'; + channel?: Maybe; +}; + +export type ChannelFeeType = { + __typename?: 'channelFeeType'; + alias?: Maybe; + color?: Maybe; + baseFee?: Maybe; + feeRate?: Maybe; + transactionId?: Maybe; + transactionVout?: Maybe; + public_key?: Maybe; +}; + +export type ChannelReportType = { + __typename?: 'channelReportType'; + local?: Maybe; + remote?: Maybe; + maxIn?: Maybe; + maxOut?: Maybe; +}; + +export type ChannelBalanceType = { + __typename?: 'channelBalanceType'; + confirmedBalance?: Maybe; + pendingBalance?: Maybe; +}; + +export type ChannelType = { + __typename?: 'channelType'; + capacity?: Maybe; + commit_transaction_fee?: Maybe; + commit_transaction_weight?: Maybe; + id?: Maybe; + is_active?: Maybe; + is_closing?: Maybe; + is_opening?: Maybe; + is_partner_initiated?: Maybe; + is_private?: Maybe; + is_static_remote_key?: Maybe; + local_balance?: Maybe; + local_reserve?: Maybe; + partner_public_key?: Maybe; + received?: Maybe; + remote_balance?: Maybe; + remote_reserve?: Maybe; + sent?: Maybe; + time_offline?: Maybe; + time_online?: Maybe; + transaction_id?: Maybe; + transaction_vout?: Maybe; + unsettled_balance?: Maybe; + partner_node_info?: Maybe; + partner_fee_info?: Maybe; +}; + export type CloseChannelType = { __typename?: 'closeChannelType'; transactionId?: Maybe; transactionOutputIndex?: Maybe; }; +export type ClosedChannelType = { + __typename?: 'closedChannelType'; + capacity?: Maybe; + close_confirm_height?: Maybe; + close_transaction_id?: Maybe; + final_local_balance?: Maybe; + final_time_locked_balance?: Maybe; + id?: Maybe; + is_breach_close?: Maybe; + is_cooperative_close?: Maybe; + is_funding_cancel?: Maybe; + is_local_force_close?: Maybe; + is_remote_force_close?: Maybe; + partner_public_key?: Maybe; + transaction_id?: Maybe; + transaction_vout?: Maybe; + partner_node_info?: Maybe; +}; + export type OpenChannelType = { __typename?: 'openChannelType'; transactionId?: Maybe; transactionOutputIndex?: Maybe; }; +export type PendingChannelType = { + __typename?: 'pendingChannelType'; + close_transaction_id?: Maybe; + is_active?: Maybe; + is_closing?: Maybe; + is_opening?: Maybe; + local_balance?: Maybe; + local_reserve?: Maybe; + partner_public_key?: Maybe; + received?: Maybe; + remote_balance?: Maybe; + remote_reserve?: Maybe; + sent?: Maybe; + transaction_fee?: Maybe; + transaction_id?: Maybe; + transaction_vout?: Maybe; + partner_node_info?: Maybe; +}; + +export type WalletInfoType = { + __typename?: 'walletInfoType'; + build_tags?: Maybe>>; + commit_hash?: Maybe; + is_autopilotrpc_enabled?: Maybe; + is_chainrpc_enabled?: Maybe; + is_invoicesrpc_enabled?: Maybe; + is_signrpc_enabled?: Maybe; + is_walletrpc_enabled?: Maybe; + is_watchtowerrpc_enabled?: Maybe; + is_wtclientrpc_enabled?: Maybe; +}; + export type ParsePaymentType = { __typename?: 'parsePaymentType'; chainAddresses?: Maybe>>; @@ -743,8 +764,8 @@ export type HopsType = { timeout?: Maybe; }; -export type InvoiceType = { - __typename?: 'invoiceType'; +export type NewInvoiceType = { + __typename?: 'newInvoiceType'; chainAddress?: Maybe; createdAt?: Maybe; description?: Maybe; @@ -754,11 +775,143 @@ export type InvoiceType = { tokens?: Maybe; }; -export type SendToType = { - __typename?: 'sendToType'; - confirmationCount?: Maybe; - id?: Maybe; - isConfirmed?: Maybe; - isOutgoing?: Maybe; - tokens?: Maybe; +export type NetworkInfoType = { + __typename?: 'networkInfoType'; + averageChannelSize?: Maybe; + channelCount?: Maybe; + maxChannelSize?: Maybe; + medianChannelSize?: Maybe; + minChannelSize?: Maybe; + nodeCount?: Maybe; + notRecentlyUpdatedPolicyCount?: Maybe; + totalCapacity?: Maybe; +}; + +export type GetForwardType = { + __typename?: 'getForwardType'; + token?: Maybe; + forwards?: Maybe>>; +}; + +export type ForwardType = { + __typename?: 'forwardType'; + created_at?: Maybe; + fee?: Maybe; + fee_mtokens?: Maybe; + incoming_channel?: Maybe; + mtokens?: Maybe; + outgoing_channel?: Maybe; + tokens?: Maybe; + incoming_channel_info?: Maybe; + outgoing_channel_info?: Maybe; +}; + +export type PaymentType = { + __typename?: 'PaymentType'; + created_at: Scalars['String']; + destination: Scalars['String']; + destination_node?: Maybe; + fee: Scalars['Int']; + fee_mtokens: Scalars['String']; + hops?: Maybe>>; + id: Scalars['String']; + index?: Maybe; + is_confirmed: Scalars['Boolean']; + is_outgoing: Scalars['Boolean']; + mtokens: Scalars['String']; + request?: Maybe; + safe_fee: Scalars['Int']; + safe_tokens?: Maybe; + secret: Scalars['String']; + tokens: Scalars['Int']; + type: Scalars['String']; + date: Scalars['String']; +}; + +export type InvoiceType = { + __typename?: 'InvoiceType'; + chain_address?: Maybe; + confirmed_at?: Maybe; + created_at: Scalars['String']; + description: Scalars['String']; + description_hash?: Maybe; + expires_at: Scalars['String']; + id: Scalars['String']; + is_canceled?: Maybe; + is_confirmed: Scalars['Boolean']; + is_held?: Maybe; + is_private: Scalars['Boolean']; + is_push?: Maybe; + received: Scalars['Int']; + received_mtokens: Scalars['String']; + request?: Maybe; + secret: Scalars['String']; + tokens: Scalars['Int']; + type: Scalars['String']; + date: Scalars['String']; +}; + +export type Transaction = InvoiceType | PaymentType; + +export type GetResumeType = { + __typename?: 'getResumeType'; + token?: Maybe; + resume?: Maybe>>; +}; + +export type ChannelHealth = { + __typename?: 'channelHealth'; + id?: Maybe; + score?: Maybe; + volumeNormalized?: Maybe; + averageVolumeNormalized?: Maybe; + partner?: Maybe; +}; + +export type ChannelsHealth = { + __typename?: 'channelsHealth'; + score?: Maybe; + channels?: Maybe>>; +}; + +export type ChannelTimeHealth = { + __typename?: 'channelTimeHealth'; + id?: Maybe; + score?: Maybe; + significant?: Maybe; + monitoredTime?: Maybe; + monitoredUptime?: Maybe; + monitoredDowntime?: Maybe; + partner?: Maybe; +}; + +export type ChannelsTimeHealth = { + __typename?: 'channelsTimeHealth'; + score?: Maybe; + channels?: Maybe>>; +}; + +export type FeeHealth = { + __typename?: 'feeHealth'; + score?: Maybe; + rate?: Maybe; + base?: Maybe; + rateScore?: Maybe; + baseScore?: Maybe; + rateOver?: Maybe; + baseOver?: Maybe; +}; + +export type ChannelFeeHealth = { + __typename?: 'channelFeeHealth'; + id?: Maybe; + partnerSide?: Maybe; + mySide?: Maybe; + partner?: Maybe; +}; + +export type ChannelsFeeHealth = { + __typename?: 'channelsFeeHealth'; + score?: Maybe; + channels?: Maybe>>; }; diff --git a/src/layouts/header/Header.styled.ts b/src/layouts/header/Header.styled.ts index 0553c38a..118befce 100644 --- a/src/layouts/header/Header.styled.ts +++ b/src/layouts/header/Header.styled.ts @@ -1,5 +1,11 @@ import styled, { css } from 'styled-components'; -import { headerTextColor, themeColors, mediaWidths } from '../../styles/Themes'; +import { + headerTextColor, + themeColors, + mediaWidths, + unSelectedNavButton, + homeCompatibleColor, +} from '../../styles/Themes'; import { SingleLine } from '../../components/generic/Styled'; export const HeaderStyle = styled.div` @@ -54,3 +60,31 @@ export const HeaderLine = styled(SingleLine)<{ loggedIn: boolean }>` `} } `; + +export const HeaderButtons = styled.div` + display: flex; + align-items: center; +`; + +interface NavProps { + selected: boolean; +} + +export const HeaderNavButton = styled.div` + padding: 4px; + border-radius: 4px; + background: ${({ selected }) => selected && homeCompatibleColor}; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + text-decoration: none; + margin: 0 4px; + color: ${({ selected }) => + selected ? headerTextColor : unSelectedNavButton}; + + &:hover { + color: ${headerTextColor}; + background: ${homeCompatibleColor}; + } +`; diff --git a/src/layouts/header/Header.tsx b/src/layouts/header/Header.tsx index 71d58deb..f8362961 100644 --- a/src/layouts/header/Header.tsx +++ b/src/layouts/header/Header.tsx @@ -1,6 +1,15 @@ import React, { useState } from 'react'; -import { Cpu, Menu, X, Circle } from 'react-feather'; +import { + Cpu, + Menu, + X, + CreditCard, + MessageCircle, + Settings, + Home, +} from 'react-feather'; import { useTransition, animated } from 'react-spring'; +import { useRouter } from 'next/router'; import { headerColor, headerTextColor } from '../../styles/Themes'; import { SingleLine } from '../../components/generic/Styled'; import { BurgerMenu } from '../../components/burgerMenu/BurgerMenu'; @@ -14,11 +23,19 @@ import { HeaderLine, HeaderTitle, IconPadding, + HeaderButtons, + HeaderNavButton, } from './Header.styled'; +const HOME = '/home'; +const TRADER = '/trading'; +const CHAT = '/chat'; +const SETTINGS = '/settings'; + export const Header = () => { + const { pathname } = useRouter(); const [open, setOpen] = useState(false); - const { syncedToChain, connected } = useStatusState(); + const { connected } = useStatusState(); const transitions = useTransition(open, null, { from: { position: 'absolute', opacity: 0 }, @@ -26,6 +43,14 @@ export const Header = () => { leave: { opacity: 0 }, }); + const renderNavButton = (link: string, NavIcon: any) => ( + + + + + + ); + const renderLoggedIn = () => ( <> @@ -44,11 +69,12 @@ export const Header = () => { - + + {renderNavButton(HOME, Home)} + {renderNavButton(TRADER, CreditCard)} + {renderNavButton(CHAT, MessageCircle)} + {renderNavButton(SETTINGS, Settings)} + ); diff --git a/src/layouts/navigation/Navigation.tsx b/src/layouts/navigation/Navigation.tsx index 8b339e02..6f665c74 100644 --- a/src/layouts/navigation/Navigation.tsx +++ b/src/layouts/navigation/Navigation.tsx @@ -13,6 +13,7 @@ import { Users, CreditCard, MessageCircle, + BarChart2, } from 'react-feather'; import { useRouter } from 'next/router'; import { @@ -119,10 +120,11 @@ const TRANS = '/transactions'; const FORWARDS = '/forwards'; const CHAIN_TRANS = '/chain'; const TOOLS = '/tools'; -const SETTINGS = '/settings'; const FEES = '/fees'; +const STATS = '/stats'; const TRADER = '/trading'; const CHAT = '/chat'; +const SETTINGS = '/settings'; interface NavigationProps { isBurger?: boolean; @@ -171,9 +173,7 @@ export const Navigation = ({ isBurger, setOpen }: NavigationProps) => { {renderNavButton('Forwards', FORWARDS, GitPullRequest, sidebar)} {renderNavButton('Chain', CHAIN_TRANS, LinkIcon, sidebar)} {renderNavButton('Tools', TOOLS, Shield, sidebar)} - {renderNavButton('P2P Trading', TRADER, CreditCard, sidebar)} - {renderNavButton('Chat', CHAT, MessageCircle, sidebar)} - {renderNavButton('Settings', SETTINGS, Settings, sidebar)} + {renderNavButton('Stats', STATS, BarChart2, sidebar)} ); @@ -188,6 +188,7 @@ export const Navigation = ({ isBurger, setOpen }: NavigationProps) => { {renderBurgerNav('Forwards', FORWARDS, GitPullRequest)} {renderBurgerNav('Chain', CHAIN_TRANS, LinkIcon)} {renderBurgerNav('Tools', TOOLS, Shield)} + {renderBurgerNav('Stats', STATS, BarChart2)} {renderBurgerNav('Trading', TRADER, CreditCard)} {renderBurgerNav('Chat', CHAT, MessageCircle)} {renderBurgerNav('Settings', SETTINGS, Settings)} diff --git a/src/views/channels/channels/ChannelCard.tsx b/src/views/channels/channels/ChannelCard.tsx index ce9719aa..4b8328e8 100644 --- a/src/views/channels/channels/ChannelCard.tsx +++ b/src/views/channels/channels/ChannelCard.tsx @@ -120,7 +120,7 @@ export const ChannelCard: React.FC = ({ base_fee, fee_rate, cltv_delta, - } = partner_node_info; + } = partner_node_info?.node || {}; const formatBalance = format({ amount: capacity }); const formatLocal = format({ amount: local_balance }); diff --git a/src/views/channels/channels/Channels.tsx b/src/views/channels/channels/Channels.tsx index a7000961..44244887 100644 --- a/src/views/channels/channels/Channels.tsx +++ b/src/views/channels/channels/Channels.tsx @@ -35,9 +35,19 @@ export const Channels: React.FC = () => { for (let i = 0; i < data.getChannels.length; i++) { const channel = data.getChannels[i]; - const { local_balance, remote_balance, partner_node_info = {} } = channel; + const { + local_balance, + remote_balance, + partner_node_info = {}, + partner_fee_info = {}, + } = channel; + + const { capacity, channel_count } = partner_node_info?.node || {}; + const { + base_fee, + fee_rate, + } = partner_fee_info?.channel?.policies?.[0]?.node.node; - const { capacity, channel_count, base_fee, fee_rate } = partner_node_info; const partner = Number(capacity) || 0; const channels = Number(channel_count) || 0; diff --git a/src/views/channels/closedChannels/ClosedCard.tsx b/src/views/channels/closedChannels/ClosedCard.tsx index 275f4772..a2d7452b 100644 --- a/src/views/channels/closedChannels/ClosedCard.tsx +++ b/src/views/channels/closedChannels/ClosedCard.tsx @@ -58,7 +58,7 @@ export const ClosedCard = ({ partner_node_info, } = channelInfo; - const { alias, color: nodeColor } = partner_node_info; + const { alias, color: nodeColor } = partner_node_info?.node || {}; const formatCapacity = ; diff --git a/src/views/channels/pendingChannels/PendingCard.tsx b/src/views/channels/pendingChannels/PendingCard.tsx index ccb62d58..89c34658 100644 --- a/src/views/channels/pendingChannels/PendingCard.tsx +++ b/src/views/channels/pendingChannels/PendingCard.tsx @@ -68,13 +68,8 @@ export const PendingCard = ({ partner_node_info, } = channelInfo; - const { - alias, - capacity, - channel_count, - color: nodeColor, - updated_at, - } = partner_node_info; + const { alias, capacity, channel_count, color: nodeColor, updated_at } = + partner_node_info?.node || {}; const formatBalance = format({ amount: local_balance + remote_balance }); const formatLocal = format({ amount: local_balance }); diff --git a/src/views/forwards/ForwardsCard.tsx b/src/views/forwards/ForwardsCard.tsx index c21932d5..bc535f61 100644 --- a/src/views/forwards/ForwardsCard.tsx +++ b/src/views/forwards/ForwardsCard.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { ForwardType } from 'src/graphql/types'; import { Separation, SubCard, @@ -15,7 +16,7 @@ import { import { Price } from '../../components/price/Price'; interface ForwardCardProps { - forward: any; + forward: ForwardType; index: number; setIndexOpen: (index: number) => void; indexOpen: number; @@ -32,10 +33,10 @@ export const ForwardCard = ({ fee, fee_mtokens, incoming_channel, - incoming_alias, outgoing_channel, - outgoing_alias, tokens, + incoming_channel_info, + outgoing_channel_info, } = forward; const formatAmount = ; @@ -67,8 +68,14 @@ export const ForwardCard = ({ - {renderLine('Incoming:', incoming_alias)} - {renderLine('Outgoing:', outgoing_alias)} + {renderLine( + 'Incoming:', + incoming_channel_info?.channel?.policies?.[0]?.node?.node?.alias + )} + {renderLine( + 'Outgoing:', + outgoing_channel_info?.channel?.policies?.[0]?.node?.node?.alias + )} diff --git a/src/views/peers/PeersCard.tsx b/src/views/peers/PeersCard.tsx index 6579d192..87ab87de 100644 --- a/src/views/peers/PeersCard.tsx +++ b/src/views/peers/PeersCard.tsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import styled from 'styled-components'; import { ArrowDown, ArrowUp } from 'react-feather'; import ReactTooltip from 'react-tooltip'; +import { PeerType } from 'src/graphql/types'; import { SubCard, Separation, @@ -43,7 +44,7 @@ const getSymbol = (status: boolean) => { }; interface PeerProps { - peer: any; + peer: PeerType; index: number; setIndexOpen: (index: number) => void; indexOpen: number; @@ -79,13 +80,8 @@ export const PeersCard = ({ const formatReceived = format({ amount: tokens_received }); const formatSent = format({ amount: tokens_sent }); - const { - alias, - capacity, - channel_count, - color, - updated_at, - } = partner_node_info; + const { alias, capacity, channel_count, color, updated_at } = + partner_node_info?.node || {}; const handleClick = () => { if (indexOpen === index) { diff --git a/src/views/stats/FeeStats.tsx b/src/views/stats/FeeStats.tsx new file mode 100644 index 00000000..b855fa34 --- /dev/null +++ b/src/views/stats/FeeStats.tsx @@ -0,0 +1,139 @@ +import * as React from 'react'; +import { useAccountState } from 'src/context/AccountContext'; +import { useGetFeeHealthQuery } from 'src/graphql/queries/__generated__/getFeeHealth.generated'; +import { + SubCard, + SingleLine, + DarkSubTitle, + Separation, +} from 'src/components/generic/Styled'; +import { ChannelFeeHealth } from 'src/graphql/types'; +import { sortBy } from 'underscore'; +import { renderLine } from 'src/components/generic/helpers'; +import { useStatsDispatch } from './context'; +import { ScoreColumn, ScoreLine, Clickable, WarningText } from './styles'; +import { StatWrapper } from './Wrapper'; +import { getIcon, getFeeMessage, getProgressColor } from './helpers'; + +type FeeStatCardProps = { + channel: ChannelFeeHealth; + index: number; + open: boolean; + openSet: (index: number) => void; + myStats?: boolean; +}; + +const FeeStatCard = ({ + channel, + myStats, + open, + openSet, + index, +}: FeeStatCardProps) => { + const renderContent = () => { + const stats = myStats ? channel.mySide : channel.partnerSide; + const { score } = stats; + + return ( + + Score + {score} + {getIcon(score)} + + ); + }; + + const renderDetails = () => { + const stats = myStats ? channel.mySide : channel.partnerSide; + const { rate, base, rateScore, baseScore, rateOver, baseOver } = stats; + + const message = getFeeMessage(rateScore, rateOver); + const baseMessage = getFeeMessage(Number(baseScore), baseOver, true); + return ( + <> + + + {message} + + + {baseMessage} + + {renderLine('Fee Rate (ppm):', rate)} + {renderLine('Base Fee (sats):', base)} + + ); + }; + + return ( + + openSet(open ? 0 : index)}> + + {channel?.partner?.node?.alias} + {renderContent()} + + + {open && renderDetails()} + + ); +}; + +export const FeeStats = () => { + const [open, openSet] = React.useState(0); + const [openTwo, openTwoSet] = React.useState(0); + const dispatch = useStatsDispatch(); + const { auth } = useAccountState(); + const { data, loading } = useGetFeeHealthQuery({ + skip: !auth, + variables: { auth }, + }); + + React.useEffect(() => { + if (data && data.getFeeHealth) { + dispatch({ + type: 'change', + state: { feeScore: data.getFeeHealth.score }, + }); + } + }, [data, dispatch]); + + if (loading || !data || !data.getFeeHealth) { + return null; + } + + const sortedArray = sortBy( + data.getFeeHealth.channels, + c => c.partnerSide.score + ); + const sortedArrayMyStats = sortBy( + data.getFeeHealth.channels, + c => c.mySide.score + ); + + return ( + <> + + {sortedArray.map((channel, index) => ( + + ))} + + + {sortedArrayMyStats.map((channel, index) => ( + + ))} + + + ); +}; diff --git a/src/views/stats/StatResume.tsx b/src/views/stats/StatResume.tsx new file mode 100644 index 00000000..51dbdcad --- /dev/null +++ b/src/views/stats/StatResume.tsx @@ -0,0 +1,85 @@ +import * as React from 'react'; +import { + CircularProgressbarWithChildren, + buildStyles, +} from 'react-circular-progressbar'; +import styled from 'styled-components'; +import { DarkSubTitle } from 'src/components/generic/Styled'; +import { mediaWidths } from 'src/styles/Themes'; +import { useStatsState } from './context'; +import { StatsTitle } from './styles'; +import { getProgressColor } from './helpers'; + +const ProgressRow = styled.div` + display: flex; + justify-content: space-around; + margin: 32px 0; + + @media (${mediaWidths.mobile}) { + margin: 16px 0; + } +`; + +const ProgressCard = styled.div` + width: 20%; + + @media (${mediaWidths.mobile}) { + width: 30%; + } +`; + +const ScoreTitle = styled.div` + font-size: 32px; + + @media (${mediaWidths.mobile}) { + font-size: 18px; + } +`; + +export const StatResume = () => { + const { volumeScore, timeScore, feeScore } = useStatsState(); + + return ( + <> + Node Statistics + + + + Volume + {volumeScore} + + + + + Time + {timeScore} + + + + + Fee + {feeScore} + + + + + ); +}; diff --git a/src/views/stats/TimeStats.tsx b/src/views/stats/TimeStats.tsx new file mode 100644 index 00000000..4273970b --- /dev/null +++ b/src/views/stats/TimeStats.tsx @@ -0,0 +1,103 @@ +import * as React from 'react'; +import { useAccountState } from 'src/context/AccountContext'; +import { useGetTimeHealthQuery } from 'src/graphql/queries/__generated__/getTimeHealth.generated'; +import { + SubCard, + SingleLine, + SubTitle, + DarkSubTitle, + Separation, +} from 'src/components/generic/Styled'; +import { ChannelTimeHealth } from 'src/graphql/types'; +import { sortBy } from 'underscore'; +import { renderLine } from 'src/components/generic/helpers'; +import { formatSeconds } from 'src/utils/helpers'; +import { useStatsDispatch } from './context'; +import { ScoreLine, WarningText, Clickable } from './styles'; +import { StatWrapper } from './Wrapper'; +import { getIcon, getTimeMessage, getProgressColor } from './helpers'; + +type TimeStatCardProps = { + channel: ChannelTimeHealth; + index: number; + open: boolean; + openSet: (index: number) => void; +}; + +const TimeStatCard = ({ channel, open, openSet, index }: TimeStatCardProps) => { + const message = getTimeMessage(channel.score); + const renderContent = () => ( + <> + + {!channel.significant && ( + + Needs to be monitored for a longer period to give significant + statistics. + + )} + + {message} + + {renderLine('Monitored time:', formatSeconds(channel.monitoredTime))} + {renderLine('Monitored up time:', formatSeconds(channel.monitoredUptime))} + {renderLine( + 'Monitored down time:', + formatSeconds(channel.monitoredDowntime) + )} + + ); + return ( + + openSet(open ? 0 : index)}> + + {channel?.partner?.node?.alias} + + Score + {channel.score} + {getIcon(channel.score, !channel.significant)} + + + + {open && renderContent()} + + ); +}; + +export const TimeStats = () => { + const [open, openSet] = React.useState(0); + const dispatch = useStatsDispatch(); + const { auth } = useAccountState(); + const { data, loading } = useGetTimeHealthQuery({ + skip: !auth, + variables: { auth }, + }); + + React.useEffect(() => { + if (data && data.getTimeHealth) { + dispatch({ + type: 'change', + state: { timeScore: data.getTimeHealth.score }, + }); + } + }, [data, dispatch]); + + if (loading || !data || !data.getTimeHealth) { + return null; + } + + const sortedArray = sortBy(data.getTimeHealth.channels, 'score'); + + return ( + + {sortedArray.map((channel, index) => ( + + ))} + + ); +}; diff --git a/src/views/stats/VolumeStats.tsx b/src/views/stats/VolumeStats.tsx new file mode 100644 index 00000000..41d280c6 --- /dev/null +++ b/src/views/stats/VolumeStats.tsx @@ -0,0 +1,100 @@ +import * as React from 'react'; +import { useGetVolumeHealthQuery } from 'src/graphql/queries/__generated__/getVolumeHealth.generated'; +import { useAccountState } from 'src/context/AccountContext'; +import { + SubCard, + SingleLine, + DarkSubTitle, + SubTitle, + Separation, +} from 'src/components/generic/Styled'; +import { sortBy } from 'underscore'; +import { renderLine } from 'src/components/generic/helpers'; +import { ChannelHealth } from 'src/graphql/types'; +import { useStatsDispatch } from './context'; +import { ScoreLine, Clickable, WarningText } from './styles'; +import { StatWrapper } from './Wrapper'; +import { getIcon, getVolumeMessage, getProgressColor } from './helpers'; + +type VolumeStatCardProps = { + channel: ChannelHealth; + index: number; + open: boolean; + openSet: (index: number) => void; +}; + +const VolumeStatCard = ({ + channel, + open, + openSet, + index, +}: VolumeStatCardProps) => { + const message = getVolumeMessage(channel.score); + const renderContent = () => ( + <> + + + {message} + + {renderLine('Volume (sats/block):', channel.volumeNormalized)} + {renderLine( + 'Average Volume (sats/block):', + channel.averageVolumeNormalized + )} + + ); + return ( + + openSet(open ? 0 : index)}> + + {channel?.partner?.node?.alias} + + {'Score'} + {channel.score} + {getIcon(channel.score)} + + + + {open && renderContent()} + + ); +}; + +export const VolumeStats = () => { + const [open, openSet] = React.useState(0); + const dispatch = useStatsDispatch(); + const { auth } = useAccountState(); + const { data, loading } = useGetVolumeHealthQuery({ + skip: !auth, + variables: { auth }, + }); + + React.useEffect(() => { + if (data && data.getVolumeHealth) { + dispatch({ + type: 'change', + state: { volumeScore: data.getVolumeHealth.score }, + }); + } + }, [data, dispatch]); + + if (loading || !data || !data.getVolumeHealth) { + return null; + } + + const sortedArray = sortBy(data.getVolumeHealth.channels, 'score'); + + return ( + + {sortedArray.map((channel, index) => ( + + ))} + + ); +}; diff --git a/src/views/stats/Wrapper.tsx b/src/views/stats/Wrapper.tsx new file mode 100644 index 00000000..23283c82 --- /dev/null +++ b/src/views/stats/Wrapper.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; +import { Card, SubTitle } from 'src/components/generic/Styled'; +import { ChevronDown, ChevronUp } from 'react-feather'; +import { StatHeaderLine } from './styles'; + +type StatWrapperProps = { + title: string; +}; + +export const StatWrapper: React.FC = ({ + children, + title, +}) => { + const [open, openSet] = React.useState(false); + + return ( + + openSet(p => !p)}> + {title} + {open ? : } + + {open && children} + + ); +}; diff --git a/src/views/stats/context/index.tsx b/src/views/stats/context/index.tsx new file mode 100644 index 00000000..8e2abf7b --- /dev/null +++ b/src/views/stats/context/index.tsx @@ -0,0 +1,66 @@ +import React, { createContext, useContext, useReducer } from 'react'; + +type State = { + volumeScore: number | null; + timeScore: number | null; + feeScore: number | null; +}; + +type ChangeState = { + volumeScore?: number; + timeScore?: number; + feeScore?: number; +}; + +type ActionType = { + type: 'change'; + state?: ChangeState; +}; + +type Dispatch = (action: ActionType) => void; + +const StateContext = createContext(undefined); +const DispatchContext = createContext(undefined); + +const initialState = { + volumeScore: 0, + timeScore: 0, + feeScore: 0, +}; + +const stateReducer = (state: State, action: ActionType): State => { + switch (action.type) { + case 'change': + return { ...state, ...action.state }; + default: + return state; + } +}; + +const StatsProvider = ({ children }) => { + const [state, dispatch] = useReducer(stateReducer, initialState); + + return ( + + {children} + + ); +}; + +const useStatsState = () => { + const context = useContext(StateContext); + if (context === undefined) { + throw new Error('useStatsState must be used within a StatsProvider'); + } + return context; +}; + +const useStatsDispatch = () => { + const context = useContext(DispatchContext); + if (context === undefined) { + throw new Error('useStatsDispatch must be used within a StatsProvider'); + } + return context; +}; + +export { StatsProvider, useStatsState, useStatsDispatch }; diff --git a/src/views/stats/helpers.tsx b/src/views/stats/helpers.tsx new file mode 100644 index 00000000..a4294ab3 --- /dev/null +++ b/src/views/stats/helpers.tsx @@ -0,0 +1,129 @@ +import * as React from 'react'; +import { chartColors } from 'src/styles/Themes'; +import { + CheckCircle, + AlertCircle, + XCircle, + AlertTriangle, +} from 'react-feather'; + +export const getProgressColor = (score: number): string => { + switch (true) { + case score > 90: + return chartColors.green; + case score > 75: + return chartColors.darkyellow; + case score > 60: + return chartColors.orange; + case score > 50: + return chartColors.orange2; + default: + return chartColors.red; + } +}; + +export const getIcon = ( + score: number, + notSignificant?: boolean +): JSX.Element => { + if (notSignificant) { + return ; + } + switch (true) { + case score > 90: + return ; + case score > 75: + return ; + case score > 60: + return ; + case score > 50: + return ; + default: + return ; + } +}; + +export const getFeeMessage = ( + score: number, + isOver: boolean, + isBase?: boolean +): string => { + let message = ''; + const ending = isBase ? 'base fees' : 'ppm fees'; + switch (true) { + case score > 90: + message = 'This channel has very good'; + break; + case score > 75: + message = 'This channel has good'; + break; + case score > 60 && isOver: + message = 'This channel has above average high'; + break; + case score > 60: + message = 'This channel could have higher'; + break; + case score > 50 && isOver: + message = 'This channel has high'; + break; + case score > 50: + message = 'This channel has too low'; + break; + case isOver: + message = 'This channel has very high'; + break; + default: + message = 'This channel has very low'; + break; + } + return `${message} ${ending}`; +}; + +export const getTimeMessage = (score: number): string => { + let message = ''; + switch (true) { + case score > 90: + message = 'This channel has very good uptime'; + break; + case score > 75: + message = 'This channel has good uptime'; + break; + case score > 60: + message = 'This channel has average uptime'; + break; + case score > 50: + message = 'This channel has below average uptime'; + break; + default: + message = 'This channel has very bad uptime'; + break; + } + return message; +}; + +export const getVolumeMessage = (score: number): string => { + let message = ''; + switch (true) { + case score > 100: + message = `This channel moves ${ + score - 100 + }% more volume than the average from all your channels`; + break; + case score > 90: + message = 'This channel moves very good volume'; + break; + case score > 75: + message = 'This channel moves good volume'; + break; + case score > 60: + message = 'This channel moves average volume'; + break; + case score > 50: + message = 'This channel moves below average volume'; + break; + default: + message = 'This channel moves very low volume'; + break; + } + return message; +}; diff --git a/src/views/stats/styles.tsx b/src/views/stats/styles.tsx new file mode 100644 index 00000000..e5fe7689 --- /dev/null +++ b/src/views/stats/styles.tsx @@ -0,0 +1,48 @@ +import styled from 'styled-components'; +import { DarkSubTitle } from 'src/components/generic/Styled'; +import { chartColors } from 'src/styles/Themes'; + +export const ScoreColumn = styled.div` + display: flex; + flex-direction: column; +`; + +export const ScoreLine = styled.div` + display: flex; + justify-content: space-between; + width: 160px; +`; + +type StatHeaderProps = { + isOpen?: boolean; +}; + +export const StatHeaderLine = styled.div` + cursor: pointer; + display: flex; + padding: 8px 0 16px; + margin-bottom: ${({ isOpen }) => (isOpen ? 0 : '-8px')}; + justify-content: space-between; + align-items: center; +`; + +export const StatsTitle = styled.div` + font-size: 24px; + width: 100%; + text-align: center; +`; + +type WarningProps = { + warningColor?: string; +}; + +export const WarningText = styled(DarkSubTitle)` + width: 100%; + text-align: center; + color: ${({ warningColor }) => + warningColor ? warningColor : chartColors.orange}; +`; + +export const Clickable = styled.div` + cursor: pointer; +`; diff --git a/src/views/transactions/InvoiceCard.tsx b/src/views/transactions/InvoiceCard.tsx index 436b2d72..59f947a0 100644 --- a/src/views/transactions/InvoiceCard.tsx +++ b/src/views/transactions/InvoiceCard.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { InvoiceType } from 'src/graphql/types'; import { Separation, SubCard, @@ -20,7 +21,7 @@ import { import { Price } from '../../components/price/Price'; interface InvoiceCardProps { - invoice: any; + invoice: InvoiceType; index: number; setIndexOpen: (index: number) => void; indexOpen: number; @@ -33,25 +34,20 @@ export const InvoiceCard = ({ indexOpen, }: InvoiceCardProps) => { const { - date, + chain_address, confirmed_at, created_at, description, - expires_at, - is_confirmed, - // received, - tokens, - chain_address, description_hash, + expires_at, id, is_canceled, + is_confirmed, is_held, - is_outgoing, is_private, - // payments, - // received_mtokens, - // request, secret, + tokens, + date, } = invoice; const formatAmount = ; @@ -86,7 +82,6 @@ export const InvoiceCard = ({ {renderLine('Description Hash:', description_hash)} {renderLine('Is Canceled:', is_canceled)} {renderLine('Is Held:', is_held)} - {renderLine('Is Outgoing:', is_outgoing)} {renderLine('Is Private:', is_private)} {renderLine('Secret:', secret)} diff --git a/src/views/transactions/PaymentsCards.tsx b/src/views/transactions/PaymentsCards.tsx index c8ed58cc..261b997c 100644 --- a/src/views/transactions/PaymentsCards.tsx +++ b/src/views/transactions/PaymentsCards.tsx @@ -1,5 +1,6 @@ import React from 'react'; import styled from 'styled-components'; +import { PaymentType } from 'src/graphql/types'; import { Separation, SubCard, @@ -21,7 +22,7 @@ import { import { Price } from '../../components/price/Price'; interface PaymentsCardProps { - payment: any; + payment: PaymentType; index: number; setIndexOpen: (index: number) => void; indexOpen: number; @@ -38,21 +39,23 @@ export const PaymentsCard = ({ indexOpen, }: PaymentsCardProps) => { const { - alias, - date, created_at, destination, + destination_node, fee, fee_mtokens, hops, - is_confirmed, - tokens, id, + is_confirmed, is_outgoing, mtokens, secret, + tokens, + date, } = payment; + const alias = destination_node?.node?.alias; + const formatAmount = ; const formatFee = ; @@ -72,12 +75,15 @@ export const PaymentsCard = ({ 'Created:', `${getDateDif(created_at)} ago (${getFormatDate(created_at)})` )} - {renderLine('Destination Node:', getNodeLink(destination))} + {renderLine('Destination Node:', getNodeLink(destination, alias))} {renderLine('Fee:', formatFee)} {renderLine('Fee msats:', `${fee_mtokens} millisats`)} {renderLine('Hops:', hops.length)} - {hops.map((hop: any, index: number) => - renderLine(`Hop ${index + 1}:`, hop) + {hops.map((hop, index: number) => + renderLine( + `Hop ${index + 1}:`, + getNodeLink(destination, hop.node.alias) + ) )} {renderLine('Id:', id)} {renderLine('Is Outgoing:', is_outgoing ? 'true' : 'false')}