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 vote down vote up
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 vote down vote up
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 vote down vote up
/** 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 vote down vote up
/**
   * 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 vote down vote up
/**
   * 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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
/** 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 vote down vote up
/** 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 vote down vote up
/**
   * 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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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();
    });
  });
});