mirror of
https://github.com/lightninglabs/lightning-terminal.git
synced 2026-08-13 12:33:36 +02:00
Merge pull request #15 from lightninglabs/feat/storybook
Add initial site layout component
This commit is contained in:
commit
aa5c1e9aac
45 changed files with 4994 additions and 486 deletions
21
.vscode/launch.json
vendored
Normal file
21
.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug Tests",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/react-scripts",
|
||||
"args": ["test", "--runInBand", "--no-cache", "--watchAll=false"],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"protocol": "inspector",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"env": { "CI": "true" },
|
||||
"disableOptimisticBPs": true
|
||||
}
|
||||
]
|
||||
}
|
||||
6
app/.storybook/.babelrc
Normal file
6
app/.storybook/.babelrc
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"plugins": [
|
||||
["@babel/plugin-proposal-decorators", { "legacy": true }],
|
||||
["@babel/plugin-proposal-class-properties", { "loose": true }]
|
||||
]
|
||||
}
|
||||
14
app/.storybook/main.js
Normal file
14
app/.storybook/main.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
module.exports = {
|
||||
stories: ['../src/**/*.stories.(ts|tsx|js|jsx|mdx)'],
|
||||
addons: [
|
||||
'@storybook/preset-create-react-app',
|
||||
'@storybook/addon-actions',
|
||||
'@storybook/addon-links',
|
||||
{
|
||||
name: '@storybook/addon-docs',
|
||||
options: {
|
||||
configureJSX: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
6
app/.storybook/manager.js
Normal file
6
app/.storybook/manager.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { themes } from '@storybook/theming/create';
|
||||
import { addons } from '@storybook/addons';
|
||||
|
||||
addons.setConfig({
|
||||
theme: themes.dark,
|
||||
});
|
||||
38
app/.storybook/preview.tsx
Normal file
38
app/.storybook/preview.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import React from 'react';
|
||||
import { action } from 'mobx';
|
||||
import { addDecorator } from '@storybook/react';
|
||||
import { ThemeProvider } from '../src/components/theme';
|
||||
import { Store, StoreProvider } from '../src/store';
|
||||
import * as sampleData from '../src/util/sampleData';
|
||||
|
||||
/**
|
||||
* Create a store with dummy data to use for stories
|
||||
*/
|
||||
const store = new Store();
|
||||
store.info = sampleData.lndGetInfo;
|
||||
store.channels = sampleData.lndListChannels;
|
||||
store.swaps = sampleData.loopListSwaps;
|
||||
|
||||
/**
|
||||
* Create dummy actions to use for stories
|
||||
*/
|
||||
const actions = {
|
||||
node: {
|
||||
getInfo: () => action('node.getInfo'),
|
||||
},
|
||||
channel: {
|
||||
getChannels: () => action('channel.getChannels'),
|
||||
},
|
||||
swap: {
|
||||
listSwaps: () => action('swap.listSwaps'),
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* decorator function to wrap all stories with the necessary providers
|
||||
*/
|
||||
addDecorator(storyFn => (
|
||||
<StoreProvider store={store} actions={actions}>
|
||||
<ThemeProvider>{storyFn()}</ThemeProvider>
|
||||
</StoreProvider>
|
||||
));
|
||||
|
|
@ -12,22 +12,33 @@
|
|||
"eject": "react-scripts eject",
|
||||
"lint": "eslint --ext .ts,.tsx --ignore-path .eslintignore .",
|
||||
"tsc": "tsc --noEmit",
|
||||
"protos": "./scripts/build-protos.sh"
|
||||
"protos": "./scripts/build-protos.sh",
|
||||
"storybook": "start-storybook -p 9009 -s public",
|
||||
"build-storybook": "build-storybook -s public"
|
||||
},
|
||||
"proxy": "https://localhost:8443",
|
||||
"dependencies": {
|
||||
"@emotion/core": "10.0.28",
|
||||
"@emotion/styled": "10.0.27",
|
||||
"@improbable-eng/grpc-web": "0.12.0",
|
||||
"debug": "4.1.1",
|
||||
"emotion-theming": "10.0.27",
|
||||
"i18next": "19.4.1",
|
||||
"i18next-browser-languagedetector": "4.0.2",
|
||||
"mobx": "5.15.4",
|
||||
"mobx-react": "6.2.2",
|
||||
"mobx-react-lite": "2.0.6",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-i18next": "11.3.4",
|
||||
"react-scripts": "3.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-actions": "^5.3.18",
|
||||
"@storybook/addon-docs": "5.3.18",
|
||||
"@storybook/addon-links": "^5.3.18",
|
||||
"@storybook/addons": "^5.3.18",
|
||||
"@storybook/preset-create-react-app": "^2.1.1",
|
||||
"@storybook/react": "^5.3.18",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
|
|
@ -39,11 +50,13 @@
|
|||
"@types/react-dom": "^16.9.0",
|
||||
"@typescript-eslint/eslint-plugin": "^2.27.0",
|
||||
"@typescript-eslint/parser": "^2.27.0",
|
||||
"bootstrap": "4.4.1",
|
||||
"cross-env": "7.0.2",
|
||||
"eslint-config-prettier": "6.10.1",
|
||||
"eslint-plugin-prettier": "3.1.3",
|
||||
"eslint-plugin-react": "7.19.0",
|
||||
"google-protobuf": "3.11.4",
|
||||
"node-sass": "4.13.1",
|
||||
"prettier": "2.0.4",
|
||||
"ts-protoc-gen": "0.12.0",
|
||||
"typescript": "~3.7.2"
|
||||
|
|
@ -58,6 +71,7 @@
|
|||
"collectCoverageFrom": [
|
||||
"src/**/*.{js,jsx,ts,tsx}",
|
||||
"!src/**/*.d.ts",
|
||||
"!src/__(stories|mocks)__/**/*.{ts,tsx}",
|
||||
"!src/types/**/*.{js,ts}",
|
||||
"!src/i18n/**/*.{js,ts}",
|
||||
"!src/index.tsx"
|
||||
|
|
|
|||
|
|
@ -5,39 +5,12 @@
|
|||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
<title>Lightning Dashboard</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,62 +0,0 @@
|
|||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.App-info {
|
||||
width: 800px;
|
||||
min-width: 40vmin;
|
||||
height: 20vmin;
|
||||
margin: 20px auto;
|
||||
overflow-y: scroll;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.App-table {
|
||||
margin: 0 auto;
|
||||
border: 1px solid #ddd;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.App-table th,
|
||||
.App-table td {
|
||||
padding: 5px 10px;
|
||||
border: 1px solid #ddd;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
51
app/src/App.scss
Normal file
51
app/src/App.scss
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
@charset "utf-8";
|
||||
|
||||
// required bootstrap styles
|
||||
@import '../node_modules/bootstrap/scss/functions';
|
||||
@import '../node_modules/bootstrap/scss/variables';
|
||||
@import '../node_modules/bootstrap/scss/mixins';
|
||||
@import '../node_modules/bootstrap/scss/utilities';
|
||||
|
||||
// optional bootstrap styles
|
||||
@import '../node_modules/bootstrap/scss/reboot';
|
||||
@import '../node_modules/bootstrap/scss/buttons';
|
||||
@import '../node_modules/bootstrap/scss/tables';
|
||||
@import '../node_modules/bootstrap/scss/nav';
|
||||
|
||||
@font-face {
|
||||
font-family: 'OpenSans Light';
|
||||
src: url('./assets/fonts/OpenSans-Light.ttf') format('truetype');
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'OpenSans Regular';
|
||||
src: url('./assets/fonts/OpenSans-Regular.ttf') format('truetype');
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'OpenSans SemiBold';
|
||||
src: url('./assets/fonts/OpenSans-SemiBold.ttf') format('truetype');
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'OpenSans Bold';
|
||||
src: url('./assets/fonts/OpenSans-Bold.ttf') format('truetype');
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'OpenSans ExtraBold';
|
||||
src: url('./assets/fonts/OpenSans-ExtraBold.ttf') format('truetype');
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
110
app/src/App.tsx
110
app/src/App.tsx
|
|
@ -1,101 +1,21 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import './App.css';
|
||||
import usePrefixedTranslation from 'hooks/usePrefixedTranslation';
|
||||
import { channel, node, swap } from 'action';
|
||||
import store from 'store';
|
||||
import React from 'react';
|
||||
import './App.scss';
|
||||
import { Store, StoreProvider } from 'store';
|
||||
import { Layout } from 'components/layout';
|
||||
import SamplePage from 'components/pages/SamplePage';
|
||||
import { ThemeProvider } from 'components/theme';
|
||||
|
||||
const App = () => {
|
||||
const { l } = usePrefixedTranslation('App');
|
||||
useEffect(() => {
|
||||
// fetch node info when the component is mounted
|
||||
const fetchInfo = async () => await node.getInfo();
|
||||
fetchInfo();
|
||||
}, []);
|
||||
|
||||
const store = new Store();
|
||||
return (
|
||||
<div className="App">
|
||||
<p>{l('App.nodeInfo')}</p>
|
||||
{store.info && (
|
||||
<>
|
||||
<table className="App-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{l('pubkey')}</th>
|
||||
<td>{store.info.identityPubkey}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{l('alias')}</th>
|
||||
<td>{store.info.alias}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{l('version')}</th>
|
||||
<td>{store.info.version}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{l('numChannels')}</th>
|
||||
<td>{store.info.numActiveChannels}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
)}
|
||||
<p>
|
||||
{store.channels.length} Channels
|
||||
<button onClick={channel.getChannels}>Fetch</button>
|
||||
</p>
|
||||
<table className="App-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Can Receive</td>
|
||||
<td>Can Send</td>
|
||||
<td>In Fee %</td>
|
||||
<td>Up time %</td>
|
||||
<td>Volume (24h)</td>
|
||||
<td>Peer/Alias</td>
|
||||
<td>Capacity</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{store.channels.map(c => (
|
||||
<tr key={c.chanId}>
|
||||
<td>{c.remoteBalance}</td>
|
||||
<td>{c.localBalance}</td>
|
||||
<td></td>
|
||||
<td>{c.uptime}</td>
|
||||
<td></td>
|
||||
<td>{c.remotePubkey}</td>
|
||||
<td>{c.capacity}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
{store.swaps.length} Swaps
|
||||
<button onClick={swap.listSwaps}>Fetch</button>
|
||||
</p>
|
||||
<table className="App-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Date</td>
|
||||
<td>Type</td>
|
||||
<td>Amount</td>
|
||||
<td>Status</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{store.swaps.map(s => (
|
||||
<tr key={s.id}>
|
||||
<td>{s.createdOn.toString()}</td>
|
||||
<td>{s.type}</td>
|
||||
<td>{s.amount.toString()}</td>
|
||||
<td>{s.status}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<StoreProvider store={store}>
|
||||
<ThemeProvider>
|
||||
<Layout>
|
||||
<SamplePage />
|
||||
</Layout>
|
||||
</ThemeProvider>
|
||||
</StoreProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default observer(App);
|
||||
export default App;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { ProtobufMessage } from '@improbable-eng/grpc-web/dist/typings/message';
|
||||
import { UnaryMethodDefinition } from '@improbable-eng/grpc-web/dist/typings/service';
|
||||
import { UnaryRpcOptions } from '@improbable-eng/grpc-web/dist/typings/unary';
|
||||
import * as samples from 'util/sampleData';
|
||||
|
||||
// mock grpc module
|
||||
export const grpc = {
|
||||
|
|
@ -30,88 +31,7 @@ export const grpc = {
|
|||
|
||||
// collection of mock API responses
|
||||
const mockApiResponses: Record<string, any> = {
|
||||
'lnrpc.Lightning.GetInfo': {
|
||||
version: '0.9.0-beta commit=v0.9.0-beta',
|
||||
identityPubkey: '038b3fc29cfc195c9b190d86ad2d40ce7550a5c6f13941f53c7d7ac5b25c912a6c',
|
||||
alias: 'alice',
|
||||
color: '#cccccc',
|
||||
numPendingChannels: 0,
|
||||
numActiveChannels: 1,
|
||||
numInactiveChannels: 0,
|
||||
numPeers: 1,
|
||||
blockHeight: 185,
|
||||
blockHash: '547d3dcfb7d56532bed2efdeea0d400f11167b34d493bcd45fedb21f2ef7ed43',
|
||||
bestHeaderTimestamp: 1586548672,
|
||||
syncedToChain: false,
|
||||
syncedToGraph: true,
|
||||
testnet: false,
|
||||
chainsList: [{ chain: 'bitcoin', network: 'regtest' }],
|
||||
urisList: [
|
||||
'038b3fc29cfc195c9b190d86ad2d40ce7550a5c6f13941f53c7d7ac5b25c912a6c@172.18.0.7:9735',
|
||||
],
|
||||
featuresMap: [
|
||||
[0, { name: 'data-loss-protect', isRequired: true, isKnown: true }],
|
||||
[13, { name: 'static-remote-key', isRequired: false, isKnown: true }],
|
||||
[15, { name: 'payment-addr', isRequired: false, isKnown: true }],
|
||||
[17, { name: 'multi-path-payments', isRequired: false, isKnown: true }],
|
||||
[5, { name: 'upfront-shutdown-script', isRequired: false, isKnown: true }],
|
||||
[7, { name: 'gossip-queries', isRequired: false, isKnown: true }],
|
||||
[9, { name: 'tlv-onion', isRequired: false, isKnown: true }],
|
||||
],
|
||||
},
|
||||
'lnrpc.Lightning.ListChannels': {
|
||||
channelsList: [
|
||||
{
|
||||
active: true,
|
||||
remotePubkey:
|
||||
'037136742c67e24681f36542f7c8916aa6f6fdf665c1dca2a107425503cff94501',
|
||||
channelPoint:
|
||||
'0ef6a4ae3d8f800f4eb736f0776f5d3a72571615a1b7218ab17c9a43f85d8949:0',
|
||||
chanId: '124244814004224',
|
||||
capacity: 15000000,
|
||||
localBalance: 9988660,
|
||||
remoteBalance: 4501409,
|
||||
commitFee: 11201,
|
||||
commitWeight: 896,
|
||||
feePerKw: 12500,
|
||||
unsettledBalance: 498730,
|
||||
totalSatoshisSent: 1338,
|
||||
totalSatoshisReceived: 499929,
|
||||
numUpdates: 6,
|
||||
pendingHtlcsList: [
|
||||
{
|
||||
incoming: false,
|
||||
amount: 498730,
|
||||
hashLock: 'pl8fmsyoSqEQFQCw6Zu9e1aIlFnMz5H+hW2mmh3kRlI=',
|
||||
expirationHeight: 285,
|
||||
},
|
||||
],
|
||||
csvDelay: 1802,
|
||||
pb_private: false,
|
||||
initiator: true,
|
||||
chanStatusFlags: 'ChanStatusDefault',
|
||||
localChanReserveSat: 150000,
|
||||
remoteChanReserveSat: 150000,
|
||||
staticRemoteKey: true,
|
||||
lifetime: 21802,
|
||||
uptime: 21802,
|
||||
closeAddress: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
'looprpc.SwapClient.ListSwaps': {
|
||||
swapsList: [...Array(7)].map((x, i) => ({
|
||||
amt: 500000,
|
||||
id: 'f4eb118383c2b09d8c7289ce21c25900cfb4545d46c47ed23a31ad2aa57ce835',
|
||||
idBytes: '9OsRg4PCsJ2MconOIcJZAM+0VF1GxH7SOjGtKqV86DU=',
|
||||
type: i % 3,
|
||||
state: i,
|
||||
initiationTime: 1586390353623905000,
|
||||
lastUpdateTime: 1586398369729857000,
|
||||
htlcAddress: 'bcrt1qzu4077erkr78k52yuf2rwkk6ayr6m3wtazdfz2qqmd7taa5vvy9s5d75gd',
|
||||
costServer: 66,
|
||||
costOnchain: 6812,
|
||||
costOffchain: 2,
|
||||
})),
|
||||
},
|
||||
'lnrpc.Lightning.GetInfo': samples.lndGetInfo,
|
||||
'lnrpc.Lightning.ListChannels': samples.lndListChannels,
|
||||
'looprpc.SwapClient.ListSwaps': samples.loopListSwaps,
|
||||
};
|
||||
|
|
|
|||
31
app/src/__stories__/Layout.stories.tsx
Normal file
31
app/src/__stories__/Layout.stories.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import React from 'react';
|
||||
import '../App.scss';
|
||||
import { Layout } from '../components/layout';
|
||||
|
||||
export default {
|
||||
title: 'Layout',
|
||||
component: Layout,
|
||||
};
|
||||
|
||||
export const Empty = () => <Layout />;
|
||||
|
||||
export const WithContent = () => (
|
||||
<Layout>
|
||||
<h1>Lorem ipsum dolor sit amet</h1>
|
||||
{[...Array(10)].map((_, i) => (
|
||||
<p key={i}>
|
||||
At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis
|
||||
praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias
|
||||
excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui
|
||||
officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem
|
||||
rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est
|
||||
eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere
|
||||
possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem
|
||||
quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et
|
||||
voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic
|
||||
tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias
|
||||
consequatur aut perferendis doloribus asperiores repellat.
|
||||
</p>
|
||||
))}
|
||||
</Layout>
|
||||
);
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import App from './App';
|
||||
import App from '../App';
|
||||
|
||||
it('renders the App', () => {
|
||||
const { getByText } = render(<App />);
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import ChannelAction from 'action/channel';
|
||||
import LndApi from 'api/lnd';
|
||||
import { GrpcClient, LndApi } from 'api';
|
||||
import { Store } from 'store';
|
||||
|
||||
describe('ChannelAction', () => {
|
||||
|
|
@ -7,7 +7,8 @@ describe('ChannelAction', () => {
|
|||
let channel: ChannelAction;
|
||||
|
||||
beforeEach(() => {
|
||||
const lndApiMock = new LndApi();
|
||||
const grpc = new GrpcClient();
|
||||
const lndApiMock = new LndApi(grpc);
|
||||
store = new Store();
|
||||
channel = new ChannelAction(store, lndApiMock);
|
||||
});
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import NodeAction from 'action/node';
|
||||
import LndApi from 'api/lnd';
|
||||
import { GrpcClient, LndApi } from 'api';
|
||||
import { Store } from 'store';
|
||||
|
||||
describe('NodeAction', () => {
|
||||
|
|
@ -7,7 +7,8 @@ describe('NodeAction', () => {
|
|||
let node: NodeAction;
|
||||
|
||||
beforeEach(() => {
|
||||
const lndApiMock = new LndApi();
|
||||
const grpc = new GrpcClient();
|
||||
const lndApiMock = new LndApi(grpc);
|
||||
store = new Store();
|
||||
node = new NodeAction(store, lndApiMock);
|
||||
});
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import SwapAction from 'action/swap';
|
||||
import LoopApi from 'api/loop';
|
||||
import { GrpcClient, LoopApi } from 'api';
|
||||
import { Store } from 'store';
|
||||
|
||||
describe('SwapAction', () => {
|
||||
|
|
@ -7,7 +7,8 @@ describe('SwapAction', () => {
|
|||
let loop: SwapAction;
|
||||
|
||||
beforeEach(() => {
|
||||
const loopApiMock = new LoopApi();
|
||||
const grpc = new GrpcClient();
|
||||
const loopApiMock = new LoopApi(grpc);
|
||||
store = new Store();
|
||||
loop = new SwapAction(store, loopApiMock);
|
||||
});
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { action, toJS } from 'mobx';
|
||||
import { actionLog as log } from 'util/log';
|
||||
import LndApi from 'api/lnd';
|
||||
import { LndApi } from 'api';
|
||||
import { Store } from 'store';
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,16 +1,35 @@
|
|||
import GrpcClient from 'api/grpc';
|
||||
import LndApi from 'api/lnd';
|
||||
import LoopApi from 'api/loop';
|
||||
import store from 'store';
|
||||
import { Store } from 'store';
|
||||
import ChannelAction from './channel';
|
||||
import NodeAction from './node';
|
||||
import SwapAction from './swap';
|
||||
|
||||
//
|
||||
// Create mobx actions
|
||||
//
|
||||
export interface StoreActions {
|
||||
node: NodeAction;
|
||||
channel: ChannelAction;
|
||||
swap: SwapAction;
|
||||
}
|
||||
|
||||
export const lndApi = new LndApi();
|
||||
export const loopApi = new LoopApi();
|
||||
export const node = new NodeAction(store, lndApi);
|
||||
export const channel = new ChannelAction(store, lndApi);
|
||||
export const swap = new SwapAction(store, loopApi);
|
||||
/**
|
||||
* Creates actions that modify the state of the given mobx store
|
||||
* @param store the Store instance that the actions will modify
|
||||
*/
|
||||
export const createActions = (store: Store): StoreActions => {
|
||||
// low level dependencies
|
||||
const grpc = new GrpcClient();
|
||||
const lndApi = new LndApi(grpc);
|
||||
const loopApi = new LoopApi(grpc);
|
||||
|
||||
// actions exposed to UI components
|
||||
const node = new NodeAction(store, lndApi);
|
||||
const channel = new ChannelAction(store, lndApi);
|
||||
const swap = new SwapAction(store, loopApi);
|
||||
|
||||
return {
|
||||
node,
|
||||
channel,
|
||||
swap,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { action, toJS } from 'mobx';
|
||||
import { actionLog as log } from 'util/log';
|
||||
import LndApi from 'api/lnd';
|
||||
import { LndApi } from 'api';
|
||||
import { Store } from 'store';
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { action, toJS } from 'mobx';
|
||||
import { SwapState, SwapType } from 'types/generated/loop_pb';
|
||||
import { actionLog as log } from 'util/log';
|
||||
import LoopApi from 'api/loop';
|
||||
import { LoopApi } from 'api';
|
||||
import { Store } from 'store';
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -5,37 +5,41 @@ import { UnaryMethodDefinition } from '@improbable-eng/grpc-web/dist/typings/ser
|
|||
import { DEV_HOST } from 'config';
|
||||
import { grpcLog as log } from 'util/log';
|
||||
|
||||
/**
|
||||
* Executes a single GRPC request and returns a promise which will resolve with the response
|
||||
* @param methodDescriptor the GRPC method to call on the service
|
||||
* @param request The GRPC request message to send
|
||||
* @param metadata headers to include with the request
|
||||
*/
|
||||
export const grpcRequest = <TReq extends ProtobufMessage, TRes extends ProtobufMessage>(
|
||||
methodDescriptor: UnaryMethodDefinition<TReq, TRes>,
|
||||
request: TReq,
|
||||
metadata?: Metadata.ConstructorArg,
|
||||
): Promise<TRes> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
log.debug(
|
||||
`Request: ${methodDescriptor.service.serviceName}.${methodDescriptor.methodName}`,
|
||||
);
|
||||
log.debug(` - req: `, request.toObject());
|
||||
grpc.unary(methodDescriptor, {
|
||||
host: DEV_HOST,
|
||||
request,
|
||||
metadata,
|
||||
onEnd: ({ status, statusMessage, headers, message, trailers }) => {
|
||||
log.debug(' - status', status, statusMessage);
|
||||
log.debug(' - headers', headers);
|
||||
if (status === grpc.Code.OK && message) {
|
||||
log.debug(' - message', message.toObject());
|
||||
resolve(message as TRes);
|
||||
} else {
|
||||
reject(new Error(`${status}: ${statusMessage}`));
|
||||
}
|
||||
log.debug(' - trailers', trailers);
|
||||
},
|
||||
class GrpcClient {
|
||||
/**
|
||||
* Executes a single GRPC request and returns a promise which will resolve with the response
|
||||
* @param methodDescriptor the GRPC method to call on the service
|
||||
* @param request The GRPC request message to send
|
||||
* @param metadata headers to include with the request
|
||||
*/
|
||||
request<TReq extends ProtobufMessage, TRes extends ProtobufMessage>(
|
||||
methodDescriptor: UnaryMethodDefinition<TReq, TRes>,
|
||||
request: TReq,
|
||||
metadata?: Metadata.ConstructorArg,
|
||||
): Promise<TRes> {
|
||||
return new Promise((resolve, reject) => {
|
||||
log.debug(
|
||||
`Request: ${methodDescriptor.service.serviceName}.${methodDescriptor.methodName}`,
|
||||
);
|
||||
log.debug(` - req: `, request.toObject());
|
||||
grpc.unary(methodDescriptor, {
|
||||
host: DEV_HOST,
|
||||
request,
|
||||
metadata,
|
||||
onEnd: ({ status, statusMessage, headers, message, trailers }) => {
|
||||
log.debug(' - status', status, statusMessage);
|
||||
log.debug(' - headers', headers);
|
||||
if (status === grpc.Code.OK && message) {
|
||||
log.debug(' - message', message.toObject());
|
||||
resolve(message as TRes);
|
||||
} else {
|
||||
reject(new Error(`${status}: ${statusMessage}`));
|
||||
}
|
||||
log.debug(' - trailers', trailers);
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default GrpcClient;
|
||||
|
|
|
|||
3
app/src/api/index.ts
Normal file
3
app/src/api/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { default as GrpcClient } from './grpc';
|
||||
export { default as LndApi } from './lnd';
|
||||
export { default as LoopApi } from './loop';
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import * as LND from 'types/generated/lnd_pb';
|
||||
import { Lightning } from 'types/generated/lnd_pb_service';
|
||||
import { DEV_MACAROON } from 'config';
|
||||
import { grpcRequest } from './grpc';
|
||||
import GrpcClient from './grpc';
|
||||
|
||||
/**
|
||||
* An API wrapper to communicate with the LND node via GRPC
|
||||
|
|
@ -12,12 +12,18 @@ class LndApi {
|
|||
macaroon: DEV_MACAROON,
|
||||
};
|
||||
|
||||
private _grpc: GrpcClient;
|
||||
|
||||
constructor(grpc: GrpcClient) {
|
||||
this._grpc = grpc;
|
||||
}
|
||||
|
||||
/**
|
||||
* call the LND `GetInfo` RPC and return the response
|
||||
*/
|
||||
async getInfo(): Promise<LND.GetInfoResponse.AsObject> {
|
||||
const req = new LND.GetInfoRequest();
|
||||
const res = await grpcRequest(Lightning.GetInfo, req, this._meta);
|
||||
const res = await this._grpc.request(Lightning.GetInfo, req, this._meta);
|
||||
return res.toObject();
|
||||
}
|
||||
|
||||
|
|
@ -26,7 +32,7 @@ class LndApi {
|
|||
*/
|
||||
async listChannels(): Promise<LND.ListChannelsResponse.AsObject> {
|
||||
const req = new LND.ListChannelsRequest();
|
||||
const res = await grpcRequest(Lightning.ListChannels, req, this._meta);
|
||||
const res = await this._grpc.request(Lightning.ListChannels, req, this._meta);
|
||||
return res.toObject();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import * as LOOP from 'types/generated/loop_pb';
|
||||
import { SwapClient } from 'types/generated/loop_pb_service';
|
||||
import { grpcRequest } from './grpc';
|
||||
import GrpcClient from './grpc';
|
||||
|
||||
/**
|
||||
* An API wrapper to communicate with the Loop daemon via GRPC
|
||||
|
|
@ -10,12 +10,18 @@ class LoopApi {
|
|||
'X-Grpc-Backend': 'loop',
|
||||
};
|
||||
|
||||
private _grpc: GrpcClient;
|
||||
|
||||
constructor(grpc: GrpcClient) {
|
||||
this._grpc = grpc;
|
||||
}
|
||||
|
||||
/**
|
||||
* call the LND `ListSwaps` RPC and return the response
|
||||
*/
|
||||
async listSwaps(): Promise<LOOP.ListSwapsResponse.AsObject> {
|
||||
const req = new LOOP.ListSwapsRequest();
|
||||
const res = await grpcRequest(SwapClient.ListSwaps, req, this._meta);
|
||||
const res = await this._grpc.request(SwapClient.ListSwaps, req, this._meta);
|
||||
return res.toObject();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
BIN
app/src/assets/fonts/OpenSans-Bold.ttf
Executable file
BIN
app/src/assets/fonts/OpenSans-Bold.ttf
Executable file
Binary file not shown.
BIN
app/src/assets/fonts/OpenSans-BoldItalic.ttf
Executable file
BIN
app/src/assets/fonts/OpenSans-BoldItalic.ttf
Executable file
Binary file not shown.
BIN
app/src/assets/fonts/OpenSans-ExtraBold.ttf
Executable file
BIN
app/src/assets/fonts/OpenSans-ExtraBold.ttf
Executable file
Binary file not shown.
BIN
app/src/assets/fonts/OpenSans-ExtraBoldItalic.ttf
Executable file
BIN
app/src/assets/fonts/OpenSans-ExtraBoldItalic.ttf
Executable file
Binary file not shown.
BIN
app/src/assets/fonts/OpenSans-Italic.ttf
Executable file
BIN
app/src/assets/fonts/OpenSans-Italic.ttf
Executable file
Binary file not shown.
BIN
app/src/assets/fonts/OpenSans-Light.ttf
Executable file
BIN
app/src/assets/fonts/OpenSans-Light.ttf
Executable file
Binary file not shown.
BIN
app/src/assets/fonts/OpenSans-LightItalic.ttf
Executable file
BIN
app/src/assets/fonts/OpenSans-LightItalic.ttf
Executable file
Binary file not shown.
BIN
app/src/assets/fonts/OpenSans-Regular.ttf
Executable file
BIN
app/src/assets/fonts/OpenSans-Regular.ttf
Executable file
Binary file not shown.
BIN
app/src/assets/fonts/OpenSans-SemiBold.ttf
Executable file
BIN
app/src/assets/fonts/OpenSans-SemiBold.ttf
Executable file
Binary file not shown.
BIN
app/src/assets/fonts/OpenSans-SemiBoldItalic.ttf
Executable file
BIN
app/src/assets/fonts/OpenSans-SemiBoldItalic.ttf
Executable file
Binary file not shown.
202
app/src/assets/fonts/OpenSans_LICENSE.txt
Executable file
202
app/src/assets/fonts/OpenSans_LICENSE.txt
Executable file
|
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
48
app/src/components/layout/Layout.tsx
Normal file
48
app/src/components/layout/Layout.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import React from 'react';
|
||||
import { styled } from 'components/theme';
|
||||
import Sidebar from './Sidebar';
|
||||
|
||||
const Styled = {
|
||||
Background: styled.div`
|
||||
min-height: 100vh;
|
||||
color: ${props => props.theme.colors.white};
|
||||
background-color: ${props => props.theme.colors.blue};
|
||||
font-family: ${props => props.theme.fonts.regular};
|
||||
font-size: ${props => props.theme.sizes.m};
|
||||
`,
|
||||
Container: styled.div`
|
||||
min-height: 100vh;
|
||||
width: 1440px;
|
||||
margin: 0 auto;
|
||||
`,
|
||||
Aside: styled.aside`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
background-color: ${props => props.theme.colors.darkBlue};
|
||||
width: 285px;
|
||||
padding: 17px;
|
||||
`,
|
||||
Content: styled.div`
|
||||
margin-left: 285px;
|
||||
padding: 15px;
|
||||
`,
|
||||
};
|
||||
|
||||
const Layout: React.FC = ({ children }) => {
|
||||
const { Background, Container, Aside, Content } = Styled;
|
||||
return (
|
||||
<Background>
|
||||
<Container>
|
||||
<Aside>
|
||||
<Sidebar />
|
||||
</Aside>
|
||||
<Content>
|
||||
<div className="container">{children}</div>
|
||||
</Content>
|
||||
</Container>
|
||||
</Background>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
62
app/src/components/layout/Sidebar.tsx
Normal file
62
app/src/components/layout/Sidebar.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import React from 'react';
|
||||
import { styled } from 'components/theme';
|
||||
|
||||
const Title = styled.div`
|
||||
font-size: ${props => props.theme.sizes.s};
|
||||
font-family: ${props => props.theme.fonts.semiBold};
|
||||
letter-spacing: 0;
|
||||
line-height: 19px;
|
||||
padding: 8px 14px;
|
||||
color: ${props => props.theme.colors.gray};
|
||||
`;
|
||||
|
||||
const Nav = styled.ul`
|
||||
padding-left: 0;
|
||||
margin-bottom: 0;
|
||||
list-style: none;
|
||||
`;
|
||||
|
||||
const NavItem = styled.li`
|
||||
font-size: ${props => props.theme.sizes.s};
|
||||
|
||||
a {
|
||||
display: block;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
padding: 0 12px;
|
||||
border-left: 3px solid transparent;
|
||||
color: ${props => props.theme.colors.whitish};
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
border-left: 3px solid ${props => props.theme.colors.pink};
|
||||
}
|
||||
}
|
||||
|
||||
&.active a {
|
||||
border-left: 3px solid ${props => props.theme.colors.whitish};
|
||||
background-color: ${props => props.theme.colors.blue};
|
||||
margin-right: -17px;
|
||||
|
||||
&:hover {
|
||||
border-left: 3px solid ${props => props.theme.colors.pink};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Sidebar: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<Title>MENU</Title>
|
||||
<Nav>
|
||||
<NavItem className="active">
|
||||
<a href="#temp">Lightning Loop</a>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<a href="#temp">Settings</a>
|
||||
</NavItem>
|
||||
</Nav>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default Sidebar;
|
||||
1
app/src/components/layout/index.ts
Normal file
1
app/src/components/layout/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default as Layout } from './Layout';
|
||||
122
app/src/components/pages/SamplePage.tsx
Normal file
122
app/src/components/pages/SamplePage.tsx
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import usePrefixedTranslation from 'hooks/usePrefixedTranslation';
|
||||
import { useActions, useStore } from 'store/provider';
|
||||
|
||||
const SamplePage: React.FC = () => {
|
||||
const store = useStore();
|
||||
const { node, channel, swap } = useActions();
|
||||
const { l } = usePrefixedTranslation('App');
|
||||
useEffect(() => {
|
||||
// fetch node info when the component is mounted
|
||||
const fetchInfo = async () => {
|
||||
try {
|
||||
await node.getInfo();
|
||||
} catch (error) {
|
||||
console.log('Failed to fetch node info', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchInfo();
|
||||
}, [node]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="text-center">
|
||||
<h3>Lightning Loop</h3>
|
||||
</header>
|
||||
<section className="mt-4">
|
||||
<h1>{l('App.nodeInfo')}</h1>
|
||||
{store.info && (
|
||||
<table className="table" style={{ color: '#fff' }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{l('pubkey')}</th>
|
||||
<td>{store.info.identityPubkey}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{l('alias')}</th>
|
||||
<td>{store.info.alias}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{l('version')}</th>
|
||||
<td>{store.info.version}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{l('numChannels')}</th>
|
||||
<td>{store.info.numActiveChannels}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</section>
|
||||
<section className="mt-4">
|
||||
<h2>
|
||||
{store.channels.length} Channels
|
||||
<button
|
||||
className="btn btn-outline-light float-right"
|
||||
onClick={channel.getChannels}
|
||||
>
|
||||
Fetch
|
||||
</button>
|
||||
</h2>
|
||||
<table className="table" style={{ color: '#fff' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Can Receive</td>
|
||||
<td>Can Send</td>
|
||||
<td>In Fee %</td>
|
||||
<td>Up time %</td>
|
||||
<td>Volume (24h)</td>
|
||||
<td>Peer/Alias</td>
|
||||
<td>Capacity</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{store.channels.map(c => (
|
||||
<tr key={c.chanId}>
|
||||
<td>{c.remoteBalance}</td>
|
||||
<td>{c.localBalance}</td>
|
||||
<td></td>
|
||||
<td>{c.uptime}</td>
|
||||
<td></td>
|
||||
<td>{c.remotePubkey.substring(0, 12)}</td>
|
||||
<td>{c.capacity}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<section className="mt-4">
|
||||
<h2>
|
||||
{store.swaps.length} Swaps
|
||||
<button className="btn btn-outline-light float-right" onClick={swap.listSwaps}>
|
||||
Fetch
|
||||
</button>
|
||||
</h2>
|
||||
<table className="table" style={{ color: '#fff' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Date</td>
|
||||
<td>Type</td>
|
||||
<td>Amount</td>
|
||||
<td>Status</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{store.swaps.map(s => (
|
||||
<tr key={s.id}>
|
||||
<td>{s.createdOn.toString()}</td>
|
||||
<td>{s.type}</td>
|
||||
<td>{s.amount.toString()}</td>
|
||||
<td>{s.status}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default observer(SamplePage);
|
||||
33
app/src/components/theme.tsx
Normal file
33
app/src/components/theme.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import React from 'react';
|
||||
import emotionStyled, { CreateStyled } from '@emotion/styled/macro';
|
||||
import { ThemeProvider as EmotionThemeProvider } from 'emotion-theming';
|
||||
|
||||
const theme = {
|
||||
fonts: {
|
||||
light: "'OpenSans Light'",
|
||||
regular: "'OpenSans Regular'",
|
||||
semiBold: "'OpenSans SemiBold'",
|
||||
bold: "'OpenSans Bold'",
|
||||
extraBold: "'OpenSans ExtraBold'",
|
||||
},
|
||||
sizes: {
|
||||
s: '14px',
|
||||
m: '18px',
|
||||
l: '22px',
|
||||
xl: '27px',
|
||||
},
|
||||
colors: {
|
||||
blue: '#252f4a',
|
||||
darkBlue: '#212133',
|
||||
gray: '#848a99',
|
||||
white: '#ffffff',
|
||||
whitish: '#f5f5f5',
|
||||
pink: '#f5406e',
|
||||
},
|
||||
};
|
||||
|
||||
export const styled = emotionStyled as CreateStyled<typeof theme>;
|
||||
|
||||
export const ThemeProvider: React.FC = ({ children }) => {
|
||||
return <EmotionThemeProvider theme={theme}>{children}</EmotionThemeProvider>;
|
||||
};
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import 'mobx-react/batchingForReactDom';
|
||||
import 'mobx-react-lite/batchingForReactDom';
|
||||
import './i18n';
|
||||
import './index.css';
|
||||
import { log } from 'util/log';
|
||||
|
|
|
|||
|
|
@ -10,4 +10,5 @@ export class Store {
|
|||
@observable swaps: Swap[] = [];
|
||||
}
|
||||
|
||||
export default new Store();
|
||||
// re-export from provider
|
||||
export { StoreProvider, useStore, useActions } from './provider';
|
||||
|
|
|
|||
59
app/src/store/provider.tsx
Normal file
59
app/src/store/provider.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { useLocalStore } from 'mobx-react-lite';
|
||||
import { createActions, StoreActions } from 'action';
|
||||
import { Store } from 'store';
|
||||
|
||||
interface ContextData {
|
||||
store: Store;
|
||||
actions: StoreActions;
|
||||
}
|
||||
|
||||
/**
|
||||
* The react context used to cache the store instance
|
||||
*/
|
||||
const StoreContext = React.createContext<ContextData | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* A Context Provider component which should wrap any components that need to
|
||||
* receive a store via the `useStore` hook
|
||||
* @param store the store instance to provide to child components via `useStore`
|
||||
* @param actions the actions to provide to child components via `useActions`. If
|
||||
* this parameter is not supplied, then the actions will be created using the store
|
||||
* provided
|
||||
*/
|
||||
export const StoreProvider: React.FC<{
|
||||
store: Store;
|
||||
actions?: StoreActions;
|
||||
}> = ({ children, store, actions }) => {
|
||||
const context: ContextData = {
|
||||
store: useLocalStore(() => store),
|
||||
actions: actions || createActions(store),
|
||||
};
|
||||
return <StoreContext.Provider value={context}>{children}</StoreContext.Provider>;
|
||||
};
|
||||
|
||||
/**
|
||||
* A React hook used to access the global store from child components that may be
|
||||
* nested many levels deep in the component tree
|
||||
*/
|
||||
export const useStore = (): Store => {
|
||||
const data = useContext(StoreContext);
|
||||
if (!data) {
|
||||
// raise an error if the context data has not been provided in a higher level component
|
||||
throw new Error('useStore must be used within a StoreProvider.');
|
||||
}
|
||||
return data.store;
|
||||
};
|
||||
|
||||
/**
|
||||
* A React hook used to access the global actions from child components that may be
|
||||
* nested many levels deep in the component tree
|
||||
*/
|
||||
export const useActions = (): StoreActions => {
|
||||
const data = useContext(StoreContext);
|
||||
if (!data) {
|
||||
// raise an error if the context data has not been provided in a higher level component
|
||||
throw new Error('useActions must be used within a StoreProvider.');
|
||||
}
|
||||
return data.actions;
|
||||
};
|
||||
96
app/src/util/sampleData.ts
Normal file
96
app/src/util/sampleData.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
import * as LND from 'types/generated/lnd_pb';
|
||||
import * as LOOP from 'types/generated/loop_pb';
|
||||
|
||||
//
|
||||
// LND API Responses
|
||||
//
|
||||
|
||||
export const lndGetInfo: LND.GetInfoResponse.AsObject = {
|
||||
version: '0.9.0-beta commit=v0.9.0-beta',
|
||||
identityPubkey: '038b3fc29cfc195c9b190d86ad2d40ce7550a5c6f13941f53c7d7ac5b25c912a6c',
|
||||
alias: 'alice',
|
||||
color: '#cccccc',
|
||||
numPendingChannels: 0,
|
||||
numActiveChannels: 1,
|
||||
numInactiveChannels: 0,
|
||||
numPeers: 1,
|
||||
blockHeight: 185,
|
||||
blockHash: '547d3dcfb7d56532bed2efdeea0d400f11167b34d493bcd45fedb21f2ef7ed43',
|
||||
bestHeaderTimestamp: 1586548672,
|
||||
syncedToChain: false,
|
||||
syncedToGraph: true,
|
||||
testnet: false,
|
||||
chainsList: [{ chain: 'bitcoin', network: 'regtest' }],
|
||||
urisList: [
|
||||
'038b3fc29cfc195c9b190d86ad2d40ce7550a5c6f13941f53c7d7ac5b25c912a6c@172.18.0.7:9735',
|
||||
],
|
||||
featuresMap: [
|
||||
[0, { name: 'data-loss-protect', isRequired: true, isKnown: true }],
|
||||
[13, { name: 'static-remote-key', isRequired: false, isKnown: true }],
|
||||
[15, { name: 'payment-addr', isRequired: false, isKnown: true }],
|
||||
[17, { name: 'multi-path-payments', isRequired: false, isKnown: true }],
|
||||
[5, { name: 'upfront-shutdown-script', isRequired: false, isKnown: true }],
|
||||
[7, { name: 'gossip-queries', isRequired: false, isKnown: true }],
|
||||
[9, { name: 'tlv-onion', isRequired: false, isKnown: true }],
|
||||
],
|
||||
};
|
||||
|
||||
export const lndListChannels: LND.ListChannelsResponse.AsObject = {
|
||||
channelsList: [
|
||||
{
|
||||
active: true,
|
||||
remotePubkey: '037136742c67e24681f36542f7c8916aa6f6fdf665c1dca2a107425503cff94501',
|
||||
channelPoint: '0ef6a4ae3d8f800f4eb736f0776f5d3a72571615a1b7218ab17c9a43f85d8949:0',
|
||||
chanId: '124244814004224',
|
||||
capacity: 15000000,
|
||||
localBalance: 9988660,
|
||||
remoteBalance: 4501409,
|
||||
commitFee: 11201,
|
||||
commitWeight: 896,
|
||||
feePerKw: 12500,
|
||||
unsettledBalance: 498730,
|
||||
totalSatoshisSent: 1338,
|
||||
totalSatoshisReceived: 499929,
|
||||
numUpdates: 6,
|
||||
pendingHtlcsList: [
|
||||
{
|
||||
incoming: false,
|
||||
amount: 498730,
|
||||
hashLock: 'pl8fmsyoSqEQFQCw6Zu9e1aIlFnMz5H+hW2mmh3kRlI=',
|
||||
expirationHeight: 285,
|
||||
},
|
||||
],
|
||||
csvDelay: 1802,
|
||||
pb_private: false,
|
||||
initiator: true,
|
||||
chanStatusFlags: 'ChanStatusDefault',
|
||||
localChanReserveSat: 150000,
|
||||
remoteChanReserveSat: 150000,
|
||||
staticRemoteKey: true,
|
||||
lifetime: 21802,
|
||||
uptime: 21802,
|
||||
closeAddress: '',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
//
|
||||
// LND API Responses
|
||||
//
|
||||
|
||||
export const loopListSwaps: LOOP.ListSwapsResponse.AsObject = {
|
||||
swapsList: [...Array(7)].map((x, i) => ({
|
||||
amt: 500000,
|
||||
id: 'f4eb118383c2b09d8c7289ce21c25900cfb4545d46c47ed23a31ad2aa57ce835',
|
||||
idBytes: '9OsRg4PCsJ2MconOIcJZAM+0VF1GxH7SOjGtKqV86DU=',
|
||||
type: (i % 3) as any,
|
||||
state: i as any,
|
||||
initiationTime: 1586390353623905000,
|
||||
lastUpdateTime: 1586398369729857000,
|
||||
htlcAddress: 'bcrt1qzu4077erkr78k52yuf2rwkk6ayr6m3wtazdfz2qqmd7taa5vvy9s5d75gd',
|
||||
costServer: 66,
|
||||
costOnchain: 6812,
|
||||
costOffchain: 2,
|
||||
})),
|
||||
};
|
||||
4223
app/yarn.lock
4223
app/yarn.lock
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue