feat: stats view (#54)

* 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 <apotdevincab@gmail.com>
This commit is contained in:
Anthony Potdevin 2020-06-05 18:50:10 +02:00 committed by GitHub
parent f80492b80a
commit 009195c86f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
85 changed files with 3320 additions and 851 deletions

View file

@ -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)

View file

@ -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/:

View file

@ -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({

41
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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 });

View file

@ -65,7 +65,7 @@ const ForwardsView = () => {
</CardTitle>
{data.getForwards.forwards.length <= 0 && renderNoForwards()}
<Card mobileCardPadding={'0'} mobileNoBackground={true}>
{data.getForwards.forwards.map((forward: any, index: number) => (
{data.getForwards.forwards.map((forward, index: number) => (
<ForwardCard
forward={forward}
key={index}

View file

@ -1,6 +1,7 @@
import React from 'react';
import { GridWrapper } from 'src/components/gridWrapper/GridWrapper';
import { withApollo } from 'config/client';
import { Version } from 'src/components/version/Version';
import { NetworkInfo } from '../src/views/home/networkInfo/NetworkInfo';
import { AccountInfo } from '../src/views/home/account/AccountInfo';
import { QuickActions } from '../src/views/home/quickActions/QuickActions';
@ -13,6 +14,7 @@ import { NodeBar } from '../src/components/nodeInfo/NodeBar';
const HomeView = () => {
return (
<>
<Version />
<AccountInfo />
<NodeBar />
<ConnectCard />

View file

@ -35,7 +35,7 @@ const SettingsView = () => {
};
const Wrapped = () => (
<GridWrapper>
<GridWrapper noNavigation={true}>
<SettingsView />
</GridWrapper>
);

40
pages/stats.tsx Normal file
View file

@ -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 (
<>
<StatResume />
<VolumeStats />
<TimeStats />
<FeeStats />
</>
);
};
const Wrapped = () => (
<GridWrapper>
<StatsProvider>
<StatsView />
</StatsProvider>
</GridWrapper>
);
export default withApollo(Wrapped);

View file

@ -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 <LoadingCard title={'Transactions'} />;
}
const resumeList = JSON.parse(data.getResume.resume);
const resumeList = data.getResume.resume;
return (
<>
@ -75,10 +74,7 @@ const TransactionsView = () => {
<ColorButton
fullWidth={true}
withMargin={'16px 0 0'}
loading={fetching}
disabled={fetching}
onClick={() => {
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',
},
};

26
scripts/updateToLatest.sh Normal file
View file

@ -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

View file

@ -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) {

View file

@ -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 };
},
},
};

View file

@ -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 },
}));
};

View file

@ -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,
},
}));
};

View file

@ -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,
},
}));
};

View file

@ -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
}
`;

View file

@ -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;
},
},
};

View file

@ -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)) };
};

View file

@ -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,
},
};

View file

@ -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,
};
};

View file

@ -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,
};
};

View file

@ -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 };
};

View file

@ -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]
}
`;

View file

@ -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 });

View file

@ -47,7 +47,7 @@ export const invoiceTypes = gql`
timeout: Int
}
type invoiceType {
type newInvoiceType {
chainAddress: String
createdAt: DateTime
description: String

View file

@ -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');
}

View file

@ -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 };
},
},
};

View file

@ -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

View file

@ -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: {

View file

@ -11,6 +11,6 @@ export const peerTypes = gql`
socket: String
tokens_received: Int
tokens_sent: Int
partner_node_info: nodeType
partner_node_info: Node
}
`;

View file

@ -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;
},
},
};

View file

@ -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]
}
`;

View file

@ -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(

View file

@ -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',
};

View file

@ -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;
}
`;

View file

@ -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 (
<SmallLink href={link} target="_blank">
{shorten(publicKey)}
</SmallLink>
<>
<SmallLink href={link} target="_blank">
{alias ? alias : shorten(publicKey)}
</SmallLink>
<CopyToClipboard text={publicKey} onCopy={() => toast.success('Copied')}>
<CopyIcon>
<Copy size={12} />
</CopyIcon>
</CopyToClipboard>
</>
);
};

View file

@ -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<GridProps>`
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<GridProps> = ({
children,
noNavigation,
}) => {
const { hasAccount, auth } = useAccountState();
const renderContent = () => {
if (hasAccount === 'false') {
@ -36,12 +48,12 @@ export const GridWrapper: React.FC = ({ children }) => {
};
return (
<Section padding={'16px 0 32px'}>
<Container>
<Container noNavigation={noNavigation}>
<ServerAccounts />
<BitcoinPrice />
<BitcoinFees />
{auth && <StatusCheck />}
<Navigation />
{!noNavigation && <Navigation />}
<ContentStyle>{renderContent()}</ContentStyle>
</Container>
</Section>

View file

@ -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 (
<CorrectLink href={href} {...props}>
<CorrectLink
href={href}
{...props}
{...(newTab && { target: '_blank', rel: 'noreferrer' })}
>
{children}
</CorrectLink>
);

View file

@ -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 (
<Link href={appUrls.update} newTab={true}>
<VersionBox>{`Version ${githubVersion} is available. You are on version ${npmVersion}`}</VersionBox>
</Link>
);
};

View file

@ -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';

View file

@ -0,0 +1,18 @@
{
"__schema": {
"types": [
{
"kind": "UNION",
"name": "Transaction",
"possibleTypes": [
{
"name": "InvoiceType"
},
{
"name": "PaymentType"
}
]
}
]
}
}

View file

@ -10,7 +10,7 @@ export type CreateInvoiceMutationVariables = {
export type CreateInvoiceMutation = { __typename?: 'Mutation' } & {
createInvoice?: Types.Maybe<
{ __typename?: 'invoiceType' } & Pick<Types.InvoiceType, 'request'>
{ __typename?: 'newInvoiceType' } & Pick<Types.NewInvoiceType, 'request'>
>;
};

View file

@ -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
}
}
}
}
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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
>;

View file

@ -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
}

View file

@ -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
>;

View file

@ -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'
>
>;

View file

@ -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
}
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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<Types.GetResumeType, 'token'> & {
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
}
}
}
}
`;

View file

@ -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
>;

View file

@ -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<Types.ChannelsHealth, 'score'> & {
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
>;

View file

@ -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
}
}
}
}
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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
}
}
}
}
}
`;

View file

@ -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
}

View file

@ -0,0 +1,7 @@
import gql from 'graphql-tag';
export const GET_LATEST_VERSION = gql`
query GetLatestVersion {
getLatestVersion
}
`;

View file

@ -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
}
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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
}
}
}
}
`;

View file

@ -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
}
}
}
}
}
`;

View file

@ -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
}
}
}
}
}
`;

View file

@ -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<Scalars['String']>;
host?: Maybe<Scalars['String']>;
macaroon?: Maybe<Scalars['String']>;
cert?: Maybe<Scalars['String']>;
};
export type Query = {
__typename?: 'Query';
getVolumeHealth?: Maybe<ChannelsHealth>;
getTimeHealth?: Maybe<ChannelsTimeHealth>;
getFeeHealth?: Maybe<ChannelsFeeHealth>;
getChannelBalance?: Maybe<ChannelBalanceType>;
getChannels?: Maybe<Array<Maybe<ChannelType>>>;
getClosedChannels?: Maybe<Array<Maybe<ClosedChannelType>>>;
@ -21,7 +31,7 @@ export type Query = {
getNetworkInfo?: Maybe<NetworkInfoType>;
getNodeInfo?: Maybe<NodeInfoType>;
adminCheck?: Maybe<Scalars['Boolean']>;
getNode?: Maybe<PartnerNodeType>;
getNode?: Maybe<NodeType>;
decodeRequest?: Maybe<DecodeType>;
getWalletInfo?: Maybe<WalletInfoType>;
getResume?: Maybe<GetResumeType>;
@ -51,6 +61,19 @@ export type Query = {
getServerAccounts?: Maybe<Array<Maybe<ServerAccountType>>>;
getLnPayInfo?: Maybe<LnPayInfoType>;
getLnPay?: Maybe<Scalars['String']>;
getLatestVersion?: Maybe<Scalars['String']>;
};
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<Scalars['Int']>;
};
export type ChannelBalanceType = {
__typename?: 'channelBalanceType';
confirmedBalance?: Maybe<Scalars['Int']>;
pendingBalance?: Maybe<Scalars['Int']>;
};
export type AuthType = {
type: Scalars['String'];
id?: Maybe<Scalars['String']>;
host?: Maybe<Scalars['String']>;
macaroon?: Maybe<Scalars['String']>;
cert?: Maybe<Scalars['String']>;
};
export type ChannelType = {
__typename?: 'channelType';
capacity?: Maybe<Scalars['Int']>;
commit_transaction_fee?: Maybe<Scalars['Int']>;
commit_transaction_weight?: Maybe<Scalars['Int']>;
id?: Maybe<Scalars['String']>;
is_active?: Maybe<Scalars['Boolean']>;
is_closing?: Maybe<Scalars['Boolean']>;
is_opening?: Maybe<Scalars['Boolean']>;
is_partner_initiated?: Maybe<Scalars['Boolean']>;
is_private?: Maybe<Scalars['Boolean']>;
is_static_remote_key?: Maybe<Scalars['Boolean']>;
local_balance?: Maybe<Scalars['Int']>;
local_reserve?: Maybe<Scalars['Int']>;
partner_public_key?: Maybe<Scalars['String']>;
received?: Maybe<Scalars['Int']>;
remote_balance?: Maybe<Scalars['Int']>;
remote_reserve?: Maybe<Scalars['Int']>;
sent?: Maybe<Scalars['Int']>;
time_offline?: Maybe<Scalars['Int']>;
time_online?: Maybe<Scalars['Int']>;
transaction_id?: Maybe<Scalars['String']>;
transaction_vout?: Maybe<Scalars['Int']>;
unsettled_balance?: Maybe<Scalars['Int']>;
partner_node_info?: Maybe<PartnerNodeType>;
};
export type PartnerNodeType = {
__typename?: 'partnerNodeType';
alias?: Maybe<Scalars['String']>;
capacity?: Maybe<Scalars['String']>;
channel_count?: Maybe<Scalars['Int']>;
color?: Maybe<Scalars['String']>;
updated_at?: Maybe<Scalars['String']>;
base_fee?: Maybe<Scalars['Int']>;
fee_rate?: Maybe<Scalars['Int']>;
cltv_delta?: Maybe<Scalars['Int']>;
};
export type ClosedChannelType = {
__typename?: 'closedChannelType';
capacity?: Maybe<Scalars['Int']>;
close_confirm_height?: Maybe<Scalars['Int']>;
close_transaction_id?: Maybe<Scalars['String']>;
final_local_balance?: Maybe<Scalars['Int']>;
final_time_locked_balance?: Maybe<Scalars['Int']>;
id?: Maybe<Scalars['String']>;
is_breach_close?: Maybe<Scalars['Boolean']>;
is_cooperative_close?: Maybe<Scalars['Boolean']>;
is_funding_cancel?: Maybe<Scalars['Boolean']>;
is_local_force_close?: Maybe<Scalars['Boolean']>;
is_remote_force_close?: Maybe<Scalars['Boolean']>;
partner_public_key?: Maybe<Scalars['String']>;
transaction_id?: Maybe<Scalars['String']>;
transaction_vout?: Maybe<Scalars['Int']>;
partner_node_info?: Maybe<PartnerNodeType>;
};
export type PendingChannelType = {
__typename?: 'pendingChannelType';
close_transaction_id?: Maybe<Scalars['String']>;
is_active?: Maybe<Scalars['Boolean']>;
is_closing?: Maybe<Scalars['Boolean']>;
is_opening?: Maybe<Scalars['Boolean']>;
local_balance?: Maybe<Scalars['Int']>;
local_reserve?: Maybe<Scalars['Int']>;
partner_public_key?: Maybe<Scalars['String']>;
received?: Maybe<Scalars['Int']>;
remote_balance?: Maybe<Scalars['Int']>;
remote_reserve?: Maybe<Scalars['Int']>;
sent?: Maybe<Scalars['Int']>;
transaction_fee?: Maybe<Scalars['Int']>;
transaction_id?: Maybe<Scalars['String']>;
transaction_vout?: Maybe<Scalars['Int']>;
partner_node_info?: Maybe<PartnerNodeType>;
};
export type ChannelFeeType = {
__typename?: 'channelFeeType';
alias?: Maybe<Scalars['String']>;
color?: Maybe<Scalars['String']>;
baseFee?: Maybe<Scalars['Float']>;
feeRate?: Maybe<Scalars['Int']>;
transactionId?: Maybe<Scalars['String']>;
transactionVout?: Maybe<Scalars['Int']>;
public_key?: Maybe<Scalars['String']>;
};
export type ChannelReportType = {
__typename?: 'channelReportType';
local?: Maybe<Scalars['Int']>;
remote?: Maybe<Scalars['Int']>;
maxIn?: Maybe<Scalars['Int']>;
maxOut?: Maybe<Scalars['Int']>;
};
export type NetworkInfoType = {
__typename?: 'networkInfoType';
averageChannelSize?: Maybe<Scalars['String']>;
channelCount?: Maybe<Scalars['Int']>;
maxChannelSize?: Maybe<Scalars['Int']>;
medianChannelSize?: Maybe<Scalars['Int']>;
minChannelSize?: Maybe<Scalars['Int']>;
nodeCount?: Maybe<Scalars['Int']>;
notRecentlyUpdatedPolicyCount?: Maybe<Scalars['Int']>;
totalCapacity?: Maybe<Scalars['String']>;
};
export type NodeInfoType = {
__typename?: 'nodeInfoType';
chains?: Maybe<Array<Maybe<Scalars['String']>>>;
color?: Maybe<Scalars['String']>;
active_channels_count?: Maybe<Scalars['Int']>;
closed_channels_count?: Maybe<Scalars['Int']>;
alias?: Maybe<Scalars['String']>;
current_block_hash?: Maybe<Scalars['String']>;
current_block_height?: Maybe<Scalars['Boolean']>;
is_synced_to_chain?: Maybe<Scalars['Boolean']>;
is_synced_to_graph?: Maybe<Scalars['Boolean']>;
latest_block_at?: Maybe<Scalars['String']>;
peers_count?: Maybe<Scalars['Int']>;
pending_channels_count?: Maybe<Scalars['Int']>;
public_key?: Maybe<Scalars['String']>;
uris?: Maybe<Array<Maybe<Scalars['String']>>>;
version?: Maybe<Scalars['String']>;
};
export type DecodeType = {
__typename?: 'decodeType';
chain_address?: Maybe<Scalars['String']>;
cltv_delta?: Maybe<Scalars['Int']>;
description?: Maybe<Scalars['String']>;
description_hash?: Maybe<Scalars['String']>;
destination?: Maybe<Scalars['String']>;
expires_at?: Maybe<Scalars['String']>;
id?: Maybe<Scalars['String']>;
mtokens?: Maybe<Scalars['String']>;
routes?: Maybe<Array<Maybe<DecodeRoutesType>>>;
safe_tokens?: Maybe<Scalars['Int']>;
tokens?: Maybe<Scalars['Int']>;
};
export type DecodeRoutesType = {
__typename?: 'DecodeRoutesType';
base_fee_mtokens?: Maybe<Scalars['String']>;
channel?: Maybe<Scalars['String']>;
cltv_delta?: Maybe<Scalars['Int']>;
fee_rate?: Maybe<Scalars['Int']>;
public_key?: Maybe<Scalars['String']>;
};
export type WalletInfoType = {
__typename?: 'walletInfoType';
build_tags?: Maybe<Array<Maybe<Scalars['String']>>>;
commit_hash?: Maybe<Scalars['String']>;
is_autopilotrpc_enabled?: Maybe<Scalars['Boolean']>;
is_chainrpc_enabled?: Maybe<Scalars['Boolean']>;
is_invoicesrpc_enabled?: Maybe<Scalars['Boolean']>;
is_signrpc_enabled?: Maybe<Scalars['Boolean']>;
is_walletrpc_enabled?: Maybe<Scalars['Boolean']>;
is_watchtowerrpc_enabled?: Maybe<Scalars['Boolean']>;
is_wtclientrpc_enabled?: Maybe<Scalars['Boolean']>;
};
export type GetResumeType = {
__typename?: 'getResumeType';
token?: Maybe<Scalars['String']>;
resume?: Maybe<Scalars['String']>;
};
export type GetForwardType = {
__typename?: 'getForwardType';
token?: Maybe<Scalars['String']>;
forwards?: Maybe<Array<Maybe<ForwardType>>>;
};
export type ForwardType = {
__typename?: 'forwardType';
created_at?: Maybe<Scalars['String']>;
fee?: Maybe<Scalars['Int']>;
fee_mtokens?: Maybe<Scalars['String']>;
incoming_channel?: Maybe<Scalars['String']>;
incoming_alias?: Maybe<Scalars['String']>;
incoming_color?: Maybe<Scalars['String']>;
mtokens?: Maybe<Scalars['String']>;
outgoing_channel?: Maybe<Scalars['String']>;
outgoing_alias?: Maybe<Scalars['String']>;
outgoing_color?: Maybe<Scalars['String']>;
tokens?: Maybe<Scalars['Int']>;
};
export type BitcoinFeeType = {
__typename?: 'bitcoinFeeType';
fast?: Maybe<Scalars['Int']>;
halfHour?: Maybe<Scalars['Int']>;
hour?: Maybe<Scalars['Int']>;
};
export type InOutType = {
__typename?: 'InOutType';
invoices?: Maybe<Scalars['String']>;
payments?: Maybe<Scalars['String']>;
confirmedInvoices?: Maybe<Scalars['Int']>;
unConfirmedInvoices?: Maybe<Scalars['Int']>;
};
export type PeerType = {
__typename?: 'peerType';
bytes_received?: Maybe<Scalars['Int']>;
bytes_sent?: Maybe<Scalars['Int']>;
is_inbound?: Maybe<Scalars['Boolean']>;
is_sync_peer?: Maybe<Scalars['Boolean']>;
ping_time?: Maybe<Scalars['Int']>;
public_key?: Maybe<Scalars['String']>;
socket?: Maybe<Scalars['String']>;
tokens_received?: Maybe<Scalars['Int']>;
tokens_sent?: Maybe<Scalars['Int']>;
partner_node_info?: Maybe<PartnerNodeType>;
};
export type GetTransactionsType = {
__typename?: 'getTransactionsType';
block_id?: Maybe<Scalars['String']>;
confirmation_count?: Maybe<Scalars['Int']>;
confirmation_height?: Maybe<Scalars['Int']>;
created_at?: Maybe<Scalars['String']>;
fee?: Maybe<Scalars['Int']>;
id?: Maybe<Scalars['String']>;
output_addresses?: Maybe<Array<Maybe<Scalars['String']>>>;
tokens?: Maybe<Scalars['Int']>;
};
export type GetUtxosType = {
__typename?: 'getUtxosType';
address?: Maybe<Scalars['String']>;
address_format?: Maybe<Scalars['String']>;
confirmation_count?: Maybe<Scalars['Int']>;
output_script?: Maybe<Scalars['String']>;
tokens?: Maybe<Scalars['Int']>;
transaction_id?: Maybe<Scalars['String']>;
transaction_vout?: Maybe<Scalars['Int']>;
};
export type HodlOfferType = {
__typename?: 'hodlOfferType';
id?: Maybe<Scalars['String']>;
version?: Maybe<Scalars['String']>;
asset_code?: Maybe<Scalars['String']>;
searchable?: Maybe<Scalars['Boolean']>;
country?: Maybe<Scalars['String']>;
country_code?: Maybe<Scalars['String']>;
working_now?: Maybe<Scalars['Boolean']>;
side?: Maybe<Scalars['String']>;
title?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']>;
currency_code?: Maybe<Scalars['String']>;
price?: Maybe<Scalars['String']>;
min_amount?: Maybe<Scalars['String']>;
max_amount?: Maybe<Scalars['String']>;
first_trade_limit?: Maybe<Scalars['String']>;
fee?: Maybe<HodlOfferFeeType>;
balance?: Maybe<Scalars['String']>;
payment_window_minutes?: Maybe<Scalars['Int']>;
confirmations?: Maybe<Scalars['Int']>;
payment_method_instructions?: Maybe<Array<Maybe<HodlOfferPaymentType>>>;
trader?: Maybe<HodlOfferTraderType>;
};
export type HodlOfferFeeType = {
__typename?: 'hodlOfferFeeType';
author_fee_rate?: Maybe<Scalars['String']>;
};
export type HodlOfferPaymentType = {
__typename?: 'hodlOfferPaymentType';
id?: Maybe<Scalars['String']>;
version?: Maybe<Scalars['String']>;
payment_method_id?: Maybe<Scalars['String']>;
payment_method_type?: Maybe<Scalars['String']>;
payment_method_name?: Maybe<Scalars['String']>;
};
export type HodlOfferTraderType = {
__typename?: 'hodlOfferTraderType';
login?: Maybe<Scalars['String']>;
online_status?: Maybe<Scalars['String']>;
rating?: Maybe<Scalars['String']>;
trades_count?: Maybe<Scalars['Int']>;
url?: Maybe<Scalars['String']>;
verified?: Maybe<Scalars['Boolean']>;
verified_by?: Maybe<Scalars['String']>;
strong_hodler?: Maybe<Scalars['Boolean']>;
country?: Maybe<Scalars['String']>;
country_code?: Maybe<Scalars['String']>;
average_payment_time_minutes?: Maybe<Scalars['Int']>;
average_release_time_minutes?: Maybe<Scalars['Int']>;
days_since_last_trade?: Maybe<Scalars['Int']>;
};
export type HodlCountryType = {
__typename?: 'hodlCountryType';
code?: Maybe<Scalars['String']>;
name?: Maybe<Scalars['String']>;
native_name?: Maybe<Scalars['String']>;
currency_code?: Maybe<Scalars['String']>;
currency_name?: Maybe<Scalars['String']>;
};
export type HodlCurrencyType = {
__typename?: 'hodlCurrencyType';
code?: Maybe<Scalars['String']>;
name?: Maybe<Scalars['String']>;
type?: Maybe<Scalars['String']>;
};
export type GetMessagesType = {
__typename?: 'getMessagesType';
token?: Maybe<Scalars['String']>;
messages?: Maybe<Array<Maybe<MessagesType>>>;
};
export type MessagesType = {
__typename?: 'messagesType';
date?: Maybe<Scalars['String']>;
id?: Maybe<Scalars['String']>;
verified?: Maybe<Scalars['Boolean']>;
contentType?: Maybe<Scalars['String']>;
sender?: Maybe<Scalars['String']>;
alias?: Maybe<Scalars['String']>;
message?: Maybe<Scalars['String']>;
tokens?: Maybe<Scalars['Int']>;
};
export type ServerAccountType = {
__typename?: 'serverAccountType';
name: Scalars['String'];
id: Scalars['String'];
type: Scalars['String'];
loggedIn: Scalars['Boolean'];
};
export type LnPayInfoType = {
__typename?: 'lnPayInfoType';
max?: Maybe<Scalars['Int']>;
min?: Maybe<Scalars['Int']>;
};
export type Mutation = {
__typename?: 'Mutation';
closeChannel?: Maybe<CloseChannelType>;
@ -587,7 +249,7 @@ export type Mutation = {
updateFees?: Maybe<Scalars['Boolean']>;
parsePayment?: Maybe<ParsePaymentType>;
pay?: Maybe<PayType>;
createInvoice?: Maybe<InvoiceType>;
createInvoice?: Maybe<NewInvoiceType>;
payViaRoute?: Maybe<Scalars['Boolean']>;
createAddress?: Maybe<Scalars['String']>;
sendToAddress?: Maybe<SendToType>;
@ -681,18 +343,377 @@ export type MutationLogoutArgs = {
type: Scalars['String'];
};
export type NodeType = {
__typename?: 'nodeType';
alias?: Maybe<Scalars['String']>;
capacity?: Maybe<Scalars['String']>;
channel_count?: Maybe<Scalars['Int']>;
color?: Maybe<Scalars['String']>;
updated_at?: Maybe<Scalars['String']>;
base_fee?: Maybe<Scalars['Int']>;
fee_rate?: Maybe<Scalars['Int']>;
cltv_delta?: Maybe<Scalars['Int']>;
public_key?: Maybe<Scalars['String']>;
};
export type Node = {
__typename?: 'Node';
node?: Maybe<NodeType>;
};
export type NodeInfoType = {
__typename?: 'nodeInfoType';
chains?: Maybe<Array<Maybe<Scalars['String']>>>;
color?: Maybe<Scalars['String']>;
active_channels_count?: Maybe<Scalars['Int']>;
closed_channels_count?: Maybe<Scalars['Int']>;
alias?: Maybe<Scalars['String']>;
current_block_hash?: Maybe<Scalars['String']>;
current_block_height?: Maybe<Scalars['Boolean']>;
is_synced_to_chain?: Maybe<Scalars['Boolean']>;
is_synced_to_graph?: Maybe<Scalars['Boolean']>;
latest_block_at?: Maybe<Scalars['String']>;
peers_count?: Maybe<Scalars['Int']>;
pending_channels_count?: Maybe<Scalars['Int']>;
public_key?: Maybe<Scalars['String']>;
uris?: Maybe<Array<Maybe<Scalars['String']>>>;
version?: Maybe<Scalars['String']>;
};
export type ServerAccountType = {
__typename?: 'serverAccountType';
name: Scalars['String'];
id: Scalars['String'];
type: Scalars['String'];
loggedIn: Scalars['Boolean'];
};
export type HodlCountryType = {
__typename?: 'hodlCountryType';
code?: Maybe<Scalars['String']>;
name?: Maybe<Scalars['String']>;
native_name?: Maybe<Scalars['String']>;
currency_code?: Maybe<Scalars['String']>;
currency_name?: Maybe<Scalars['String']>;
};
export type HodlCurrencyType = {
__typename?: 'hodlCurrencyType';
code?: Maybe<Scalars['String']>;
name?: Maybe<Scalars['String']>;
type?: Maybe<Scalars['String']>;
};
export type HodlOfferFeeType = {
__typename?: 'hodlOfferFeeType';
author_fee_rate?: Maybe<Scalars['String']>;
};
export type HodlOfferPaymentType = {
__typename?: 'hodlOfferPaymentType';
id?: Maybe<Scalars['String']>;
version?: Maybe<Scalars['String']>;
payment_method_id?: Maybe<Scalars['String']>;
payment_method_type?: Maybe<Scalars['String']>;
payment_method_name?: Maybe<Scalars['String']>;
};
export type HodlOfferTraderType = {
__typename?: 'hodlOfferTraderType';
login?: Maybe<Scalars['String']>;
online_status?: Maybe<Scalars['String']>;
rating?: Maybe<Scalars['String']>;
trades_count?: Maybe<Scalars['Int']>;
url?: Maybe<Scalars['String']>;
verified?: Maybe<Scalars['Boolean']>;
verified_by?: Maybe<Scalars['String']>;
strong_hodler?: Maybe<Scalars['Boolean']>;
country?: Maybe<Scalars['String']>;
country_code?: Maybe<Scalars['String']>;
average_payment_time_minutes?: Maybe<Scalars['Int']>;
average_release_time_minutes?: Maybe<Scalars['Int']>;
days_since_last_trade?: Maybe<Scalars['Int']>;
};
export type HodlOfferType = {
__typename?: 'hodlOfferType';
id?: Maybe<Scalars['String']>;
version?: Maybe<Scalars['String']>;
asset_code?: Maybe<Scalars['String']>;
searchable?: Maybe<Scalars['Boolean']>;
country?: Maybe<Scalars['String']>;
country_code?: Maybe<Scalars['String']>;
working_now?: Maybe<Scalars['Boolean']>;
side?: Maybe<Scalars['String']>;
title?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']>;
currency_code?: Maybe<Scalars['String']>;
price?: Maybe<Scalars['String']>;
min_amount?: Maybe<Scalars['String']>;
max_amount?: Maybe<Scalars['String']>;
first_trade_limit?: Maybe<Scalars['String']>;
fee?: Maybe<HodlOfferFeeType>;
balance?: Maybe<Scalars['String']>;
payment_window_minutes?: Maybe<Scalars['Int']>;
confirmations?: Maybe<Scalars['Int']>;
payment_method_instructions?: Maybe<Array<Maybe<HodlOfferPaymentType>>>;
trader?: Maybe<HodlOfferTraderType>;
};
export type LnPayInfoType = {
__typename?: 'lnPayInfoType';
max?: Maybe<Scalars['Int']>;
min?: Maybe<Scalars['Int']>;
};
export type BitcoinFeeType = {
__typename?: 'bitcoinFeeType';
fast?: Maybe<Scalars['Int']>;
halfHour?: Maybe<Scalars['Int']>;
hour?: Maybe<Scalars['Int']>;
};
export type PeerType = {
__typename?: 'peerType';
bytes_received?: Maybe<Scalars['Int']>;
bytes_sent?: Maybe<Scalars['Int']>;
is_inbound?: Maybe<Scalars['Boolean']>;
is_sync_peer?: Maybe<Scalars['Boolean']>;
ping_time?: Maybe<Scalars['Int']>;
public_key?: Maybe<Scalars['String']>;
socket?: Maybe<Scalars['String']>;
tokens_received?: Maybe<Scalars['Int']>;
tokens_sent?: Maybe<Scalars['Int']>;
partner_node_info?: Maybe<Node>;
};
export type GetUtxosType = {
__typename?: 'getUtxosType';
address?: Maybe<Scalars['String']>;
address_format?: Maybe<Scalars['String']>;
confirmation_count?: Maybe<Scalars['Int']>;
output_script?: Maybe<Scalars['String']>;
tokens?: Maybe<Scalars['Int']>;
transaction_id?: Maybe<Scalars['String']>;
transaction_vout?: Maybe<Scalars['Int']>;
};
export type SendToType = {
__typename?: 'sendToType';
confirmationCount?: Maybe<Scalars['String']>;
id?: Maybe<Scalars['String']>;
isConfirmed?: Maybe<Scalars['Boolean']>;
isOutgoing?: Maybe<Scalars['Boolean']>;
tokens?: Maybe<Scalars['Int']>;
};
export type GetTransactionsType = {
__typename?: 'getTransactionsType';
block_id?: Maybe<Scalars['String']>;
confirmation_count?: Maybe<Scalars['Int']>;
confirmation_height?: Maybe<Scalars['Int']>;
created_at?: Maybe<Scalars['String']>;
fee?: Maybe<Scalars['Int']>;
id?: Maybe<Scalars['String']>;
output_addresses?: Maybe<Array<Maybe<Scalars['String']>>>;
tokens?: Maybe<Scalars['Int']>;
};
export type DecodeType = {
__typename?: 'decodeType';
chain_address?: Maybe<Scalars['String']>;
cltv_delta?: Maybe<Scalars['Int']>;
description?: Maybe<Scalars['String']>;
description_hash?: Maybe<Scalars['String']>;
destination?: Maybe<Scalars['String']>;
expires_at?: Maybe<Scalars['String']>;
id?: Maybe<Scalars['String']>;
mtokens?: Maybe<Scalars['String']>;
routes?: Maybe<Array<Maybe<DecodeRoutesType>>>;
safe_tokens?: Maybe<Scalars['Int']>;
tokens?: Maybe<Scalars['Int']>;
};
export type DecodeRoutesType = {
__typename?: 'DecodeRoutesType';
base_fee_mtokens?: Maybe<Scalars['String']>;
channel?: Maybe<Scalars['String']>;
cltv_delta?: Maybe<Scalars['Int']>;
fee_rate?: Maybe<Scalars['Int']>;
public_key?: Maybe<Scalars['String']>;
};
export type GetMessagesType = {
__typename?: 'getMessagesType';
token?: Maybe<Scalars['String']>;
messages?: Maybe<Array<Maybe<MessagesType>>>;
};
export type MessagesType = {
__typename?: 'messagesType';
date?: Maybe<Scalars['String']>;
id?: Maybe<Scalars['String']>;
verified?: Maybe<Scalars['Boolean']>;
contentType?: Maybe<Scalars['String']>;
sender?: Maybe<Scalars['String']>;
alias?: Maybe<Scalars['String']>;
message?: Maybe<Scalars['String']>;
tokens?: Maybe<Scalars['Int']>;
};
export type InOutType = {
__typename?: 'InOutType';
invoices?: Maybe<Scalars['String']>;
payments?: Maybe<Scalars['String']>;
confirmedInvoices?: Maybe<Scalars['Int']>;
unConfirmedInvoices?: Maybe<Scalars['Int']>;
};
export type PolicyType = {
__typename?: 'policyType';
base_fee_mtokens?: Maybe<Scalars['String']>;
cltv_delta?: Maybe<Scalars['Int']>;
fee_rate?: Maybe<Scalars['Int']>;
is_disabled?: Maybe<Scalars['Boolean']>;
max_htlc_mtokens?: Maybe<Scalars['String']>;
min_htlc_mtokens?: Maybe<Scalars['String']>;
public_key: Scalars['String'];
updated_at?: Maybe<Scalars['String']>;
my_node?: Maybe<Scalars['Boolean']>;
node?: Maybe<Node>;
};
export type SingleChannelType = {
__typename?: 'singleChannelType';
capacity: Scalars['Int'];
id: Scalars['String'];
policies: Array<PolicyType>;
transaction_id: Scalars['String'];
transaction_vout: Scalars['Int'];
updated_at?: Maybe<Scalars['String']>;
};
export type Channel = {
__typename?: 'Channel';
channel?: Maybe<SingleChannelType>;
};
export type ChannelFeeType = {
__typename?: 'channelFeeType';
alias?: Maybe<Scalars['String']>;
color?: Maybe<Scalars['String']>;
baseFee?: Maybe<Scalars['Float']>;
feeRate?: Maybe<Scalars['Int']>;
transactionId?: Maybe<Scalars['String']>;
transactionVout?: Maybe<Scalars['Int']>;
public_key?: Maybe<Scalars['String']>;
};
export type ChannelReportType = {
__typename?: 'channelReportType';
local?: Maybe<Scalars['Int']>;
remote?: Maybe<Scalars['Int']>;
maxIn?: Maybe<Scalars['Int']>;
maxOut?: Maybe<Scalars['Int']>;
};
export type ChannelBalanceType = {
__typename?: 'channelBalanceType';
confirmedBalance?: Maybe<Scalars['Int']>;
pendingBalance?: Maybe<Scalars['Int']>;
};
export type ChannelType = {
__typename?: 'channelType';
capacity?: Maybe<Scalars['Int']>;
commit_transaction_fee?: Maybe<Scalars['Int']>;
commit_transaction_weight?: Maybe<Scalars['Int']>;
id?: Maybe<Scalars['String']>;
is_active?: Maybe<Scalars['Boolean']>;
is_closing?: Maybe<Scalars['Boolean']>;
is_opening?: Maybe<Scalars['Boolean']>;
is_partner_initiated?: Maybe<Scalars['Boolean']>;
is_private?: Maybe<Scalars['Boolean']>;
is_static_remote_key?: Maybe<Scalars['Boolean']>;
local_balance?: Maybe<Scalars['Int']>;
local_reserve?: Maybe<Scalars['Int']>;
partner_public_key?: Maybe<Scalars['String']>;
received?: Maybe<Scalars['Int']>;
remote_balance?: Maybe<Scalars['Int']>;
remote_reserve?: Maybe<Scalars['Int']>;
sent?: Maybe<Scalars['Int']>;
time_offline?: Maybe<Scalars['Int']>;
time_online?: Maybe<Scalars['Int']>;
transaction_id?: Maybe<Scalars['String']>;
transaction_vout?: Maybe<Scalars['Int']>;
unsettled_balance?: Maybe<Scalars['Int']>;
partner_node_info?: Maybe<Node>;
partner_fee_info?: Maybe<Channel>;
};
export type CloseChannelType = {
__typename?: 'closeChannelType';
transactionId?: Maybe<Scalars['String']>;
transactionOutputIndex?: Maybe<Scalars['String']>;
};
export type ClosedChannelType = {
__typename?: 'closedChannelType';
capacity?: Maybe<Scalars['Int']>;
close_confirm_height?: Maybe<Scalars['Int']>;
close_transaction_id?: Maybe<Scalars['String']>;
final_local_balance?: Maybe<Scalars['Int']>;
final_time_locked_balance?: Maybe<Scalars['Int']>;
id?: Maybe<Scalars['String']>;
is_breach_close?: Maybe<Scalars['Boolean']>;
is_cooperative_close?: Maybe<Scalars['Boolean']>;
is_funding_cancel?: Maybe<Scalars['Boolean']>;
is_local_force_close?: Maybe<Scalars['Boolean']>;
is_remote_force_close?: Maybe<Scalars['Boolean']>;
partner_public_key?: Maybe<Scalars['String']>;
transaction_id?: Maybe<Scalars['String']>;
transaction_vout?: Maybe<Scalars['Int']>;
partner_node_info?: Maybe<Node>;
};
export type OpenChannelType = {
__typename?: 'openChannelType';
transactionId?: Maybe<Scalars['String']>;
transactionOutputIndex?: Maybe<Scalars['String']>;
};
export type PendingChannelType = {
__typename?: 'pendingChannelType';
close_transaction_id?: Maybe<Scalars['String']>;
is_active?: Maybe<Scalars['Boolean']>;
is_closing?: Maybe<Scalars['Boolean']>;
is_opening?: Maybe<Scalars['Boolean']>;
local_balance?: Maybe<Scalars['Int']>;
local_reserve?: Maybe<Scalars['Int']>;
partner_public_key?: Maybe<Scalars['String']>;
received?: Maybe<Scalars['Int']>;
remote_balance?: Maybe<Scalars['Int']>;
remote_reserve?: Maybe<Scalars['Int']>;
sent?: Maybe<Scalars['Int']>;
transaction_fee?: Maybe<Scalars['Int']>;
transaction_id?: Maybe<Scalars['String']>;
transaction_vout?: Maybe<Scalars['Int']>;
partner_node_info?: Maybe<Node>;
};
export type WalletInfoType = {
__typename?: 'walletInfoType';
build_tags?: Maybe<Array<Maybe<Scalars['String']>>>;
commit_hash?: Maybe<Scalars['String']>;
is_autopilotrpc_enabled?: Maybe<Scalars['Boolean']>;
is_chainrpc_enabled?: Maybe<Scalars['Boolean']>;
is_invoicesrpc_enabled?: Maybe<Scalars['Boolean']>;
is_signrpc_enabled?: Maybe<Scalars['Boolean']>;
is_walletrpc_enabled?: Maybe<Scalars['Boolean']>;
is_watchtowerrpc_enabled?: Maybe<Scalars['Boolean']>;
is_wtclientrpc_enabled?: Maybe<Scalars['Boolean']>;
};
export type ParsePaymentType = {
__typename?: 'parsePaymentType';
chainAddresses?: Maybe<Array<Maybe<Scalars['String']>>>;
@ -743,8 +764,8 @@ export type HopsType = {
timeout?: Maybe<Scalars['Int']>;
};
export type InvoiceType = {
__typename?: 'invoiceType';
export type NewInvoiceType = {
__typename?: 'newInvoiceType';
chainAddress?: Maybe<Scalars['String']>;
createdAt?: Maybe<Scalars['DateTime']>;
description?: Maybe<Scalars['String']>;
@ -754,11 +775,143 @@ export type InvoiceType = {
tokens?: Maybe<Scalars['Int']>;
};
export type SendToType = {
__typename?: 'sendToType';
confirmationCount?: Maybe<Scalars['String']>;
id?: Maybe<Scalars['String']>;
isConfirmed?: Maybe<Scalars['Boolean']>;
isOutgoing?: Maybe<Scalars['Boolean']>;
tokens?: Maybe<Scalars['Int']>;
export type NetworkInfoType = {
__typename?: 'networkInfoType';
averageChannelSize?: Maybe<Scalars['String']>;
channelCount?: Maybe<Scalars['Int']>;
maxChannelSize?: Maybe<Scalars['Int']>;
medianChannelSize?: Maybe<Scalars['Int']>;
minChannelSize?: Maybe<Scalars['Int']>;
nodeCount?: Maybe<Scalars['Int']>;
notRecentlyUpdatedPolicyCount?: Maybe<Scalars['Int']>;
totalCapacity?: Maybe<Scalars['String']>;
};
export type GetForwardType = {
__typename?: 'getForwardType';
token?: Maybe<Scalars['String']>;
forwards?: Maybe<Array<Maybe<ForwardType>>>;
};
export type ForwardType = {
__typename?: 'forwardType';
created_at?: Maybe<Scalars['String']>;
fee?: Maybe<Scalars['Int']>;
fee_mtokens?: Maybe<Scalars['String']>;
incoming_channel?: Maybe<Scalars['String']>;
mtokens?: Maybe<Scalars['String']>;
outgoing_channel?: Maybe<Scalars['String']>;
tokens?: Maybe<Scalars['Int']>;
incoming_channel_info?: Maybe<Channel>;
outgoing_channel_info?: Maybe<Channel>;
};
export type PaymentType = {
__typename?: 'PaymentType';
created_at: Scalars['String'];
destination: Scalars['String'];
destination_node?: Maybe<Node>;
fee: Scalars['Int'];
fee_mtokens: Scalars['String'];
hops?: Maybe<Array<Maybe<Node>>>;
id: Scalars['String'];
index?: Maybe<Scalars['Int']>;
is_confirmed: Scalars['Boolean'];
is_outgoing: Scalars['Boolean'];
mtokens: Scalars['String'];
request?: Maybe<Scalars['String']>;
safe_fee: Scalars['Int'];
safe_tokens?: Maybe<Scalars['Int']>;
secret: Scalars['String'];
tokens: Scalars['Int'];
type: Scalars['String'];
date: Scalars['String'];
};
export type InvoiceType = {
__typename?: 'InvoiceType';
chain_address?: Maybe<Scalars['String']>;
confirmed_at?: Maybe<Scalars['String']>;
created_at: Scalars['String'];
description: Scalars['String'];
description_hash?: Maybe<Scalars['String']>;
expires_at: Scalars['String'];
id: Scalars['String'];
is_canceled?: Maybe<Scalars['Boolean']>;
is_confirmed: Scalars['Boolean'];
is_held?: Maybe<Scalars['Boolean']>;
is_private: Scalars['Boolean'];
is_push?: Maybe<Scalars['Boolean']>;
received: Scalars['Int'];
received_mtokens: Scalars['String'];
request?: Maybe<Scalars['String']>;
secret: Scalars['String'];
tokens: Scalars['Int'];
type: Scalars['String'];
date: Scalars['String'];
};
export type Transaction = InvoiceType | PaymentType;
export type GetResumeType = {
__typename?: 'getResumeType';
token?: Maybe<Scalars['String']>;
resume?: Maybe<Array<Maybe<Transaction>>>;
};
export type ChannelHealth = {
__typename?: 'channelHealth';
id?: Maybe<Scalars['String']>;
score?: Maybe<Scalars['Int']>;
volumeNormalized?: Maybe<Scalars['String']>;
averageVolumeNormalized?: Maybe<Scalars['String']>;
partner?: Maybe<Node>;
};
export type ChannelsHealth = {
__typename?: 'channelsHealth';
score?: Maybe<Scalars['Int']>;
channels?: Maybe<Array<Maybe<ChannelHealth>>>;
};
export type ChannelTimeHealth = {
__typename?: 'channelTimeHealth';
id?: Maybe<Scalars['String']>;
score?: Maybe<Scalars['Int']>;
significant?: Maybe<Scalars['Boolean']>;
monitoredTime?: Maybe<Scalars['Int']>;
monitoredUptime?: Maybe<Scalars['Int']>;
monitoredDowntime?: Maybe<Scalars['Int']>;
partner?: Maybe<Node>;
};
export type ChannelsTimeHealth = {
__typename?: 'channelsTimeHealth';
score?: Maybe<Scalars['Int']>;
channels?: Maybe<Array<Maybe<ChannelTimeHealth>>>;
};
export type FeeHealth = {
__typename?: 'feeHealth';
score?: Maybe<Scalars['Int']>;
rate?: Maybe<Scalars['Int']>;
base?: Maybe<Scalars['String']>;
rateScore?: Maybe<Scalars['Int']>;
baseScore?: Maybe<Scalars['Int']>;
rateOver?: Maybe<Scalars['Boolean']>;
baseOver?: Maybe<Scalars['Boolean']>;
};
export type ChannelFeeHealth = {
__typename?: 'channelFeeHealth';
id?: Maybe<Scalars['String']>;
partnerSide?: Maybe<FeeHealth>;
mySide?: Maybe<FeeHealth>;
partner?: Maybe<Node>;
};
export type ChannelsFeeHealth = {
__typename?: 'channelsFeeHealth';
score?: Maybe<Scalars['Int']>;
channels?: Maybe<Array<Maybe<ChannelFeeHealth>>>;
};

View file

@ -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<NavProps>`
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};
}
`;

View file

@ -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) => (
<Link to={link} noStyling={true}>
<HeaderNavButton selected={pathname === link}>
<NavIcon size={18} />
</HeaderNavButton>
</Link>
);
const renderLoggedIn = () => (
<>
<ViewSwitch>
@ -44,11 +69,12 @@ export const Header = () => {
</IconWrapper>
</ViewSwitch>
<ViewSwitch hideMobile={true}>
<Circle
size={12}
strokeWidth={'0'}
color={syncedToChain ? '#95de64' : '#ff7875'}
/>
<HeaderButtons>
{renderNavButton(HOME, Home)}
{renderNavButton(TRADER, CreditCard)}
{renderNavButton(CHAT, MessageCircle)}
{renderNavButton(SETTINGS, Settings)}
</HeaderButtons>
</ViewSwitch>
</>
);

View file

@ -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)}
</ButtonSection>
);
@ -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)}

View file

@ -120,7 +120,7 @@ export const ChannelCard: React.FC<ChannelCardProps> = ({
base_fee,
fee_rate,
cltv_delta,
} = partner_node_info;
} = partner_node_info?.node || {};
const formatBalance = format({ amount: capacity });
const formatLocal = format({ amount: local_balance });

View file

@ -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;

View file

@ -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 = <Price amount={capacity} />;

View file

@ -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 });

View file

@ -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 = <Price amount={tokens} />;
@ -67,8 +68,14 @@ export const ForwardCard = ({
<ResponsiveLine>
<ResponsiveSingle>
<ColumnLine>
{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
)}
</ColumnLine>
</ResponsiveSingle>
<ColumnLine>

View file

@ -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) {

View file

@ -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 (
<ScoreLine>
<DarkSubTitle>Score</DarkSubTitle>
{score}
{getIcon(score)}
</ScoreLine>
);
};
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 (
<>
<Separation />
<WarningText warningColor={getProgressColor(rateScore)}>
{message}
</WarningText>
<WarningText warningColor={getProgressColor(baseScore)}>
{baseMessage}
</WarningText>
{renderLine('Fee Rate (ppm):', rate)}
{renderLine('Base Fee (sats):', base)}
</>
);
};
return (
<SubCard key={channel.id}>
<Clickable onClick={() => openSet(open ? 0 : index)}>
<SingleLine>
{channel?.partner?.node?.alias}
<ScoreColumn>{renderContent()}</ScoreColumn>
</SingleLine>
</Clickable>
{open && renderDetails()}
</SubCard>
);
};
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 (
<>
<StatWrapper title={'Fee Stats'}>
{sortedArray.map((channel, index) => (
<FeeStatCard
key={channel.id}
channel={channel}
open={index + 1 === open}
openSet={openSet}
index={index + 1}
/>
))}
</StatWrapper>
<StatWrapper title={'My Fee Stats'}>
{sortedArrayMyStats.map((channel, index) => (
<FeeStatCard
key={channel.id}
channel={channel}
myStats={true}
open={index + 1 === openTwo}
openSet={openTwoSet}
index={index + 1}
/>
))}
</StatWrapper>
</>
);
};

View file

@ -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 (
<>
<StatsTitle>Node Statistics</StatsTitle>
<ProgressRow>
<ProgressCard>
<CircularProgressbarWithChildren
value={volumeScore}
styles={buildStyles({
pathColor: getProgressColor(volumeScore),
trailColor: 'rgba(0, 0, 0, 0.1)',
})}
>
<DarkSubTitle>Volume</DarkSubTitle>
<ScoreTitle>{volumeScore}</ScoreTitle>
</CircularProgressbarWithChildren>
</ProgressCard>
<ProgressCard>
<CircularProgressbarWithChildren
value={timeScore}
styles={buildStyles({
pathColor: getProgressColor(timeScore),
trailColor: 'rgba(0, 0, 0, 0.1)',
})}
>
<DarkSubTitle>Time</DarkSubTitle>
<ScoreTitle>{timeScore}</ScoreTitle>
</CircularProgressbarWithChildren>
</ProgressCard>
<ProgressCard>
<CircularProgressbarWithChildren
value={feeScore}
styles={buildStyles({
pathColor: getProgressColor(feeScore),
trailColor: 'rgba(0, 0, 0, 0.1)',
})}
>
<DarkSubTitle>Fee</DarkSubTitle>
<ScoreTitle>{feeScore}</ScoreTitle>
</CircularProgressbarWithChildren>
</ProgressCard>
</ProgressRow>
</>
);
};

View file

@ -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 = () => (
<>
<Separation />
{!channel.significant && (
<WarningText>
Needs to be monitored for a longer period to give significant
statistics.
</WarningText>
)}
<WarningText warningColor={getProgressColor(channel.score)}>
{message}
</WarningText>
{renderLine('Monitored time:', formatSeconds(channel.monitoredTime))}
{renderLine('Monitored up time:', formatSeconds(channel.monitoredUptime))}
{renderLine(
'Monitored down time:',
formatSeconds(channel.monitoredDowntime)
)}
</>
);
return (
<SubCard key={channel.id}>
<Clickable onClick={() => openSet(open ? 0 : index)}>
<SingleLine>
<SubTitle>{channel?.partner?.node?.alias}</SubTitle>
<ScoreLine>
<DarkSubTitle>Score</DarkSubTitle>
{channel.score}
{getIcon(channel.score, !channel.significant)}
</ScoreLine>
</SingleLine>
</Clickable>
{open && renderContent()}
</SubCard>
);
};
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 (
<StatWrapper title={'Time Stats'}>
{sortedArray.map((channel, index) => (
<TimeStatCard
key={channel.id}
channel={channel}
open={index + 1 === open}
openSet={openSet}
index={index + 1}
/>
))}
</StatWrapper>
);
};

View file

@ -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 = () => (
<>
<Separation />
<WarningText warningColor={getProgressColor(channel.score)}>
{message}
</WarningText>
{renderLine('Volume (sats/block):', channel.volumeNormalized)}
{renderLine(
'Average Volume (sats/block):',
channel.averageVolumeNormalized
)}
</>
);
return (
<SubCard key={channel.id}>
<Clickable onClick={() => openSet(open ? 0 : index)}>
<SingleLine>
<SubTitle>{channel?.partner?.node?.alias}</SubTitle>
<ScoreLine>
<DarkSubTitle>{'Score'}</DarkSubTitle>
{channel.score}
{getIcon(channel.score)}
</ScoreLine>
</SingleLine>
</Clickable>
{open && renderContent()}
</SubCard>
);
};
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 (
<StatWrapper title={'Volume Stats'}>
{sortedArray.map((channel, index) => (
<VolumeStatCard
key={channel.id}
channel={channel}
open={index + 1 === open}
openSet={openSet}
index={index + 1}
/>
))}
</StatWrapper>
);
};

View file

@ -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<StatWrapperProps> = ({
children,
title,
}) => {
const [open, openSet] = React.useState(false);
return (
<Card>
<StatHeaderLine isOpen={open} onClick={() => openSet(p => !p)}>
<SubTitle>{title}</SubTitle>
{open ? <ChevronUp /> : <ChevronDown />}
</StatHeaderLine>
{open && children}
</Card>
);
};

View file

@ -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<State | undefined>(undefined);
const DispatchContext = createContext<Dispatch | undefined>(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 (
<DispatchContext.Provider value={dispatch}>
<StateContext.Provider value={state}>{children}</StateContext.Provider>
</DispatchContext.Provider>
);
};
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 };

129
src/views/stats/helpers.tsx Normal file
View file

@ -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 <AlertTriangle color={chartColors.orange} />;
}
switch (true) {
case score > 90:
return <CheckCircle color={getProgressColor(score)} />;
case score > 75:
return <CheckCircle color={getProgressColor(score)} />;
case score > 60:
return <AlertCircle color={getProgressColor(score)} />;
case score > 50:
return <AlertCircle color={getProgressColor(score)} />;
default:
return <XCircle color={getProgressColor(score)} />;
}
};
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;
};

View file

@ -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<StatHeaderProps>`
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)<WarningProps>`
width: 100%;
text-align: center;
color: ${({ warningColor }) =>
warningColor ? warningColor : chartColors.orange};
`;
export const Clickable = styled.div`
cursor: pointer;
`;

View file

@ -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 = <Price amount={tokens} />;
@ -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)}
</>

View file

@ -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 = <Price amount={tokens} />;
const formatFee = <Price amount={fee} />;
@ -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')}