Skip to content

Commit 11ba8a7

Browse files
authored
Merge pull request #55 from lightninglabs/alias-fees
Add alias and fees to channel list
2 parents e8d1bca + 8abe969 commit 11ba8a7

File tree

18 files changed

+375
-52
lines changed

18 files changed

+375
-52
lines changed

app/src/__stories__/LoopPage.stories.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import React from 'react';
2+
import { observable, ObservableMap, values } from 'mobx';
23
import { useStore } from 'store';
4+
import { Channel } from 'store/models';
35
import { Layout } from 'components/layout';
46
import LoopPage from 'components/loop/LoopPage';
57

@@ -9,10 +11,20 @@ export default {
911
parameters: { contained: true },
1012
};
1113

14+
const channelSubset = (channels: ObservableMap<string, Channel>) => {
15+
const few = values(channels)
16+
.slice(0, 20)
17+
.reduce((result, c) => {
18+
result[c.chanId] = c;
19+
return result;
20+
}, {} as Record<string, Channel>);
21+
return observable.map(few);
22+
};
23+
1224
export const Default = () => {
13-
const store = useStore();
25+
const { channelStore } = useStore();
1426
// only use a small set of channels
15-
store.channelStore.sortedChannels.splice(10);
27+
channelStore.channels = channelSubset(channelStore.channels);
1628

1729
return <LoopPage />;
1830
};

app/src/__stories__/StoryWrapper.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { BalanceMode, Unit } from 'util/constants';
44
import { AuthenticationError } from 'util/errors';
55
import { sampleApiResponses } from 'util/tests/sampleData';
66
import { createStore, StoreProvider } from 'store';
7-
import { PersistentSettings } from 'store/stores/settingsStore';
87
import { Background } from 'components/base';
98
import { ThemeProvider } from 'components/theme';
109

@@ -26,13 +25,14 @@ const grpc = {
2625
// fake the AppStorage dependency so that settings aren't shared across stories
2726
class StoryAppStorage {
2827
set = () => undefined;
29-
get = (): PersistentSettings => ({
28+
get = (): any => ({
3029
sidebarVisible: true,
3130
unit: Unit.sats,
3231
balanceMode: BalanceMode.receive,
3332
});
3433
setSession = () => undefined;
3534
getSession = () => '';
35+
getCached = () => Promise.resolve({});
3636
}
3737

3838
// Create a store that pulls data from the mock GRPC and doesn't use

app/src/__tests__/components/loop/ChannelRow.spec.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ describe('ChannelRow component', () => {
4949
expect(getByText(channel.uptime.toString())).toBeInTheDocument();
5050
});
5151

52-
it('should display the peer pubkey', () => {
52+
it('should display the peer pubkey or alias', () => {
5353
const { getByText } = render();
54-
expect(getByText(channel.ellipsedPubkey)).toBeInTheDocument();
54+
expect(getByText(channel.aliasLabel)).toBeInTheDocument();
5555
});
5656

5757
it('should display the capacity', () => {

app/src/__tests__/store/authStore.spec.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
import { grpc } from '@improbable-eng/grpc-web';
22
import { waitFor } from '@testing-library/react';
3-
import AppStorage from 'util/appStorage';
43
import { lndListChannels } from 'util/tests/sampleData';
54
import { AuthStore, createStore, Store } from 'store';
6-
import { PersistentSettings } from 'store/stores/settingsStore';
75

86
const grpcMock = grpc as jest.Mocked<typeof grpc>;
9-
const appStorageMock = AppStorage as jest.Mock<AppStorage<PersistentSettings>>;
107

118
describe('AuthStore', () => {
129
let rootStore: Store;
@@ -47,8 +44,9 @@ describe('AuthStore', () => {
4744
});
4845

4946
it('should load credentials from session storage', async () => {
50-
const getMock = appStorageMock.mock.instances[0].getSession as jest.Mock;
51-
getMock.mockReturnValue('test-creds');
47+
jest
48+
.spyOn(window.sessionStorage.__proto__, 'getItem')
49+
.mockReturnValueOnce('test-creds');
5250
await store.init();
5351
expect(store.credentials).toBe('test-creds');
5452
});
@@ -62,8 +60,9 @@ describe('AuthStore', () => {
6260
}
6361
return undefined as any;
6462
});
65-
const getMock = appStorageMock.mock.instances[0].getSession as jest.Mock;
66-
getMock.mockReturnValue('test-creds');
63+
jest
64+
.spyOn(window.sessionStorage.__proto__, 'getItem')
65+
.mockReturnValueOnce('test-creds');
6766
await store.init();
6867
expect(store.credentials).toBe('');
6968
});

app/src/__tests__/store/channelStore.spec.ts

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ import { grpc } from '@improbable-eng/grpc-web';
44
import { waitFor } from '@testing-library/react';
55
import Big from 'big.js';
66
import { BalanceMode } from 'util/constants';
7-
import { lndChannelEvent, lndListChannels } from 'util/tests/sampleData';
7+
import {
8+
lndChannel,
9+
lndChannelEvent,
10+
lndGetChanInfo,
11+
lndGetNodeInfo,
12+
lndListChannels,
13+
} from 'util/tests/sampleData';
814
import { createStore, Store } from 'store';
915
import Channel from 'store/models/channel';
1016
import ChannelStore from 'store/stores/channelStore';
@@ -120,6 +126,72 @@ describe('ChannelStore', () => {
120126
expect(+store.totalOutbound).toBe(outbound);
121127
});
122128

129+
it('should fetch aliases for channels', async () => {
130+
await store.fetchChannels();
131+
const channel = store.channels.get(lndChannel.chanId) as Channel;
132+
expect(channel.alias).toBeUndefined();
133+
// the alias is fetched from the API and should be updated after a few ticks
134+
await waitFor(() => {
135+
expect(channel.alias).toBe(lndGetNodeInfo.node.alias);
136+
});
137+
});
138+
139+
it('should use cached aliases for channels', async () => {
140+
const cache = {
141+
expires: Date.now() + 60 * 1000,
142+
data: {
143+
[lndGetNodeInfo.node.pubKey]: lndGetNodeInfo.node.alias,
144+
},
145+
};
146+
jest
147+
.spyOn(window.sessionStorage.__proto__, 'getItem')
148+
.mockReturnValue(JSON.stringify(cache));
149+
150+
const channel = new Channel(rootStore, lndChannel);
151+
store.channels = observable.map({
152+
[channel.chanId]: channel,
153+
});
154+
155+
await store.fetchAliases();
156+
expect(channel.alias).toBe(lndGetNodeInfo.node.alias);
157+
expect(grpcMock.unary).not.toBeCalled();
158+
});
159+
160+
it('should fetch fee rates for channels', async () => {
161+
await store.fetchChannels();
162+
const channel = store.channels.get(lndChannel.chanId) as Channel;
163+
expect(channel.remoteFeeRate).toBe(0);
164+
// the alias is fetched from the API and should be updated after a few ticks
165+
await waitFor(() => {
166+
const rate = +Big(lndGetChanInfo.node1Policy.feeRateMilliMsat)
167+
.div(1000000)
168+
.mul(100);
169+
expect(channel.remoteFeeRate).toBe(rate);
170+
});
171+
});
172+
173+
it('should use cached fee rates for channels', async () => {
174+
const rate = +Big(lndGetChanInfo.node1Policy.feeRateMilliMsat).div(1000000).mul(100);
175+
const cache = {
176+
expires: Date.now() + 60 * 1000,
177+
data: {
178+
[lndGetChanInfo.channelId]: rate,
179+
},
180+
};
181+
jest
182+
.spyOn(window.sessionStorage.__proto__, 'getItem')
183+
.mockReturnValue(JSON.stringify(cache));
184+
185+
const channel = new Channel(rootStore, lndChannel);
186+
store.channels = observable.map({
187+
[channel.chanId]: channel,
188+
});
189+
190+
await store.fetchFeeRates();
191+
expect(channel.remoteFeeRate).toBe(rate);
192+
expect(grpcMock.unary).not.toBeCalled();
193+
});
194+
123195
describe('onChannelEvent', () => {
124196
const {
125197
OPEN_CHANNEL,

app/src/__tests__/store/settingsStore.spec.ts

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
1-
import AppStorage from 'util/appStorage';
21
import { BalanceMode, Unit } from 'util/constants';
32
import { createStore, SettingsStore } from 'store';
4-
import { PersistentSettings } from 'store/stores/settingsStore';
5-
6-
const appStorageMock = AppStorage as jest.Mock<AppStorage<PersistentSettings>>;
73

84
describe('SettingsStore', () => {
95
let store: SettingsStore;
@@ -13,12 +9,13 @@ describe('SettingsStore', () => {
139
});
1410

1511
it('should load settings', async () => {
16-
const getMock = appStorageMock.mock.instances[0].get as jest.Mock;
17-
getMock.mockImplementation(() => ({
18-
sidebarVisible: false,
19-
unit: Unit.bits,
20-
balanceMode: BalanceMode.routing,
21-
}));
12+
jest.spyOn(window.localStorage.__proto__, 'getItem').mockReturnValueOnce(
13+
JSON.stringify({
14+
sidebarVisible: false,
15+
unit: Unit.bits,
16+
balanceMode: BalanceMode.routing,
17+
}),
18+
);
2219

2320
store.load();
2421

@@ -28,9 +25,6 @@ describe('SettingsStore', () => {
2825
});
2926

3027
it('should do nothing if nothing is saved in storage', () => {
31-
const getMock = appStorageMock.mock.instances[0].get as jest.Mock;
32-
getMock.mockReturnValue(undefined as any);
33-
3428
store.load();
3529

3630
expect(store.sidebarVisible).toEqual(true);

app/src/__tests__/util/appStorage.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ describe('appStorage util', () => {
99
someString: 'abc',
1010
someBool: false,
1111
};
12-
const appStorage = new AppStorage<typeof settings>();
12+
const appStorage = new AppStorage();
1313

1414
it('should save an object to localStorage', () => {
1515
appStorage.set(key, settings);

app/src/api/lnd.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,26 @@ class LndApi extends BaseApi<LndEvents> {
5656
return res.toObject();
5757
}
5858

59+
/**
60+
* call the LND `GetNodeInfo` RPC and return the response
61+
*/
62+
async getNodeInfo(pubkey: string): Promise<LND.NodeInfo.AsObject> {
63+
const req = new LND.NodeInfoRequest();
64+
req.setPubKey(pubkey);
65+
const res = await this._grpc.request(Lightning.GetNodeInfo, req, this._meta);
66+
return res.toObject();
67+
}
68+
69+
/**
70+
* call the LND `GetChanInfo` RPC and return the response
71+
*/
72+
async getChannelInfo(id: string): Promise<LND.ChannelEdge.AsObject> {
73+
const req = new LND.ChanInfoRequest();
74+
req.setChanId(id);
75+
const res = await this._grpc.request(Lightning.GetChanInfo, req, this._meta);
76+
return res.toObject();
77+
}
78+
5979
/**
6080
* Connect to the LND streaming endpoints
6181
*/

app/src/components/loop/ChannelBalance.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const Styled = {
99
Wrapper: styled.div<{ pct: number; status: BalanceStatus; active: boolean }>`
1010
display: flex;
1111
width: 100%;
12+
padding: 0 5%;
1213
1314
> div {
1415
min-width: 6px;

app/src/components/loop/ChannelRow.tsx

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ const Styled = {
3939
Column: styled(Column)<{ last?: boolean }>`
4040
white-space: nowrap;
4141
line-height: ${ROW_HEIGHT}px;
42+
overflow: hidden;
43+
text-overflow: ellipsis;
44+
padding-left: 5px;
45+
padding-right: ${props => (props.last ? '15' : '5')}px;
4246
`,
4347
StatusIcon: styled.span`
4448
float: left;
@@ -58,6 +62,7 @@ const Styled = {
5862

5963
export const ChannelRowHeader: React.FC = () => {
6064
const { l } = usePrefixedTranslation('cmps.loop.ChannelRowHeader');
65+
const { Column } = Styled;
6166
return (
6267
<Row>
6368
<Column right>
@@ -67,13 +72,16 @@ export const ChannelRowHeader: React.FC = () => {
6772
<Column>
6873
<HeaderFour>{l('canSend')}</HeaderFour>
6974
</Column>
70-
<Column>
75+
<Column cols={1}>
76+
<HeaderFour>{l('feeRate')}</HeaderFour>
77+
</Column>
78+
<Column cols={1}>
7179
<HeaderFour>{l('upTime')}</HeaderFour>
7280
</Column>
73-
<Column>
81+
<Column cols={2}>
7482
<HeaderFour>{l('peer')}</HeaderFour>
7583
</Column>
76-
<Column right>
84+
<Column right last>
7785
<HeaderFour>{l('capacity')}</HeaderFour>
7886
</Column>
7987
</Row>
@@ -134,10 +142,11 @@ const ChannelRow: React.FC<Props> = ({ channel, style }) => {
134142
<Column>
135143
<Unit sats={channel.localBalance} suffix={false} />
136144
</Column>
137-
<Column>{channel.uptimePercent}</Column>
138-
<Column>
145+
<Column cols={1}>{channel.remoteFeeRate}</Column>
146+
<Column cols={1}>{channel.uptimePercent}</Column>
147+
<Column cols={2}>
139148
<Tip overlay={channel.remotePubkey} placement="left">
140-
<span>{channel.ellipsedPubkey}</span>
149+
<span>{channel.aliasLabel}</span>
141150
</Tip>
142151
</Column>
143152
<Column right>

0 commit comments

Comments
 (0)