mobx#values TypeScript Examples
The following examples show how to use
mobx#values.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: ChannelList.stories.tsx From lightning-terminal with MIT License | 6 votes |
channelSubset = (channels: ObservableMap<string, Channel>) => {
const few = values(channels)
.slice(0, 20)
.reduce((result, c) => {
result[c.chanId] = c;
return result;
}, {} as Record<string, Channel>);
return observable.map(few);
}
Example #2
Source File: KubeResourceChart.tsx From lens-resource-map-extension with MIT License | 6 votes |
async componentDidMount() {
this.setState(this.state);
this.registerStores();
await this.loadData();
this.displayChart();
const fg = this.chartRef.current;
//fg?.zoom(1.2, 1000);
fg?.d3Force('link').strength(2).distance(() => 60)
fg?.d3Force('charge', d33d.forceManyBody().strength(-60).distanceMax(250));
fg?.d3Force('collide', d3.forceCollide(40));
fg?.d3Force("center", d3.forceCenter());
const reactionOpts = {
equals: comparer.structural,
}
const { object } = this.props
const api = Renderer.K8sApi.apiManager.getApiByKind(object.kind, object.apiVersion);
const store = Renderer.K8sApi.apiManager.getStore(api);
this.disposers.push(reaction(() => this.props.object, (value, prev, _reaction) => { value.getId() !== prev.getId() ? this.displayChart() : this.refreshChart() }));
this.disposers.push(reaction(() => this.podsStore.items.toJSON(), (values, previousValue, _reaction) => { this.refreshItems(values, previousValue) }, reactionOpts));
this.disposers.push(reaction(() => store.items.toJSON(), (values, previousValue, _reaction) => { this.refreshItems(values, previousValue) }, reactionOpts));
}
Example #3
Source File: swapStore.ts From lightning-terminal with MIT License | 6 votes |
/** removes completed swap IDs from the swappedChannels list */
pruneSwappedChannels() {
this._store.log.info('pruning swapped channels list');
// create a list of the currently pending swaps
const processingIds = values(this.swaps)
.filter(s => s.isPending)
.map(s => s.id);
// loop over the swapped channels that are stored
entries(this.swappedChannels).forEach(([chanId, swapIds]) => {
// filter out the swaps that are no longer processing
const pendingSwapIds = swapIds.filter(id => processingIds.includes(id));
if (swapIds.length !== pendingSwapIds.length) {
// if the list has changed then the swapped channels value needs to be updated
if (pendingSwapIds.length === 0) {
// remove the channel id key if there are no more processing swaps using it
this.swappedChannels.delete(chanId);
} else {
// update the swapIds with the updated list
this.swappedChannels.set(chanId, pendingSwapIds);
}
}
});
this._store.log.info('updated swapStore.swappedChannels', toJS(this.swappedChannels));
}
Example #4
Source File: channelStore.ts From lightning-terminal with MIT License | 6 votes |
/**
* queries the LND api to fetch the fees for all of the peers we have
* channels opened with
*/
async fetchFeeRates() {
const feeRates = await this._store.storage.getCached<number>({
cacheKey: 'feeRates',
requiredKeys: values(this.channels).map(c => c.chanId),
log: this._store.log,
fetchFromApi: async (missingKeys, data) => {
// call getNodeInfo for each pubkey and wait for all the requests to complete
const chanInfos = await Promise.all(
missingKeys.map(id => this._store.api.lnd.getChannelInfo(id)),
);
// return an updated mapping from chanId to fee rate
return chanInfos.reduce((acc, info) => {
const { channelId, node1Pub, node1Policy, node2Policy } = info;
const localPubkey = this._store.nodeStore.pubkey;
const policy = node1Pub === localPubkey ? node2Policy : node1Policy;
if (policy) {
acc[channelId] = +Big(policy.feeRateMilliMsat);
}
return acc;
}, data);
},
});
runInAction(() => {
// set the fee on each channel in the store
values(this.channels).forEach(c => {
const rate = feeRates[c.chanId];
if (rate && c.remoteFeeRate !== rate) {
c.remoteFeeRate = rate;
}
});
this._store.log.info('updated channels with feeRates', feeRates);
});
}
Example #5
Source File: channelStore.ts From lightning-terminal with MIT License | 6 votes |
/**
* queries the LND api to fetch the aliases for all of the peers we have
* channels opened with
*/
async fetchAliases() {
const aliases = await this._store.storage.getCached<string>({
cacheKey: 'aliases',
requiredKeys: values(this.channels).map(c => c.remotePubkey),
log: this._store.log,
fetchFromApi: async (missingKeys, data) => {
// call getNodeInfo for each pubkey and wait for all the requests to complete
const nodeInfos = await Promise.all(
missingKeys.map(id => this._store.api.lnd.getNodeInfo(id)),
);
// return a mapping from pubkey to alias
return nodeInfos.reduce((acc, { node }) => {
if (node) acc[node.pubKey] = node.alias;
return acc;
}, data);
},
});
runInAction(() => {
// set the alias on each channel in the store
values(this.channels).forEach(c => {
const alias = aliases[c.remotePubkey];
if (alias && alias !== c.alias) {
c.alias = alias;
}
});
this._store.log.info('updated channels with aliases', aliases);
});
}
Example #6
Source File: AlertContainer.tsx From lightning-terminal with MIT License | 6 votes |
AlertContainer: React.FC = () => {
const { appView } = useStore();
const { Container, CloseIcon } = Styled;
const closeButton = (
<CloseIcon>
<Close />
</CloseIcon>
);
return (
<>
{values(appView.alerts).map(n => (
<AlertToast key={n.id} alert={n} onClose={appView.clearAlert} />
))}
<Container position="top-right" autoClose={5 * 1000} closeButton={closeButton} />
</>
);
}
Example #7
Source File: nodeStore.spec.ts From lightning-terminal with MIT License | 5 votes |
describe('NodeStore', () => {
let rootStore: Store;
let store: NodeStore;
beforeEach(() => {
rootStore = createStore();
store = rootStore.nodeStore;
});
it('should fetch node info', async () => {
expect(store.pubkey).toBe('');
expect(store.alias).toBe('');
expect(store.chain).toBe('bitcoin');
expect(store.network).toBe('mainnet');
await store.fetchInfo();
expect(store.pubkey).toEqual(lndGetInfo.identityPubkey);
expect(store.alias).toEqual(lndGetInfo.alias);
expect(store.chain).toEqual(lndGetInfo.chainsList[0].chain);
expect(store.network).toEqual(lndGetInfo.chainsList[0].network);
});
it('should handle errors fetching balances', async () => {
grpcMock.unary.mockImplementationOnce(desc => {
if (desc.methodName === 'GetInfo') throw new Error('test-err');
return undefined as any;
});
expect(rootStore.appView.alerts.size).toBe(0);
await store.fetchInfo();
await waitFor(() => {
expect(rootStore.appView.alerts.size).toBe(1);
expect(values(rootStore.appView.alerts)[0].message).toBe('test-err');
});
});
it('should fetch node balances', async () => {
expect(+store.wallet.channelBalance).toBe(0);
expect(+store.wallet.walletBalance).toBe(0);
await store.fetchBalances();
expect(store.wallet.channelBalance.toString()).toEqual(lndChannelBalance.balance);
expect(store.wallet.walletBalance.toString()).toEqual(lndWalletBalance.totalBalance);
});
it('should handle errors fetching balances', async () => {
grpcMock.unary.mockImplementationOnce(desc => {
if (desc.methodName === 'ChannelBalance') throw new Error('test-err');
return undefined as any;
});
expect(rootStore.appView.alerts.size).toBe(0);
await store.fetchBalances();
await waitFor(() => {
expect(rootStore.appView.alerts.size).toBe(1);
expect(values(rootStore.appView.alerts)[0].message).toBe('test-err');
});
});
it('should handle a transaction event', () => {
expect(+store.wallet.walletBalance).toBe(0);
store.onTransaction(lndTransaction);
expect(store.wallet.walletBalance.toString()).toBe(lndTransaction.amount);
});
it('should handle duplicate transaction events', () => {
expect(+store.wallet.walletBalance).toBe(0);
store.onTransaction(lndTransaction);
expect(store.wallet.walletBalance.toString()).toBe(lndTransaction.amount);
store.onTransaction(lndTransaction);
expect(store.wallet.walletBalance.toString()).toBe(lndTransaction.amount);
});
});
Example #8
Source File: appView.spec.ts From lightning-terminal with MIT License | 5 votes |
describe('AppView', () => {
let rootStore: Store;
let store: AppView;
beforeEach(() => {
rootStore = createStore();
store = rootStore.appView;
});
it('should add an alert', async () => {
expect(store.alerts.size).toBe(0);
store.notify('test message', 'test title');
expect(store.alerts.size).toBe(1);
const alert = values(store.alerts)[0];
expect(alert.message).toBe('test message');
expect(alert.title).toBe('test title');
expect(alert.type).toBe('error');
});
it('should clear an alert', () => {
expect(store.alerts.size).toBe(0);
store.notify('test message', 'test title');
expect(store.alerts.size).toBe(1);
const alert = values(store.alerts)[0];
store.clearAlert(alert.id);
expect(store.alerts.size).toBe(0);
});
it('should handle errors', () => {
store.handleError(new Error('message'), 'title');
expect(store.alerts.size).toBe(1);
});
it('should handle authentication errors', () => {
rootStore.authStore.authenticated = true;
expect(store.alerts.size).toBe(0);
store.handleError(new AuthenticationError());
expect(rootStore.authStore.authenticated).toBe(false);
expect(store.alerts.size).toBe(1);
});
});
Example #9
Source File: channelStore.ts From lightning-terminal with MIT License | 5 votes |
/** update the channel list based on events from the API */
onChannelEvent(event: ChannelEventUpdate.AsObject) {
this._store.log.info('handle incoming channel event', event);
if (event.type === INACTIVE_CHANNEL && event.inactiveChannel) {
// set the channel in state to inactive
const point = this._channelPointToString(event.inactiveChannel);
values(this.channels)
.filter(c => c.channelPoint === point)
.forEach(c => {
c.active = false;
this._store.log.info('updated channel', toJS(c));
});
} else if (event.type === ACTIVE_CHANNEL && event.activeChannel) {
// set the channel in state to active
const point = this._channelPointToString(event.activeChannel);
values(this.channels)
.filter(c => c.channelPoint === point)
.forEach(c => {
c.active = true;
this._store.log.info('updated channel', toJS(c));
});
} else if (event.type === CLOSED_CHANNEL && event.closedChannel) {
// delete the closed channel
const channel = this.channels.get(event.closedChannel.chanId);
this.channels.delete(event.closedChannel.chanId);
this._store.log.info('removed closed channel', toJS(channel));
this._store.nodeStore.fetchBalancesThrottled();
} else if (event.type === OPEN_CHANNEL && event.openChannel) {
// add the new opened channel
const channel = Channel.create(this._store, event.openChannel);
this.channels.set(channel.chanId, channel);
this._store.log.info('added new open channel', toJS(channel));
this._store.nodeStore.fetchBalancesThrottled();
// update the pending channels list to remove any pending
this.fetchPendingChannelsThrottled();
// fetch the alias for the added channel
this.fetchAliases();
// fetch the remote fee rates for the added channel
this.fetchFeeRates();
} else if (event.type === PENDING_OPEN_CHANNEL && event.pendingOpenChannel) {
// add or update the pending channels by fetching the full list from the
// API, since the event doesn't contain the channel data
this.fetchPendingChannelsThrottled();
// fetch orders, leases, & latest batch whenever a channel is opened
this._store.orderStore.fetchOrdersThrottled();
this._store.orderStore.fetchLeasesThrottled();
this._store.batchStore.fetchLatestBatchThrottled();
}
}
Example #10
Source File: orderStore.ts From lightning-terminal with MIT License | 5 votes |
/** exports the list of leases to CSV file */
exportLeases() {
this._store.log.info('exporting Leases to a CSV file');
const leases = values(this.leases).slice();
this._store.csv.export('leases', Lease.csvColumns, toJS(leases));
}
Example #11
Source File: sessionStore.ts From lightning-terminal with MIT License | 5 votes |
/**
* queries the LIT api to fetch the list of sessions and stores them
* in the state
*/
async fetchSessions() {
this._store.log.info('fetching sessions');
try {
const { sessionsList } = await this._store.api.lit.listSessions();
runInAction(() => {
const serverIds: string[] = [];
sessionsList.forEach(rpcSession => {
const litSession = rpcSession as LIT.Session.AsObject;
// update existing orders or create new ones in state. using this
// approach instead of overwriting the array will cause fewer state
// mutations, resulting in better react rendering performance
const pubKey = hex(litSession.localPublicKey);
const session = this.sessions.get(pubKey) || new Session(this._store);
session.update(litSession);
this.sessions.set(pubKey, session);
serverIds.push(pubKey);
});
// remove any sessions in state that are not in the API response
const localIds = Object.keys(this.sessions);
localIds
.filter(id => !serverIds.includes(id))
.forEach(id => this.sessions.delete(id));
this._store.log.info('updated sessionStore.sessions', toJS(this.sessions));
});
// Ensures that there is at least one session created
if (this.sortedSessions.length === 0) {
const count = values(this.sessions).filter(s =>
s.label.startsWith('Default Session'),
).length;
const countText = count === 0 ? '' : `(${count})`;
await this.addSession(
`Default Session ${countText}`,
LIT.SessionType.TYPE_MACAROON_ADMIN,
MAX_DATE,
);
}
} catch (error: any) {
this._store.appView.handleError(error, 'Unable to fetch sessions');
}
}
Example #12
Source File: MapChart.tsx From eth2stats-dashboard with MIT License | 5 votes |
MapChart: React.FC<IMapChartProps> = observer((props) => {
const {store} = props; // useStores();
const tabsVisible = store.getConfig().length > 1;
const scrollHeight = getScrollHeight(tabsVisible);
if (store.clientStore.clientsLoading) {
return (<MapLoading/>);
}
const markers = store.clientStore.locations;
if (markers.size === 0) {
return (<MapNoData/>);
}
return (
<Scrollbars autoHide autoHeight autoHeightMin={0}
autoHeightMax={scrollHeight}>
<ComposableMap width={width} height={height} projection={projection} className="bg-darkblue-200">
<ZoomableGroup>
<Geographies geography={geoUrl}>
{({geographies}) =>
geographies.map(geo => (
<Geography
key={geo.rsmKey}
geography={geo}
clipPath="url(#rsm-sphere)"
fill="#172232"
/>
))
}
</Geographies>
{values(markers).map((marker) => (
<MapMarker marker={marker} key={marker.id}/>
))}
</ZoomableGroup>
</ComposableMap>
<MapTooltip markers={markers} store={store}/>
</Scrollbars>
);
})
Example #13
Source File: buildSwapView.spec.ts From lightning-terminal with MIT License | 4 votes |
describe('BuildSwapView', () => {
let rootStore: Store;
let store: BuildSwapView;
beforeEach(async () => {
rootStore = createStore();
await rootStore.fetchAllData();
store = rootStore.buildSwapView;
});
it('should not start a swap if there are no channels', async () => {
rootStore.channelStore.channels.clear();
expect(store.currentStep).toBe(BuildSwapSteps.Closed);
expect(rootStore.appView.alerts.size).toBe(0);
await store.startSwap();
expect(store.currentStep).toBe(BuildSwapSteps.Closed);
expect(rootStore.appView.alerts.size).toBe(1);
});
it('should toggle the selected channels', () => {
expect(store.selectedChanIds).toHaveLength(0);
store.toggleSelectedChannel('test');
expect(store.selectedChanIds).toHaveLength(1);
store.toggleSelectedChannel('test');
expect(store.selectedChanIds).toHaveLength(0);
});
it('should infer the swap direction based on the selected channels (receiving mode)', () => {
rootStore.settingsStore.setBalanceMode(BalanceMode.receive);
const channels = rootStore.channelStore.sortedChannels;
store.toggleSelectedChannel(channels[0].chanId);
expect(store.inferredDirection).toEqual(SwapDirection.OUT);
store.toggleSelectedChannel(channels[channels.length - 1].chanId);
expect(store.inferredDirection).toEqual(SwapDirection.OUT);
});
it('should infer the swap direction based on the selected channels (sending mode)', () => {
rootStore.settingsStore.setBalanceMode(BalanceMode.send);
const channels = rootStore.channelStore.sortedChannels;
store.toggleSelectedChannel(channels[0].chanId);
expect(store.inferredDirection).toEqual(SwapDirection.IN);
store.toggleSelectedChannel(channels[channels.length - 1].chanId);
expect(store.inferredDirection).toEqual(SwapDirection.IN);
});
it('should infer the swap direction based on the selected channels (routing mode)', () => {
rootStore.settingsStore.setBalanceMode(BalanceMode.routing);
const channels = rootStore.channelStore.sortedChannels;
let c = channels[0];
c.localBalance = c.capacity.mul(0.2);
c.remoteBalance = c.capacity.sub(c.localBalance);
store.toggleSelectedChannel(c.chanId);
expect(store.inferredDirection).toEqual(SwapDirection.IN);
c = channels[channels.length - 1];
c.localBalance = c.capacity.mul(0.85);
c.remoteBalance = c.capacity.sub(c.localBalance);
store.toggleSelectedChannel(channels[channels.length - 1].chanId);
expect(store.inferredDirection).toEqual(SwapDirection.OUT);
});
it('should not infer the swap direction with no selected channels (routing mode)', () => {
rootStore.settingsStore.setBalanceMode(BalanceMode.routing);
expect(store.inferredDirection).toBeUndefined();
});
it('should fetch loop terms', async () => {
expect(store.terms.in).toEqual({ min: Big(0), max: Big(0) });
expect(store.terms.out).toEqual({ min: Big(0), max: Big(0) });
await store.getTerms();
expect(store.terms.in).toEqual({ min: Big(250000), max: Big(1000000) });
expect(store.terms.out).toEqual({
min: Big(250000),
max: Big(1000000),
minCltv: 20,
maxCltv: 60,
});
});
it('should handle errors fetching loop terms', async () => {
grpcMock.unary.mockImplementationOnce(desc => {
if (desc.methodName === 'GetLoopInTerms') throw new Error('test-err');
return undefined as any;
});
expect(rootStore.appView.alerts.size).toBe(0);
await store.getTerms();
await waitFor(() => {
expect(rootStore.appView.alerts.size).toBe(1);
expect(values(rootStore.appView.alerts)[0].message).toBe('test-err');
});
});
it('should return the amount in between min/max by default', async () => {
await store.getTerms();
expect(+store.amountForSelected).toBe(625000);
});
it('should ensure amount is greater than the min terms', async () => {
store.setAmount(Big(loopInTerms.minSwapAmount).sub(100));
await store.getTerms();
expect(store.amountForSelected.toString()).toBe(loopInTerms.minSwapAmount);
});
it('should ensure amount is less than the max terms', async () => {
store.setAmount(Big(loopInTerms.maxSwapAmount + 100));
await store.getTerms();
expect(store.amountForSelected.toString()).toBe(loopInTerms.maxSwapAmount);
});
it('should validate the conf target', async () => {
const { minCltvDelta, maxCltvDelta } = loopOutTerms;
expect(store.confTarget).toBeUndefined();
let target = maxCltvDelta - 10;
store.setConfTarget(target);
expect(store.confTarget).toBe(target);
store.setDirection(SwapDirection.OUT);
await store.getTerms();
store.setConfTarget(target);
expect(store.confTarget).toBe(target);
target = minCltvDelta - 10;
expect(() => store.setConfTarget(target)).toThrow();
target = maxCltvDelta + 10;
expect(() => store.setConfTarget(target)).toThrow();
});
it('should submit the Loop Out conf target', async () => {
const target = 23;
store.setDirection(SwapDirection.OUT);
store.setAmount(Big(500000));
expect(store.confTarget).toBeUndefined();
store.setConfTarget(target);
expect(store.confTarget).toBe(target);
let reqTarget = '';
// mock the grpc unary function in order to capture the supplied dest
// passed in with the API request
injectIntoGrpcUnary((desc, props) => {
reqTarget = (props.request.toObject() as any).sweepConfTarget;
});
store.requestSwap();
await waitFor(() => expect(reqTarget).toBe(target));
});
it('should submit the Loop Out address', async () => {
const addr = 'xyzabc';
store.setDirection(SwapDirection.OUT);
store.setAmount(Big(500000));
expect(store.loopOutAddress).toBeUndefined();
store.setLoopOutAddress(addr);
expect(store.loopOutAddress).toBe(addr);
// store.goToNextStep();
let reqAddr = '';
// mock the grpc unary function in order to capture the supplied dest
// passed in with the API request
injectIntoGrpcUnary((desc, props) => {
reqAddr = (props.request.toObject() as any).dest;
});
store.requestSwap();
await waitFor(() => expect(reqAddr).toBe(addr));
});
it('should select all channels with the same peer for loop in', () => {
const channels = rootStore.channelStore.sortedChannels;
channels[1].remotePubkey = channels[0].remotePubkey;
channels[2].remotePubkey = channels[0].remotePubkey;
expect(store.selectedChanIds).toHaveLength(0);
store.toggleSelectedChannel(channels[0].chanId);
store.setDirection(SwapDirection.IN);
expect(store.selectedChanIds).toHaveLength(3);
});
it('should fetch a loop in quote', async () => {
expect(+store.quote.swapFee).toEqual(0);
expect(+store.quote.minerFee).toEqual(0);
expect(+store.quote.prepayAmount).toEqual(0);
store.setDirection(SwapDirection.IN);
store.setAmount(Big(600));
await store.getQuote();
expect(+store.quote.swapFee).toEqual(83);
expect(+store.quote.minerFee).toEqual(7387);
expect(+store.quote.prepayAmount).toEqual(0);
});
it('should fetch a loop out quote', async () => {
expect(+store.quote.swapFee).toEqual(0);
expect(+store.quote.minerFee).toEqual(0);
expect(+store.quote.prepayAmount).toEqual(0);
store.setDirection(SwapDirection.OUT);
store.setAmount(Big(600));
await store.getQuote();
expect(+store.quote.swapFee).toEqual(83);
expect(+store.quote.minerFee).toEqual(7387);
expect(+store.quote.prepayAmount).toEqual(1337);
});
it('should handle errors fetching loop quote', async () => {
grpcMock.unary.mockImplementationOnce(desc => {
if (desc.methodName === 'LoopOutQuote') throw new Error('test-err');
return undefined as any;
});
store.setDirection(SwapDirection.OUT);
store.setAmount(Big(600));
expect(rootStore.appView.alerts.size).toBe(0);
await store.getQuote();
await waitFor(() => {
expect(rootStore.appView.alerts.size).toBe(1);
expect(values(rootStore.appView.alerts)[0].message).toBe('test-err');
});
});
it('should perform a loop in', async () => {
const channels = rootStore.channelStore.sortedChannels;
// the pubkey in the sampleData is not valid, so hard-code this valid one
channels[0].remotePubkey =
'035c82e14eb74d2324daa17eebea8c58b46a9eabac87191cc83ee26275b514e6a0';
store.toggleSelectedChannel(channels[0].chanId);
store.setDirection(SwapDirection.IN);
store.setAmount(Big(600));
store.requestSwap();
await waitFor(() => {
expect(grpcMock.unary).toHaveBeenCalledWith(
expect.objectContaining({ methodName: 'LoopIn' }),
expect.any(Object),
);
});
});
it('should perform a loop out', async () => {
const channels = rootStore.channelStore.sortedChannels;
store.toggleSelectedChannel(channels[0].chanId);
store.setDirection(SwapDirection.OUT);
store.setAmount(Big(600));
store.requestSwap();
await waitFor(() => {
expect(grpcMock.unary).toHaveBeenCalledWith(
expect.objectContaining({ methodName: 'LoopOut' }),
expect.anything(),
);
});
});
it('should store swapped channels after a loop in', async () => {
const channels = rootStore.channelStore.sortedChannels;
// the pubkey in the sampleData is not valid, so hard-code this valid one
channels[0].remotePubkey =
'035c82e14eb74d2324daa17eebea8c58b46a9eabac87191cc83ee26275b514e6a0';
store.toggleSelectedChannel(channels[0].chanId);
store.setDirection(SwapDirection.IN);
store.setAmount(Big(600));
expect(rootStore.swapStore.swappedChannels.size).toBe(0);
store.requestSwap();
await waitFor(() => expect(store.currentStep).toBe(BuildSwapSteps.Closed));
expect(rootStore.swapStore.swappedChannels.size).toBe(1);
});
it('should store swapped channels after a loop out', async () => {
const channels = rootStore.channelStore.sortedChannels;
store.toggleSelectedChannel(channels[0].chanId);
store.setDirection(SwapDirection.OUT);
store.setAmount(Big(600));
expect(rootStore.swapStore.swappedChannels.size).toBe(0);
store.requestSwap();
await waitFor(() => expect(store.currentStep).toBe(BuildSwapSteps.Closed));
expect(rootStore.swapStore.swappedChannels.size).toBe(1);
});
it('should set the correct swap deadline in production', async () => {
store.setDirection(SwapDirection.OUT);
store.setAmount(Big(600));
let deadline = '';
// mock the grpc unary function in order to capture the supplied deadline
// passed in with the API request
injectIntoGrpcUnary((desc, props) => {
deadline = (props.request.toObject() as any).swapPublicationDeadline;
});
// run a loop on mainnet and verify the deadline
rootStore.nodeStore.network = 'mainnet';
store.requestSwap();
await waitFor(() => expect(+deadline).toBeGreaterThan(0));
// inject again for the next swap
injectIntoGrpcUnary((desc, props) => {
deadline = (props.request.toObject() as any).swapPublicationDeadline;
});
// run a loop on regtest and verify the deadline
rootStore.nodeStore.network = 'regtest';
store.requestSwap();
await waitFor(() => expect(+deadline).toEqual(0));
});
it('should handle errors when performing a loop', async () => {
grpcMock.unary.mockImplementationOnce(desc => {
if (desc.methodName === 'LoopIn') throw new Error('test-err');
return undefined as any;
});
store.setDirection(SwapDirection.IN);
store.setAmount(Big(600));
expect(rootStore.appView.alerts.size).toBe(0);
store.requestSwap();
await waitFor(() => {
expect(rootStore.appView.alerts.size).toBe(1);
expect(values(rootStore.appView.alerts)[0].message).toBe('test-err');
});
});
it('should delay for 3 seconds before performing a swap in production', async () => {
store.setDirection(SwapDirection.OUT);
store.setAmount(Big(600));
let executed = false;
// mock the grpc unary function in order to know when the API request is executed
injectIntoGrpcUnary(() => (executed = true));
// use mock timers so the test doesn't actually need to run for 3 seconds
jest.useFakeTimers();
// run a loop in production and verify the delay
Object.defineProperty(process, 'env', { get: () => ({ NODE_ENV: 'production' }) });
store.requestSwap();
jest.advanceTimersByTime(SWAP_ABORT_DELAY - 1);
// the loop still should not have executed here
expect(executed).toBe(false);
// this should trigger the timeout at 3000
jest.advanceTimersByTime(1);
expect(executed).toBe(true);
// reset the env and mock timers
Object.defineProperty(process, 'env', { get: () => ({ NODE_ENV: 'test' }) });
jest.useRealTimers();
});
it('should do nothing when abortSwap is called without requestSwap', async () => {
const spy = jest.spyOn(window, 'clearTimeout');
expect(store.processingTimeout).toBeUndefined();
// run a loop in production and verify the delay
store.abortSwap();
expect(store.processingTimeout).toBeUndefined();
expect(spy).not.toBeCalled();
spy.mockClear();
});
describe('min/max swap limits', () => {
const addChannel = (capacity: number, localBalance: number) => {
const remoteBalance = capacity - localBalance;
const lndChan = {
...lndChannel,
capacity: `${capacity}`,
localBalance: `${localBalance}`,
remoteBalance: `${remoteBalance}`,
};
const channel = Channel.create(rootStore, lndChan);
channel.chanId = `${channel.chanId}${rootStore.channelStore.channels.size}`;
channel.remotePubkey = `${channel.remotePubkey}${rootStore.channelStore.channels.size}`;
rootStore.channelStore.channels.set(channel.chanId, channel);
};
const round = (amount: number) => {
return Math.floor(amount / store.AMOUNT_INCREMENT) * store.AMOUNT_INCREMENT;
};
beforeEach(() => {
rootStore.channelStore.channels.clear();
[
{ capacity: 200000, local: 100000 },
{ capacity: 100000, local: 50000 },
{ capacity: 100000, local: 20000 },
].forEach(({ capacity, local }) => addChannel(capacity, local));
});
it('should limit Loop In max based on all remote balances', async () => {
await store.getTerms();
store.setDirection(SwapDirection.IN);
// should be the sum of all remote balances minus the reserve
expect(+store.termsForDirection.max).toBe(round(230000 * 0.99));
});
it('should limit Loop In max based on selected remote balances', async () => {
store.toggleSelectedChannel(store.channels[0].chanId);
store.toggleSelectedChannel(store.channels[1].chanId);
await store.getTerms();
store.setDirection(SwapDirection.IN);
// should be the sum of the first two remote balances minus the reserve
expect(+store.termsForDirection.max).toBe(round(150000 * 0.99));
});
it('should limit Loop Out max based on all local balances', async () => {
await store.getTerms();
store.setDirection(SwapDirection.OUT);
// should be the sum of all local balances minus the reserve
expect(+store.termsForDirection.max).toBe(round(170000 * 0.99));
});
it('should limit Loop Out max based on selected local balances', async () => {
store.toggleSelectedChannel(store.channels[0].chanId);
store.toggleSelectedChannel(store.channels[1].chanId);
await store.getTerms();
store.setDirection(SwapDirection.OUT);
// should be the sum of the first two local balances minus the reserve
expect(+store.termsForDirection.max).toBe(round(150000 * 0.99));
});
});
});
Example #14
Source File: channelStore.spec.ts From lightning-terminal with MIT License | 4 votes |
describe('ChannelStore', () => {
let rootStore: Store;
let store: ChannelStore;
const channelSubset = (channels: ObservableMap<string, Channel>) => {
const few = values(channels)
.slice(0, 20)
.reduce((result, c) => {
result[c.chanId] = c;
return result;
}, {} as Record<string, Channel>);
return observable.map(few);
};
beforeEach(() => {
rootStore = createStore();
store = rootStore.channelStore;
});
it('should fetch list of channels', async () => {
expect(store.channels.size).toEqual(0);
await store.fetchChannels();
expect(store.channels.size).toEqual(lndListChannels.channelsList.length);
});
it('should handle errors fetching channels', async () => {
grpcMock.unary.mockImplementationOnce(desc => {
if (desc.methodName === 'ListChannels') throw new Error('test-err');
return undefined as any;
});
expect(rootStore.appView.alerts.size).toBe(0);
await store.fetchChannels();
await waitFor(() => {
expect(rootStore.appView.alerts.size).toBe(1);
expect(values(rootStore.appView.alerts)[0].message).toBe('test-err');
});
});
it('should fetch list of pending channels', async () => {
expect(store.pendingChannels.size).toEqual(0);
await store.fetchPendingChannels();
expect(store.pendingChannels.size).toEqual(4);
runInAction(() => {
store.sortedPendingChannels[0].channelPoint = 'asdf';
});
await store.fetchPendingChannels();
expect(store.sortedPendingChannels[0].channelPoint).not.toBe('asdf');
expect(store.pendingChannels.size).toEqual(4);
});
it('should handle errors fetching channels', async () => {
grpcMock.unary.mockImplementationOnce(desc => {
if (desc.methodName === 'PendingChannels') throw new Error('test-err');
return undefined as any;
});
expect(rootStore.appView.alerts.size).toBe(0);
await store.fetchPendingChannels();
await waitFor(() => {
expect(rootStore.appView.alerts.size).toBe(1);
expect(values(rootStore.appView.alerts)[0].message).toBe('test-err');
});
});
it('should update existing channels with the same id', async () => {
expect(store.channels.size).toEqual(0);
await store.fetchChannels();
expect(store.channels.size).toEqual(lndListChannels.channelsList.length);
const prevChan = store.sortedChannels[0];
const prevUptime = prevChan.uptime;
prevChan.uptime = Big(123);
await store.fetchChannels();
const updatedChan = store.sortedChannels[0];
// the existing channel should be updated
expect(prevChan).toBe(updatedChan);
expect(updatedChan.uptime).toEqual(prevUptime);
});
it('should sort channels correctly when using receive mode', async () => {
await store.fetchChannels();
rootStore.settingsStore.setBalanceMode(BalanceMode.receive);
store.channels = channelSubset(store.channels);
store.sortedChannels.forEach((c, i) => {
if (i === 0) return;
expect(c.localPercent).toBeLessThanOrEqual(
store.sortedChannels[i - 1].localPercent,
);
});
});
it('should sort channels correctly when using send mode', async () => {
await store.fetchChannels();
rootStore.settingsStore.setBalanceMode(BalanceMode.send);
store.channels = channelSubset(store.channels);
store.sortedChannels.forEach((c, i) => {
if (i === 0) return;
expect(c.localPercent).toBeGreaterThanOrEqual(
store.sortedChannels[i - 1].localPercent,
);
});
});
it('should sort channels correctly when using routing mode', async () => {
await store.fetchChannels();
rootStore.settingsStore.setBalanceMode(BalanceMode.routing);
store.channels = channelSubset(store.channels);
store.sortedChannels.forEach((c, i) => {
if (i === 0) return;
const currPct = Math.max(c.localPercent, 99 - c.localPercent);
const prev = store.sortedChannels[i - 1];
const prevPct = Math.max(prev.localPercent, 99 - prev.localPercent);
expect(currPct).toBeLessThanOrEqual(prevPct);
});
});
it('should compute inbound liquidity', async () => {
await store.fetchChannels();
const inbound = lndListChannels.channelsList.reduce(
(sum, chan) => sum.plus(chan.remoteBalance),
Big(0),
);
expect(+store.totalInbound).toBe(+inbound);
});
it('should compute outbound liquidity', async () => {
await store.fetchChannels();
const outbound = lndListChannels.channelsList.reduce(
(sum, chan) => sum.plus(chan.localBalance),
Big(0),
);
expect(+store.totalOutbound).toBe(+outbound);
});
it('should fetch aliases for channels', async () => {
await store.fetchChannels();
const channel = store.channels.get(lndChannel.chanId) as Channel;
expect(channel.alias).toBeUndefined();
// the alias is fetched from the API and should be updated after a few ticks
await waitFor(() => {
expect(channel.alias).toBe(lndGetNodeInfo.node.alias);
expect(channel.aliasLabel).toBe(lndGetNodeInfo.node.alias);
});
});
it('should use cached aliases for channels', async () => {
const cache = {
expires: Date.now() + 60 * 1000,
data: {
[lndGetNodeInfo.node.pubKey]: lndGetNodeInfo.node.alias,
},
};
jest
.spyOn(window.sessionStorage.__proto__, 'getItem')
.mockReturnValue(JSON.stringify(cache));
const channel = Channel.create(rootStore, lndChannel);
store.channels = observable.map({
[channel.chanId]: channel,
});
await store.fetchAliases();
expect(channel.alias).toBe(lndGetNodeInfo.node.alias);
expect(grpcMock.unary).not.toBeCalled();
});
it('should fetch fee rates for channels', async () => {
await store.fetchChannels();
const channel = store.channels.get(lndChannel.chanId) as Channel;
expect(channel.remoteFeeRate).toBe(0);
// the alias is fetched from the API and should be updated after a few ticks
await waitFor(() => {
expect(channel.remoteFeeRate.toString()).toBe(
lndGetChanInfo.node1Policy.feeRateMilliMsat,
);
});
});
it('should use cached fee rates for channels', async () => {
const rate = +Big(lndGetChanInfo.node1Policy.feeRateMilliMsat).div(1000000).mul(100);
const cache = {
expires: Date.now() + 60 * 1000,
data: {
[lndGetChanInfo.channelId]: rate,
},
};
jest
.spyOn(window.sessionStorage.__proto__, 'getItem')
.mockReturnValue(JSON.stringify(cache));
const channel = Channel.create(rootStore, lndChannel);
store.channels = observable.map({
[channel.chanId]: channel,
});
await store.fetchFeeRates();
expect(channel.remoteFeeRate).toBe(rate);
expect(grpcMock.unary).not.toBeCalled();
});
describe('onChannelEvent', () => {
const {
OPEN_CHANNEL,
CLOSED_CHANNEL,
ACTIVE_CHANNEL,
INACTIVE_CHANNEL,
PENDING_OPEN_CHANNEL,
} = LND.ChannelEventUpdate.UpdateType;
beforeEach(async () => {
await store.fetchChannels();
});
it('should handle inactive channel event', async () => {
const event = { ...lndChannelEvent, type: INACTIVE_CHANNEL };
const len = lndListChannels.channelsList.length;
expect(store.activeChannels).toHaveLength(len);
store.onChannelEvent(event);
expect(store.activeChannels).toHaveLength(len - 1);
});
it('should handle active channel event', async () => {
await store.fetchChannels();
const chan = store.channels.get(lndListChannels.channelsList[0].chanId) as Channel;
chan.active = false;
const event = { ...lndChannelEvent, type: ACTIVE_CHANNEL };
const len = lndListChannels.channelsList.length;
expect(store.activeChannels).toHaveLength(len - 1);
store.onChannelEvent(event);
expect(store.activeChannels).toHaveLength(len);
});
it('should handle open channel event', async () => {
const event = { ...lndChannelEvent, type: OPEN_CHANNEL };
event.openChannel.chanId = '12345';
expect(store.channels.get('12345')).toBeUndefined();
store.onChannelEvent(event);
expect(store.channels.get('12345')).toBeDefined();
});
it('should handle close channel event', async () => {
const event = { ...lndChannelEvent, type: CLOSED_CHANNEL };
const chanId = event.closedChannel.chanId;
expect(store.channels.get(chanId)).toBeDefined();
store.onChannelEvent(event);
expect(store.channels.get(chanId)).toBeUndefined();
});
it('should handle pending open channel event', async () => {
const event = { ...lndChannelEvent, type: PENDING_OPEN_CHANNEL };
store.pendingChannels.clear();
expect(store.pendingChannels.size).toBe(0);
expect(() => store.onChannelEvent(event)).not.toThrow();
});
it('should do nothing for unknown channel event type', async () => {
const event = { ...lndChannelEvent, type: 99 };
const len = lndListChannels.channelsList.length;
expect(store.activeChannels).toHaveLength(len);
store.onChannelEvent(event as any);
expect(store.activeChannels).toHaveLength(len);
});
});
});
Example #15
Source File: orderStore.spec.ts From lightning-terminal with MIT License | 4 votes |
describe('OrderStore', () => {
let rootStore: Store;
let store: OrderStore;
beforeEach(() => {
rootStore = createStore();
store = rootStore.orderStore;
});
it('should list orders', async () => {
expect(store.orders.size).toBe(0);
await store.fetchOrders();
const count = poolListOrders.asksList.length + poolListOrders.bidsList.length;
expect(store.orders.size).toBe(count);
});
it('should handle errors fetching orders', async () => {
grpcMock.unary.mockImplementationOnce(() => {
throw new Error('test-err');
});
expect(rootStore.appView.alerts.size).toBe(0);
await store.fetchOrders();
await waitFor(() => {
expect(rootStore.appView.alerts.size).toBe(1);
expect(values(rootStore.appView.alerts)[0].message).toBe('test-err');
});
});
it.each<[number, string]>([
[AUCT.OrderState.ORDER_SUBMITTED, 'Submitted'],
[AUCT.OrderState.ORDER_CLEARED, 'Cleared'],
[AUCT.OrderState.ORDER_PARTIALLY_FILLED, 'Partially Filled'],
[AUCT.OrderState.ORDER_EXECUTED, 'Filled'],
[AUCT.OrderState.ORDER_CANCELED, 'Cancelled'],
[AUCT.OrderState.ORDER_EXPIRED, 'Expired'],
[AUCT.OrderState.ORDER_FAILED, 'Failed'],
[-1, 'Unknown'],
])('should return the correct order state label', (state: number, label: string) => {
const poolOrder = {
...(poolListOrders.asksList[0].details as POOL.Order.AsObject),
state: state as any,
};
const order = new Order(rootStore);
order.update(poolOrder, OrderType.Ask, 2016);
expect(order.stateLabel).toBe(label);
});
it('should update existing orders with the same nonce', async () => {
expect(store.orders.size).toEqual(0);
await store.fetchOrders();
expect(store.orders.size).toBeGreaterThan(0);
const prevOrder = values(store.orders).slice()[0];
const prevAmount = +prevOrder.amount;
prevOrder.amount = Big(123);
await store.fetchOrders();
const updatedOrder = values(store.orders).slice()[0];
// the existing order should be updated
expect(prevOrder).toBe(updatedOrder);
expect(+updatedOrder.amount).toEqual(prevAmount);
});
it('should submit an ask order', async () => {
await rootStore.accountStore.fetchAccounts();
const nonce = await store.submitOrder(
OrderType.Ask,
Big(100000),
2000,
2016,
100000,
253,
);
expect(nonce).toBe(hex(poolSubmitOrder.acceptedOrderNonce));
});
it('should submit a bid order', async () => {
await rootStore.accountStore.fetchAccounts();
const nonce = await store.submitOrder(
OrderType.Bid,
Big(100000),
2000,
2016,
100000,
253,
);
expect(nonce).toBe(hex(poolSubmitOrder.acceptedOrderNonce));
});
it('should handle invalid orders', async () => {
await rootStore.accountStore.fetchAccounts();
// handle the GetInfo call
grpcMock.unary.mockImplementationOnce((desc, opts) => {
opts.onEnd(sampleGrpcResponse(desc));
return undefined as any;
});
// mock the SubmitOrder response
grpcMock.unary.mockImplementationOnce((desc, opts) => {
if (desc.methodName === 'SubmitOrder') {
const res = {
...poolSubmitOrder,
invalidOrder: poolInvalidOrder,
};
opts.onEnd({
status: grpc.Code.OK,
message: { toObject: () => res },
} as any);
}
return undefined as any;
});
await store.submitOrder(OrderType.Bid, Big(100000), 2000, 2016, 100000, 253);
expect(rootStore.appView.alerts.size).toBe(1);
expect(values(rootStore.appView.alerts)[0].message).toBe(poolInvalidOrder.failString);
});
it('should throw if the fixed rate rate is too low', async () => {
await rootStore.accountStore.fetchAccounts();
await store.submitOrder(OrderType.Bid, Big(100000), 0.9, 20000, 100000, 253);
expect(rootStore.appView.alerts.size).toBe(1);
expect(values(rootStore.appView.alerts)[0].message).toMatch(/The rate is too low.*/);
});
it('should cancel an order', async () => {
await rootStore.accountStore.fetchAccounts();
await store.fetchOrders();
await store.cancelOrder(values(store.orders)[0].nonce);
expect(grpcMock.unary).toBeCalledWith(
expect.objectContaining({ methodName: 'CancelOrder' }),
expect.anything(),
);
});
it('should handle errors cancelling an order', async () => {
await rootStore.accountStore.fetchAccounts();
await store.fetchOrders();
grpcMock.unary.mockImplementationOnce(() => {
throw new Error('test-err');
});
expect(rootStore.appView.alerts.size).toBe(0);
await store.cancelOrder(values(store.orders)[0].nonce);
await waitFor(() => {
expect(rootStore.appView.alerts.size).toBe(1);
expect(values(rootStore.appView.alerts)[0].message).toBe('test-err');
});
});
});
Example #16
Source File: swapStore.spec.ts From lightning-terminal with MIT License | 4 votes |
describe('SwapStore', () => {
let rootStore: Store;
let store: SwapStore;
beforeEach(async () => {
rootStore = createStore();
store = rootStore.swapStore;
});
it('should add swapped channels', () => {
expect(store.swappedChannels.size).toBe(0);
store.addSwappedChannels('s1', ['c1', 'c2']);
expect(store.swappedChannels.size).toBe(2);
expect(store.swappedChannels.get('c1')).toEqual(['s1']);
expect(store.swappedChannels.get('c2')).toEqual(['s1']);
store.addSwappedChannels('s2', ['c2']);
expect(store.swappedChannels.size).toBe(2);
expect(store.swappedChannels.get('c2')).toEqual(['s1', 's2']);
});
it('should prune the swapped channels list', async () => {
await rootStore.channelStore.fetchChannels();
await store.fetchSwaps();
const swaps = store.sortedSwaps;
// make these swaps pending
swaps[0].state = LOOP.SwapState.HTLC_PUBLISHED;
swaps[1].state = LOOP.SwapState.INITIATED;
const channels = rootStore.channelStore.sortedChannels;
const [c1, c2, c3] = channels.map(c => c.chanId);
store.addSwappedChannels(swaps[0].id, [c1, c2]);
store.addSwappedChannels(swaps[1].id, [c2, c3]);
// confirm swapped channels are set
expect(store.swappedChannels.size).toBe(3);
expect(store.swappedChannels.get(c2)).toHaveLength(2);
// change one swap to complete
swaps[1].state = LOOP.SwapState.SUCCESS;
store.pruneSwappedChannels();
// confirm swap1 removed
expect(store.swappedChannels.size).toBe(2);
expect(store.swappedChannels.get(c1)).toHaveLength(1);
expect(store.swappedChannels.get(c2)).toHaveLength(1);
expect(store.swappedChannels.get(c3)).toBeUndefined();
});
it('should fetch list of swaps', async () => {
expect(store.sortedSwaps).toHaveLength(0);
await store.fetchSwaps();
expect(store.sortedSwaps).toHaveLength(7);
});
it('should handle errors fetching swaps', async () => {
grpcMock.unary.mockImplementationOnce(desc => {
if (desc.methodName === 'ListSwaps') throw new Error('test-err');
return undefined as any;
});
expect(rootStore.appView.alerts.size).toBe(0);
await store.fetchSwaps();
await waitFor(() => {
expect(rootStore.appView.alerts.size).toBe(1);
expect(values(rootStore.appView.alerts)[0].message).toBe('test-err');
});
});
it('should update existing swaps with the same id', async () => {
expect(store.swaps.size).toEqual(0);
await store.fetchSwaps();
expect(store.swaps.size).toEqual(loopListSwaps.swapsList.length);
const prevSwap = store.sortedSwaps[0];
const prevAmount = prevSwap.amount;
prevSwap.amount = Big(123);
await store.fetchSwaps();
const updatedSwap = store.sortedSwaps[0];
// the existing swap should be updated
expect(prevSwap).toBe(updatedSwap);
expect(updatedSwap.amount).toEqual(prevAmount);
});
it.each<[number, string]>([
[LOOP.SwapState.INITIATED, 'Initiated'],
[LOOP.SwapState.PREIMAGE_REVEALED, 'Preimage Revealed'],
[LOOP.SwapState.HTLC_PUBLISHED, 'HTLC Published'],
[LOOP.SwapState.SUCCESS, 'Success'],
[LOOP.SwapState.FAILED, 'Failed'],
[LOOP.SwapState.INVOICE_SETTLED, 'Invoice Settled'],
[-1, 'Unknown'],
])('should display the correct label for swap state %s', async (state, label) => {
await store.fetchSwaps();
const swap = store.sortedSwaps[0];
swap.state = state;
expect(swap.stateLabel).toEqual(label);
});
it.each<[number, string]>([
[LOOP.SwapType.LOOP_IN, 'Loop In'],
[LOOP.SwapType.LOOP_OUT, 'Loop Out'],
[-1, 'Unknown'],
])('should display the correct name for swap type %s', async (type, label) => {
await store.fetchSwaps();
const swap = store.sortedSwaps[0];
swap.type = type;
expect(swap.typeName).toEqual(label);
});
it('should handle swap events', () => {
const swap = loopListSwaps.swapsList[0];
swap.id += 'test';
expect(store.sortedSwaps).toHaveLength(0);
store.onSwapUpdate(swap);
expect(store.sortedSwaps).toHaveLength(1);
});
});
Example #17
Source File: batchStore.spec.ts From lightning-terminal with MIT License | 4 votes |
describe('BatchStore', () => {
let rootStore: Store;
let store: BatchStore;
let index: number;
beforeEach(() => {
rootStore = createStore();
store = rootStore.batchStore;
// mock the BatchSnapshot response to return a unique id for each batch
// to avoid overwriting the same record in the store
index = 0;
grpcMock.unary.mockImplementation((desc, opts) => {
let res: any;
if (desc.methodName === 'BatchSnapshots') {
const count = (opts.request.toObject() as any).numBatchesBack;
res = {
batchesList: [...Array(count)].map((_, i) => ({
...poolBatchSnapshot,
batchId: `${index + i}-${poolBatchSnapshot.batchId}`,
prevBatchId: `${index + i}-${poolBatchSnapshot.prevBatchId}`,
})),
};
index += BATCH_QUERY_LIMIT;
} else if (desc.methodName === 'LeaseDurations') {
res = poolLeaseDurations;
}
opts.onEnd({
status: grpc.Code.OK,
message: { toObject: () => res },
} as any);
return undefined as any;
});
});
it('should fetch batches', async () => {
expect(store.batches.size).toBe(0);
await store.fetchBatches();
expect(store.batches.size).toBe(BATCH_QUERY_LIMIT);
});
it('should append start from the oldest batch when fetching batches multiple times', async () => {
expect(store.batches.size).toBe(0);
await store.fetchBatches();
expect(store.batches.size).toBe(BATCH_QUERY_LIMIT);
// calling a second time should append new batches to the list
await store.fetchBatches();
expect(store.batches.size).toBe(BATCH_QUERY_LIMIT * 2);
});
it('should handle a number of batches less than the query limit', async () => {
// mock the BatchSnapshot response to return 5 batches with the last one having a
// blank prevBatchId to signify that there are no more batches available
grpcMock.unary.mockImplementation((desc, opts) => {
let res: any;
if (desc.methodName === 'BatchSnapshots') {
res = {
batchesList: [...Array(5)].map((_, i) => ({
...poolBatchSnapshot,
batchId: b64(`${hex(poolBatchSnapshot.batchId)}0${i}`),
prevBatchId: i < 4 ? b64(`${hex(poolBatchSnapshot.prevBatchId)}0${i}`) : '',
})),
};
index += BATCH_QUERY_LIMIT;
} else if (desc.methodName === 'LeaseDurations') {
res = poolLeaseDurations;
}
opts.onEnd({
status: grpc.Code.OK,
message: { toObject: () => res },
} as any);
return undefined as any;
});
expect(store.batches.size).toBe(0);
await store.fetchBatches();
expect(store.batches.size).toBe(5);
await store.fetchBatches();
expect(store.batches.size).toBe(5);
});
it('should handle errors when fetching batches', async () => {
grpcMock.unary.mockImplementation((desc, opts) => {
if (desc.methodName === 'BatchSnapshots') {
throw new Error('test-err');
}
opts.onEnd({
status: grpc.Code.OK,
message: { toObject: () => poolLeaseDurations },
} as any);
return undefined as any;
});
expect(rootStore.appView.alerts.size).toBe(0);
await store.fetchBatches();
expect(rootStore.appView.alerts.size).toBe(1);
expect(values(rootStore.appView.alerts)[0].message).toBe('test-err');
});
it('should not show error when last snapshot is not found', async () => {
grpcMock.unary.mockImplementation((desc, opts) => {
if (desc.methodName === 'BatchSnapshots') {
throw new Error('batch snapshot not found');
}
opts.onEnd({
status: grpc.Code.OK,
message: { toObject: () => poolLeaseDurations },
} as any);
return undefined as any;
});
expect(rootStore.appView.alerts.size).toBe(0);
await store.fetchBatches();
expect(rootStore.appView.alerts.size).toBe(0);
});
it('should fetch the latest batch', async () => {
await store.fetchBatches();
expect(store.batches.size).toBe(BATCH_QUERY_LIMIT);
// return the same last batch to ensure no new data is added
const lastBatchId = store.sortedBatches[0].batchId;
index--;
await store.fetchLatestBatch();
expect(store.batches.size).toBe(BATCH_QUERY_LIMIT);
expect(store.sortedBatches[0].batchId).toBe(lastBatchId);
// return a new batch as the latest
index = 100;
await store.fetchLatestBatch();
expect(store.batches.size).toBe(BATCH_QUERY_LIMIT + 1);
});
it('should handle errors when fetching the latest batch', async () => {
grpcMock.unary.mockImplementationOnce(() => {
throw new Error('test-err');
});
expect(rootStore.appView.alerts.size).toBe(0);
await store.fetchLatestBatch();
expect(rootStore.appView.alerts.size).toBe(1);
expect(values(rootStore.appView.alerts)[0].message).toBe('test-err');
});
it('should return the sorted batches', async () => {
await store.fetchBatches();
expect(store.sortedBatches[0].batchId).toBe(hex(`0-${poolBatchSnapshot.batchId}`));
expect(store.sortedBatches[BATCH_QUERY_LIMIT - 1].batchId).toBe(
hex(`19-${poolBatchSnapshot.batchId}`),
);
index = 500;
await store.fetchLatestBatch();
expect(store.sortedBatches[0].batchId).toBe(hex(`500-${poolBatchSnapshot.batchId}`));
expect(store.sortedBatches[BATCH_QUERY_LIMIT].batchId).toBe(
hex(`19-${poolBatchSnapshot.batchId}`),
);
});
it('should fetch lease durations', async () => {
expect(store.leaseDurations.size).toBe(0);
await store.fetchLeaseDurations();
expect(store.leaseDurations.size).toBe(
poolLeaseDurations.leaseDurationBucketsMap.length,
);
expect(store.selectedLeaseDuration).toBe(
poolLeaseDurations.leaseDurationBucketsMap[0][0],
);
});
it('should handle errors when fetching lease durations', async () => {
grpcMock.unary.mockImplementationOnce(() => {
throw new Error('test-err');
});
expect(rootStore.appView.alerts.size).toBe(0);
await store.fetchLeaseDurations();
expect(rootStore.appView.alerts.size).toBe(1);
expect(values(rootStore.appView.alerts)[0].message).toBe('test-err');
});
it('should fetch node tier', async () => {
// return sample data from gRPC requests instead of the batches defined in beforeEach()
grpcMock.unary.mockImplementation((desc, opts) => {
const path = `${desc.service.serviceName}.${desc.methodName}`;
opts.onEnd({
status: grpc.Code.OK,
message: { toObject: () => sampleApiResponses[path] },
} as any);
return undefined as any;
});
await rootStore.nodeStore.fetchInfo();
expect(store.nodeTier).toBeUndefined();
await store.fetchNodeTier();
expect(store.nodeTier).toBe(AUCT.NodeTier.TIER_1);
// set the pubkey to a random value
runInAction(() => {
rootStore.nodeStore.pubkey = 'asdf';
});
await store.fetchNodeTier();
// confirm the tier is set to T0 if the pubkey is not found in the response
expect(store.nodeTier).toBe(AUCT.NodeTier.TIER_0);
});
it('should handle errors when fetching node tier', async () => {
grpcMock.unary.mockImplementationOnce(() => {
throw new Error('test-err');
});
expect(rootStore.appView.alerts.size).toBe(0);
await store.fetchNodeTier();
expect(rootStore.appView.alerts.size).toBe(1);
expect(values(rootStore.appView.alerts)[0].message).toBe('test-err');
});
it('should set the active market', async () => {
expect(store.selectedLeaseDuration).toBe(0);
await store.fetchBatches();
expect(store.selectedLeaseDuration).toBe(2016);
expect(keys(store.leaseDurations)).toEqual([2016, 4032, 6048, 8064]);
store.setActiveMarket(4032);
expect(store.selectedLeaseDuration).toBe(4032);
store.setActiveMarket(5000);
expect(store.selectedLeaseDuration).toBe(4032);
expect(rootStore.appView.alerts.size).toBe(1);
});
it('should start and stop polling', async () => {
let callCount = 0;
injectIntoGrpcUnary(desc => {
if (desc.methodName === 'BatchSnapshots') callCount++;
});
// allow polling in this test
Object.defineProperty(config, 'IS_TEST', { get: () => false });
jest.useFakeTimers();
jest.spyOn(global, 'setInterval');
store.startPolling();
expect(setInterval).toBeCalled();
expect(callCount).toBe(0);
// fast forward 1 minute
jest.advanceTimersByTime(60 * 1000);
await waitFor(() => {
expect(callCount).toBe(1);
});
jest.spyOn(global, 'clearInterval');
store.stopPolling();
expect(clearInterval).toBeCalled();
// fast forward 1 more minute
jest.advanceTimersByTime(120 * 1000);
expect(callCount).toBe(1);
// revert IS_TEST
Object.defineProperty(config, 'IS_TEST', { get: () => true });
jest.useRealTimers();
});
});
Example #18
Source File: accountStore.spec.ts From lightning-terminal with MIT License | 4 votes |
describe('AccountStore', () => {
let rootStore: Store;
let store: AccountStore;
beforeEach(() => {
rootStore = createStore();
store = rootStore.accountStore;
});
it('should list accounts', async () => {
expect(store.accounts.size).toBe(0);
await store.fetchAccounts();
expect(store.accounts.size).toBe(poolListAccounts.accountsList.length);
});
it('should handle errors fetching accounts', async () => {
grpcMock.unary.mockImplementationOnce(() => {
throw new Error('test-err');
});
expect(rootStore.appView.alerts.size).toBe(0);
await store.fetchAccounts();
await waitFor(() => {
expect(rootStore.appView.alerts.size).toBe(1);
expect(values(rootStore.appView.alerts)[0].message).toBe('test-err');
});
});
it('should return sorted accounts', async () => {
const a = new Account(rootStore, { ...poolInitAccount, value: '300' });
const b = new Account(rootStore, { ...poolInitAccount, value: '100' });
const c = new Account(rootStore, {
...poolInitAccount,
expirationHeight: 5000,
state: POOL.AccountState.PENDING_OPEN,
});
const d = new Account(rootStore, {
...poolInitAccount,
expirationHeight: 2000,
state: POOL.AccountState.PENDING_UPDATE,
});
// make the traderKey's unique
[a, b, c, d].forEach((acct, i) => {
acct.traderKey = `${i}${acct.traderKey}`;
});
store.accounts.set(d.traderKey, d);
store.accounts.set(c.traderKey, c);
store.accounts.set(b.traderKey, b);
store.accounts.set(a.traderKey, a);
const expected = [a, b, c, d].map(x => x.traderKey);
const actual = store.sortedAccounts.map(x => x.traderKey);
expect(actual).toEqual(expected);
});
it('should excluded closed accounts in sorted accounts', async () => {
const a = new Account(rootStore, { ...poolInitAccount, value: '300' });
const b = new Account(rootStore, { ...poolInitAccount, value: '100' });
const c = new Account(rootStore, {
...poolInitAccount,
expirationHeight: 5000,
state: POOL.AccountState.CLOSED,
});
const d = new Account(rootStore, {
...poolInitAccount,
expirationHeight: 2000,
state: POOL.AccountState.PENDING_OPEN,
});
// make the traderKey's unique
[a, b, c, d].forEach((acct, i) => {
acct.traderKey = `${i}${acct.traderKey}`;
});
store.accounts.set(d.traderKey, d);
store.accounts.set(c.traderKey, c);
store.accounts.set(b.traderKey, b);
store.accounts.set(a.traderKey, a);
const expected = [a, b, d].map(x => x.traderKey);
const actual = store.sortedAccounts.map(x => x.traderKey);
expect(actual).toEqual(expected);
});
it.each<[number, string]>([
[POOL.AccountState.PENDING_OPEN, 'Pending Open'],
[POOL.AccountState.PENDING_UPDATE, 'Pending Update'],
[POOL.AccountState.PENDING_BATCH, 'Pending Batch'],
[POOL.AccountState.OPEN, 'Open'],
[POOL.AccountState.EXPIRED, 'Expired'],
[POOL.AccountState.PENDING_CLOSED, 'Pending Closed'],
[POOL.AccountState.CLOSED, 'Closed'],
[POOL.AccountState.RECOVERY_FAILED, 'Recovery Failed'],
[-1, 'Unknown'],
])('should return the correct account state label', (state: number, label: string) => {
const poolAccount = {
...poolListAccounts.accountsList[0],
state: state as any,
};
const account = new Account(rootStore, poolAccount);
expect(account.stateLabel).toBe(label);
});
it('should update existing accounts with the same id', async () => {
expect(store.accounts.size).toEqual(0);
await store.fetchAccounts();
expect(store.accounts.size).toEqual(poolListAccounts.accountsList.length);
const prevAcct = values(store.accounts).slice()[0];
const prevBalance = prevAcct.totalBalance;
prevAcct.totalBalance = Big(123);
await store.fetchAccounts();
const updatedChan = values(store.accounts).slice()[0];
// the existing channel should be updated
expect(prevAcct).toBe(updatedChan);
expect(updatedChan.totalBalance).toEqual(prevBalance);
});
it('should handle errors querying the active account', async () => {
expect(() => store.activeAccount).toThrow();
await store.fetchAccounts();
expect(() => store.activeAccount).not.toThrow();
store.activeTraderKey = 'invalid';
expect(() => store.activeAccount).toThrow();
});
it('should copy the txn id to clipboard', async () => {
await store.fetchAccounts();
store.copyTxnId();
expect(copyToClipboard).toBeCalledWith(store.activeAccount.fundingTxnId);
expect(rootStore.appView.alerts.size).toBe(1);
expect(values(rootStore.appView.alerts)[0].message).toBe(
'Copied funding txn ID to clipboard',
);
});
it('should create a new Account', async () => {
expect(store.accounts.size).toEqual(0);
await store.createAccount(Big(3000000), 4032);
expect(store.accounts.size).toEqual(1);
expect(store.activeAccount).toBeDefined();
});
it('should handle errors creating a new Account', async () => {
grpcMock.unary.mockImplementationOnce(() => {
throw new Error('test-err');
});
expect(rootStore.appView.alerts.size).toBe(0);
await store.createAccount(Big(3000000), 4032);
await waitFor(() => {
expect(rootStore.appView.alerts.size).toBe(1);
expect(values(rootStore.appView.alerts)[0].message).toBe('test-err');
});
});
it('should close an Account', async () => {
await store.fetchAccounts();
const txid = await store.closeAccount(100);
expect(txid).toEqual(poolCloseAccount.closeTxid);
});
it('should handle errors closing an Account', async () => {
await store.fetchAccounts();
grpcMock.unary.mockImplementationOnce(() => {
throw new Error('test-err');
});
expect(rootStore.appView.alerts.size).toBe(0);
await store.closeAccount(100);
await waitFor(() => {
expect(rootStore.appView.alerts.size).toBe(1);
expect(values(rootStore.appView.alerts)[0].message).toBe('test-err');
});
});
it('should renew an Account', async () => {
await store.fetchAccounts();
const txid = await store.renewAccount(100, 253);
expect(txid).toEqual(poolCloseAccount.closeTxid);
});
it('should handle errors renewing an Account', async () => {
await store.fetchAccounts();
grpcMock.unary.mockImplementationOnce(() => {
throw new Error('test-err');
});
expect(rootStore.appView.alerts.size).toBe(0);
await store.renewAccount(100, 253);
await waitFor(() => {
expect(rootStore.appView.alerts.size).toBe(1);
expect(values(rootStore.appView.alerts)[0].message).toBe('test-err');
});
});
it('should deposit funds into an account', async () => {
await store.fetchAccounts();
const txid = await store.deposit(1);
expect(store.activeAccount.totalBalance.toString()).toBe(
poolDepositAccount.account?.value,
);
expect(txid).toEqual(poolDepositAccount.depositTxid);
});
it('should handle errors depositing funds', async () => {
await store.fetchAccounts();
grpcMock.unary.mockImplementationOnce(() => {
throw new Error('test-err');
});
expect(rootStore.appView.alerts.size).toBe(0);
await store.deposit(1);
await waitFor(() => {
expect(rootStore.appView.alerts.size).toBe(1);
expect(values(rootStore.appView.alerts)[0].message).toBe('test-err');
});
});
it('should withdraw funds from an account', async () => {
await store.fetchAccounts();
const txid = await store.withdraw(1);
expect(store.activeAccount.totalBalance.toString()).toBe(
poolWithdrawAccount.account?.value,
);
expect(txid).toEqual(poolWithdrawAccount.withdrawTxid);
});
it('should handle errors withdrawing funds', async () => {
await store.fetchAccounts();
grpcMock.unary.mockImplementationOnce(() => {
throw new Error('test-err');
});
expect(rootStore.appView.alerts.size).toBe(0);
await store.withdraw(1);
await waitFor(() => {
expect(rootStore.appView.alerts.size).toBe(1);
expect(values(rootStore.appView.alerts)[0].message).toBe('test-err');
});
});
});
Example #19
Source File: SwapWizard.spec.tsx From lightning-terminal with MIT License | 4 votes |
describe('SwapWizard component', () => {
let store: Store;
beforeEach(async () => {
store = createStore();
await store.fetchAllData();
await store.buildSwapView.startSwap();
store.channelStore.sortedChannels.slice(0, 3).forEach(c => {
store.buildSwapView.toggleSelectedChannel(c.chanId);
});
await store.buildSwapView.setDirection(SwapDirection.OUT);
});
const render = () => {
return renderWithProviders(<SwapWizard />, store);
};
describe('General behavior', () => {
it('should display the description labels', () => {
const { getByText } = render();
expect(getByText('Step 1 of 2')).toBeInTheDocument();
expect(getByText('Loop Out Amount')).toBeInTheDocument();
});
it('should navigate forward and back through each step', async () => {
const { getByText } = render();
expect(getByText('Step 1 of 2')).toBeInTheDocument();
fireEvent.click(getByText('Next'));
expect(getByText('Step 2 of 2')).toBeInTheDocument();
fireEvent.click(getByText('Confirm'));
expect(getByText('Submitting Loop')).toBeInTheDocument();
fireEvent.click(getByText('arrow-left.svg'));
expect(getByText('Step 2 of 2')).toBeInTheDocument();
fireEvent.click(getByText('arrow-left.svg'));
expect(getByText('Step 1 of 2')).toBeInTheDocument();
});
});
describe('Config Step', () => {
it('should display the correct min an max values', () => {
const { getByText } = render();
const { min, max } = store.buildSwapView.termsForDirection;
expect(getByText(formatSats(min))).toBeInTheDocument();
expect(getByText(formatSats(max))).toBeInTheDocument();
});
it('should display the correct number of channels', () => {
const { getByText } = render();
const { selectedChanIds } = store.buildSwapView;
expect(getByText(`${selectedChanIds.length}`)).toBeInTheDocument();
});
it('should update the amount when the slider changes', () => {
const { getByText, getByLabelText } = render();
const build = store.buildSwapView;
expect(+build.amountForSelected).toEqual(625000);
expect(getByText(`625,000 sats`)).toBeInTheDocument();
fireEvent.change(getByLabelText('range-slider'), { target: { value: '575000' } });
expect(+build.amountForSelected).toEqual(575000);
expect(getByText(`575,000 sats`)).toBeInTheDocument();
});
it('should show additional options', () => {
const { getByText } = render();
fireEvent.click(getByText('Additional Options'));
expect(getByText('Hide Options')).toBeInTheDocument();
});
it('should store the specified conf target', () => {
const { getByText, getByPlaceholderText } = render();
fireEvent.click(getByText('Additional Options'));
fireEvent.change(getByPlaceholderText('number of blocks (ex: 6)'), {
target: { value: 20 },
});
expect(store.buildSwapView.confTarget).toBeUndefined();
fireEvent.click(getByText('Next'));
expect(store.buildSwapView.confTarget).toBe(20);
});
it('should store the specified destination address', () => {
const { getByText, getByPlaceholderText } = render();
fireEvent.click(getByText('Additional Options'));
fireEvent.change(getByPlaceholderText('segwit address'), {
target: { value: 'abcdef' },
});
expect(store.buildSwapView.loopOutAddress).toBeUndefined();
fireEvent.click(getByText('Next'));
expect(store.buildSwapView.loopOutAddress).toBe('abcdef');
});
it('should handle invalid conf target', () => {
const { getByText, getByPlaceholderText } = render();
fireEvent.click(getByText('Additional Options'));
fireEvent.change(getByPlaceholderText('number of blocks (ex: 6)'), {
target: { value: 'asdf' },
});
fireEvent.click(getByText('Next'));
expect(values(store.appView.alerts)[0].message).toBe(
'Confirmation target must be between 20 and 60.',
);
});
});
describe('Review Step', () => {
beforeEach(async () => {
store.buildSwapView.setAmount(Big(500000));
store.buildSwapView.goToNextStep();
await store.buildSwapView.getQuote();
});
it('should display the description labels', () => {
const { getByText } = render();
expect(getByText('Step 2 of 2')).toBeInTheDocument();
expect(getByText('Review Loop amount and fee')).toBeInTheDocument();
expect(getByText('Loop Out Amount')).toBeInTheDocument();
expect(getByText('Fees')).toBeInTheDocument();
expect(getByText('Total')).toBeInTheDocument();
});
it('should display the correct values', () => {
const { getByText } = render();
const build = store.buildSwapView;
expect(getByText(formatSats(build.amount))).toBeInTheDocument();
expect(getByText(build.feesLabel)).toBeInTheDocument();
expect(getByText(formatSats(build.invoiceTotal))).toBeInTheDocument();
});
});
describe('Processing Step', () => {
beforeEach(async () => {
store.buildSwapView.setAmount(Big(500000));
store.buildSwapView.goToNextStep();
await store.buildSwapView.getQuote();
store.buildSwapView.goToNextStep();
});
it('should display the description label', () => {
const { getByText } = render();
expect(getByText('Submitting Loop')).toBeInTheDocument();
});
});
});