Skip to content

add initial mobx infrastructure #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 14, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ Requirements: [Go](https://golang.org/doc/install), [protoc](https://github.com/
## One-time Setup

Create certificate for browser to backend proxy communication

```
openssl genrsa -out https.key 2048
openssl req -new -x509 -key https.key -out https.cert -days 365
```

Install client app dependencies

```sh
npm install -g yarn # if yarn isn't already installed
cd app/
Expand All @@ -24,11 +26,22 @@ yarn
- Start backend server
```sh
go run . --lndhost=localhost:10011 --loophost=localhost:11010
```
```
- Run the client app in a separate terminal
```sh
cd app
yarn start
```

Open browser at https://localhost:3000 and accept invalid cert (may not work in Chrome, use Firefox)

## Testing

- Run all unit tests and output coverage
```sh
yarn test:ci
```
- Run tests on locally modified files in watch mode
```sh
yarn test
```
9 changes: 9 additions & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"start": "BROWSER=none react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"test:ci": "CI=true yarn test --coverage",
"eject": "react-scripts eject",
"protos": "./scripts/build-protos.sh"
},
Expand Down Expand Up @@ -42,6 +43,14 @@
"src/types/generated/**/*.js"
]
},
"jest": {
"collectCoverageFrom": [
"src/**/*.{js,jsx,ts,tsx}",
"!src/**/*.d.ts",
"!src/types/**/*.{js,ts}",
"!src/index.tsx"
]
},
"browserslist": {
"production": [
">0.2%",
Expand Down
4 changes: 2 additions & 2 deletions app/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import React from 'react';
import { render } from '@testing-library/react';
import App from './App';

test('renders learn react link', () => {
it('renders the App', () => {
const { getByText } = render(<App />);
const linkElement = getByText(/learn react/i);
const linkElement = getByText('Node Info');
expect(linkElement).toBeInTheDocument();
});
6 changes: 3 additions & 3 deletions app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, { useEffect } from 'react';
import { observer } from 'mobx-react';
import './App.css';
import { channel, swap, node } from 'action';
import { channel, node, swap } from 'action';
import store from 'store';
import { observer } from 'mobx-react';

const App = () => {
useEffect(() => {
Expand All @@ -13,9 +13,9 @@ const App = () => {

return (
<div className="App">
<p>Node Info</p>
{store.info && (
<>
<p>Node Info</p>
<table className="App-table">
<tbody>
<tr>
Expand Down
117 changes: 117 additions & 0 deletions app/src/__mocks__/@improbable-eng/grpc-web.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
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';

// mock grpc module
export const grpc = {
Code: {
OK: 0,
Canceled: 1,
},
// mock unary function to simulate GRPC requests
unary: <TReq extends ProtobufMessage, TRes extends ProtobufMessage>(
methodDescriptor: UnaryMethodDefinition<TReq, TRes>,
props: UnaryRpcOptions<TReq, TRes>,
) => {
const path = `${methodDescriptor.service.serviceName}.${methodDescriptor.methodName}`;
// return a response by calling the onEnd function
props.onEnd({
status: 0,
statusMessage: '',
// the message returned should have a toObject function
message: {
toObject: () => mockApiResponses[path],
} as TRes,
headers: {} as any,
trailers: {} as any,
});
},
};

// 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,
})),
},
};
20 changes: 20 additions & 0 deletions app/src/action/channel.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import ChannelAction from 'action/channel';
import LndApi from 'api/lnd';
import { Store } from 'store';

describe('ChannelAction', () => {
let store: Store;
let channel: ChannelAction;

beforeEach(() => {
const lndApiMock = new LndApi();
store = new Store();
channel = new ChannelAction(store, lndApiMock);
});

it('should fetch list of channels', async () => {
expect(store.channels).toEqual([]);
await channel.getChannels();
expect(store.channels).toHaveLength(1);
});
});
13 changes: 11 additions & 2 deletions app/src/action/channel.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Store } from 'store';
import { action } from 'mobx';
import LndApi from 'api/lnd';
import { Store } from 'store';

/**
* Action used to update the channel state in the store with responses from
Expand All @@ -19,7 +19,16 @@ class ChannelAction {
* fetch channels from the LND RPC
*/
@action.bound async getChannels() {
this._store.channels = await this._lnd.listChannels();
const channels = await this._lnd.listChannels();
this._store.channels = channels.channelsList.map(c => ({
chanId: c.chanId,
remotePubkey: c.remotePubkey,
capacity: c.capacity,
localBalance: c.localBalance,
remoteBalance: c.remoteBalance,
uptime: c.uptime,
active: c.active,
}));
}
}

Expand Down
21 changes: 21 additions & 0 deletions app/src/action/node.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import NodeAction from 'action/node';
import LndApi from 'api/lnd';
import { Store } from 'store';

describe('NodeAction', () => {
let store: Store;
let node: NodeAction;

beforeEach(() => {
const lndApiMock = new LndApi();
store = new Store();
node = new NodeAction(store, lndApiMock);
});

it('should fetch list of channels', async () => {
expect(store.info).toBeUndefined();
await node.getInfo();
expect(store.info).toBeDefined();
expect(store.info?.alias).toEqual('alice');
});
});
20 changes: 20 additions & 0 deletions app/src/action/swap.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import SwapAction from 'action/swap';
import LoopApi from 'api/loop';
import { Store } from 'store';

describe('SwapAction', () => {
let store: Store;
let loop: SwapAction;

beforeEach(() => {
const loopApiMock = new LoopApi();
store = new Store();
loop = new SwapAction(store, loopApiMock);
});

it('should fetch list of channels', async () => {
expect(store.swaps).toEqual([]);
await loop.listSwaps();
expect(store.swaps).toHaveLength(7);
});
});
9 changes: 1 addition & 8 deletions app/src/api/grpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,11 @@ export const grpcRequest = <TReq extends ProtobufMessage, TRes extends ProtobufM
request,
metadata,
onEnd: ({ status, statusMessage, headers, message, trailers }) => {
console.log(
`GRPC Request: ${methodDescriptor.service.serviceName}.${methodDescriptor.methodName}`,
);
console.log(' - status', status, statusMessage);
console.log(' - headers', headers);
if (status === grpc.Code.OK && message) {
resolve(message as TRes);
console.log(' - message', message.toObject());
} else {
reject(new Error(statusMessage));
reject(new Error(`${status}: ${statusMessage}`));
}
console.log(' - trailers', trailers);
},
});
});
Expand Down
29 changes: 9 additions & 20 deletions app/src/api/lnd.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,33 @@
import { GetInfoResponse, ListChannelsRequest } from 'types/generated/lnd_pb';
import * as LND from 'types/generated/lnd_pb';
import { Lightning } from 'types/generated/lnd_pb_service';
import { Channel, NodeInfo } from 'types/state';
import { DEV_MACAROON } from 'config';
import { grpcRequest } from './grpc';

/**
* An API wrapper to communicate with the LND node via GRPC
*/
class LndApi {
private _meta = {
_meta = {
'X-Grpc-Backend': 'lnd',
macaroon: DEV_MACAROON,
};

/**
* call the LND `GetInfo` RPC and return the response
*/
async getInfo(): Promise<NodeInfo> {
const res = await grpcRequest(Lightning.GetInfo, new GetInfoResponse(), this._meta);
async getInfo(): Promise<LND.GetInfoResponse.AsObject> {
const req = new LND.GetInfoResponse();
const res = await grpcRequest(Lightning.GetInfo, req, this._meta);
return res.toObject();
}

/**
* call the LND `ListChannels` RPC and return the response
*/
async listChannels(): Promise<Channel[]> {
const res = await grpcRequest(
Lightning.ListChannels,
new ListChannelsRequest(),
this._meta,
);
return res.toObject().channelsList.map(c => ({
chanId: c.chanId,
remotePubkey: c.remotePubkey,
capacity: c.capacity,
localBalance: c.localBalance,
remoteBalance: c.remoteBalance,
uptime: c.uptime,
active: c.active,
}));
async listChannels(): Promise<LND.ListChannelsResponse.AsObject> {
const req = new LND.ListChannelsRequest();
const res = await grpcRequest(Lightning.ListChannels, req, this._meta);
return res.toObject();
}
}

Expand Down
Loading