events#once TypeScript Examples

The following examples show how to use events#once. 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: utils.spec.ts    From hoprnet with GNU General Public License v3.0 7 votes vote down vote up
/**
 * Creates a UDP socket and binds it to the given port.
 * @param port port to which the socket should be bound
 * @returns a bound socket
 */
export function bindToUdpSocket(port?: number): Promise<Socket> {
  const socket = createSocket('udp4')

  return new Promise<Socket>((resolve, reject) => {
    socket.once('error', (err: any) => {
      socket.removeListener('listening', resolve)
      reject(err)
    })
    socket.once('listening', () => {
      socket.removeListener('error', reject)
      resolve(socket)
    })

    try {
      socket.bind(port)
    } catch (err) {
      reject(err)
    }
  })
}
Example #2
Source File: rpcCall.ts    From defillama-sdk with GNU Affero General Public License v3.0 6 votes vote down vote up
export async function call(provider: BaseProvider, data: Deferrable<TransactionRequest>, block: BlockTag, chain?: string) {
  if (!chain) chain = 'noChain'
  const counter: Counter = getChainCounter(chain)
  const currentId = counter.requestCount++
  const eventId = `${chain}-${currentId}`

  if (counter.activeWorkers > maxParallelCalls) {
    counter.queue.push(eventId)
    await once(emitter, eventId)
  }

  counter.activeWorkers++

  if (DEBUG_MODE_ENABLED) {
    const showEveryX = counter.queue.length > 100 ? 50 : 10 // show log fewer times if lot more are queued up
    if (currentId % showEveryX === 0) console.log(`chain: ${chain} request #: ${currentId} queue: ${counter.queue.length} active requests: ${counter.activeWorkers}`)
  }

  let response
  try {
    response = await provider.call(data, block)
    onComplete()
  } catch (e) {
    onComplete()
    throw e
  }

  return response

  function onComplete() {
    counter.activeWorkers--
    if (counter.queue.length) {
      const nextRequestId = counter.pickFromTop ? counter.queue.shift() : counter.queue.pop()
      counter.pickFromTop = !counter.pickFromTop
      emitter.emit(<string> nextRequestId)
    }
  }
}
Example #3
Source File: CollectBlock.ts    From mineflayer-collectblock with MIT License 6 votes vote down vote up
/**
   * Cancels the current collection task, if still active.
   *
   * @param cb - The callback to use when the task is stopped.
   */
  async cancelTask (cb?: Callback): Promise<void> {
    if (this.targets.empty) {
      if (cb != null) cb()
      return await Promise.resolve()
    }
    this.bot.pathfinder.stop()
    if (cb != null) {
      // @ts-expect-error
      this.bot.once('collectBlock_finished', cb)
    }
    await once(this.bot, 'collectBlock_finished')
  }
Example #4
Source File: heads.ts    From phaneron with GNU General Public License v3.0 6 votes vote down vote up
async runEvents(): Promise<void> {
		if (this.headsSpec) {
			this.running = true
			let eventId = 0
			await this.loadEvent(this.headsSpec.events[eventId])
			while (this.running && eventId < this.headsSpec.events.length) {
				await this.runEvent(this.headsSpec.events[eventId])
				eventId++
				if (eventId < this.headsSpec.events.length)
					await this.loadEvent(this.headsSpec.events[eventId])

				await once(this.eventDone, 'done')
				if (eventId === this.headsSpec.events.length) {
					await this.channel.clear(0)
					this.running = false
				}
			}
		}
	}
Example #5
Source File: layer.ts    From phaneron with GNU General Public License v3.0 6 votes vote down vote up
async play(ticker?: (t: string) => void): Promise<void> {
		if (this.nextSrcSpec.source && this.nextSrcSpec.transition?.type === 'cut') {
			if (this.curSrcSpec.source) {
				this.curSrcSpec.source.release()
				await once(this.endEvent, 'end')
			}
			this.curSrcSpec.source = this.nextSrcSpec.source
			this.curSrcSpec.mixer = this.nextSrcSpec.mixer
		}

		this.curSrcSpec.transition = this.nextSrcSpec.transition
		if (this.curSrcSpec.transition.type !== 'cut') {
			this.curSrcSpec.transition.source = this.nextSrcSpec.source
			this.curSrcSpec.transition.sourceMixer = this.nextSrcSpec.mixer
		}
		this.nextSrcSpec.source = undefined
		this.nextSrcSpec.mixer = undefined
		this.nextSrcSpec.transition = JSON.parse(DefaultTransitionSpec)

		this.autoPlay = false
		this.layerTick = ticker
		this.curSrcSpec.source?.setPaused(false)
		this.curSrcSpec.transition?.source?.setPaused(false)
		this.curSrcSpec.transition?.mask?.setPaused(false)

		await this.update()
		this.channelUpdate()

		// delay further commands until any transition has completed - reduces demand on cpu/gpu
		if (this.curSrcSpec.transition.type !== 'cut') await once(this.endEvent, 'transitionComplete')
	}
Example #6
Source File: utils.spec.ts    From hoprnet with GNU General Public License v3.0 6 votes vote down vote up
export async function stopNode(socket: Closing) {
  const closePromise = once(socket, 'close')

  socket.close()

  return closePromise
}
Example #7
Source File: transitioner.ts    From phaneron with GNU General Public License v3.0 6 votes vote down vote up
async release(): Promise<void> {
		this.silence?.release()
		this.black?.release()
		await once(this.endEvent, 'end')
		this.silence = null
		this.black = null
		this.audTransition = null
		this.vidTransition?.finish()
		this.vidTransition = null
		this.audioPipe = undefined
		this.videoPipe = undefined
	}
Example #8
Source File: server.ts    From eufy-security-ws with MIT License 6 votes vote down vote up
async start(): Promise<void> {
        this.server = createServer();
        this.wsServer = new WebSocketServer({ server: this.server });
        this.sockets = new ClientsController(this.driver, this.logger);
        this.wsServer.on("connection", (socket, request) => this.sockets?.addSocket(socket, request));

        this.logger.debug(`Starting server on host ${this.options.host}, port ${this.options.port}`);

        this.server.on("error", this.onError.bind(this));
        this.server.listen(this.options.port, this.options.host);
        await once(this.server, "listening");
        this.emit("listening");
        this.logger.info(`Eufy Security server listening on host ${this.options.host}, port ${this.options.port}`);
        await this.driver.connect()
    }
Example #9
Source File: server.ts    From eufy-security-ws with MIT License 6 votes vote down vote up
async destroy(): Promise<void> {
        this.logger.debug(`Closing server...`);
        if (this.sockets) {
            this.sockets.disconnect();
        }
        if (this.server) {
            this.server.close();
            await once(this.server, "close");
        }

        this.logger.info(`Server closed`);
    }
Example #10
Source File: utils.spec.ts    From hoprnet with GNU General Public License v3.0 6 votes vote down vote up
export async function waitUntilListening<ListenOpts>(socket: Listening<ListenOpts>, port: ListenOpts) {
  const promise = once(socket, 'listening')

  socket.listen(port)

  return promise
}
Example #11
Source File: listener.ts    From hoprnet with GNU General Public License v3.0 6 votes vote down vote up
/**
   * Closes the UDP socket
   * @returns Promise that resolves once UDP socket is closed
   */
  private closeUDP() {
    const promise = once(this.udpSocket, 'close')

    this.udpSocket.close()

    return promise
  }
Example #12
Source File: listener.ts    From hoprnet with GNU General Public License v3.0 6 votes vote down vote up
/**
   * Closes the TCP socket and tries to close all pending
   * connections.
   * @returns Promise that resolves once TCP socket is closed
   */
  private async closeTCP() {
    if (!this.tcpSocket.listening) {
      return
    }

    await Promise.all(this.__connections.map((conn: MultiaddrConnection) => attemptClose(conn, error)))

    const promise = once(this.tcpSocket, 'close')

    this.tcpSocket.close()

    // Node.js bug workaround: ocassionally on macOS close is not emitted and callback is not called
    return Promise.race([
      promise,
      new Promise<void>((resolve) =>
        setTimeout(() => {
          resolve()
        }, SOCKET_CLOSE_TIMEOUT)
      )
    ])
  }
Example #13
Source File: listener.ts    From hoprnet with GNU General Public License v3.0 6 votes vote down vote up
/**
   * Tracks connections to close them once necessary.
   * @param maConn connection to track
   */
  private trackConn(maConn: MultiaddrConnection) {
    this.__connections.push(maConn)
    verbose(`currently tracking ${this.__connections.length} connections ++`)

    const untrackConn = () => {
      verbose(`currently tracking ${this.__connections.length} connections --`)
      let index = this.__connections.findIndex((c: MultiaddrConnection) => c === maConn)

      if (index < 0) {
        // connection not found
        verbose(`DEBUG: Connection not found.`, maConn)
        return
      }

      if ([index + 1, 1].includes(this.__connections.length)) {
        this.__connections.pop()
      } else {
        this.__connections[index] = this.__connections.pop() as MultiaddrConnection
      }
    }

    ;(maConn.conn as EventEmitter).once('close', untrackConn)
  }
Example #14
Source File: listener.ts    From hoprnet with GNU General Public License v3.0 6 votes vote down vote up
attachSocketHandlers() {
    this.udpSocket.once('close', () => {
      if (![State.CLOSING, State.CLOSED].includes(this.state)) {
        console.trace(`UDP socket was closed earlier than expected. Please report this!`)
      }
    })

    this.tcpSocket.once('close', () => {
      if (![State.CLOSING, State.CLOSED].includes(this.state)) {
        console.trace(`TCP socket was closed earlier than expected. Please report this!`)
      }
    })

    // Forward socket errors
    this.tcpSocket.on('error', (err) => this.emit('error', err))
    this.udpSocket.on('error', (err) => this.emit('error', err))

    this.tcpSocket.on('connection', async (socket: TCPSocket) => {
      try {
        await this.onTCPConnection(socket)
      } catch (err) {
        error(`network error`, err)
      }
    })
    this.udpSocket.on('message', (msg: Buffer, rinfo: RemoteInfo) => handleStunRequest(this.udpSocket, msg, rinfo))
  }
Example #15
Source File: heartbeat.spec.ts    From hoprnet with GNU General Public License v3.0 5 votes vote down vote up
/**
 * Creates an event-based fake network
 * @returns a fake network
 */
function createFakeNetwork() {
  const network = new EventEmitter()

  const subscribedPeers = new Map<string, string>()

  // mocks libp2p.handle(protocol)
  const subscribe = (
    self: PeerId,
    protocol: string,
    handler: (msg: Uint8Array, remotePeer: PeerId) => Promise<Uint8Array>
  ) => {
    network.on(reqEventName(self, protocol), async (from: PeerId, request: Uint8Array) => {
      const response = await handler(request, from)

      network.emit(resEventName(self, from, protocol), self, response)
    })

    subscribedPeers.set(self.toB58String(), reqEventName(self, protocol))
  }

  // mocks libp2p.dialProtocol
  const sendMessage = async (self: PeerId, dest: PeerId, protocol: string, msg: Uint8Array) => {
    if (network.listenerCount(reqEventName(dest, protocol)) > 0) {
      const recvPromise = once(network, resEventName(dest, self, protocol))

      network.emit(reqEventName(dest, protocol), self, msg)

      const result = (await recvPromise) as [from: PeerId, response: Uint8Array]

      return Promise.resolve([result[1]])
    }

    return Promise.reject()
  }

  // mocks libp2p.stop
  const unsubscribe = (peer: PeerId) => {
    if (subscribedPeers.has(peer.toB58String())) {
      const protocol = subscribedPeers.get(peer.toB58String())

      network.removeAllListeners(protocol)
    }
  }

  return {
    subscribe,
    sendMessage,
    close: network.removeAllListeners.bind(network),
    unsubscribe
  }
}
Example #16
Source File: entry.spec.ts    From hoprnet with GNU General Public License v3.0 5 votes vote down vote up
function createFakeNetwork() {
  const network = new EventEmitter()

  const listen = (addr: string) => {
    const emitter = new EventEmitter()
    network.on(connectEvent(addr), () => emitter.emit('connected'))

    return emitter
  }

  const connect = (ma: Multiaddr, opts: HoprConnectDialOptions) => {
    const addr = ma.toString()

    if (opts.onDisconnect) {
      network.once(disconnectEvent(addr), () => opts?.onDisconnect?.(ma))
    }

    if (network.listeners(connectEvent(addr)).length >= 1) {
      network.emit(connectEvent(addr))

      const conn = {
        _closed: false,
        close: async () => {
          conn._closed = true
        },
        newStream: (_protocols: string[]) =>
          Promise.resolve({
            stream: {
              source: (async function* () {
                yield OK
              })(),
              sink: async (source: AsyncIterableIterator<any>) => {
                // consume the send stream
                for await (const _sth of source) {
                }
              }
            }
          })
      }
      return Promise.resolve(conn)
    } else {
      return Promise.resolve(undefined)
    }
  }

  const close = (ma: Multiaddr) => {
    network.emit(disconnectEvent(ma.toString()))
  }

  return {
    listen,
    connect,
    close,
    stop: network.removeAllListeners.bind(network)
  }
}
Example #17
Source File: inspector-util.ts    From stacks-blockchain-api with GNU General Public License v3.0 5 votes vote down vote up
export async function startProfilerServer(
  httpServerPort?: number | string
): Promise<{
  server: Server;
  address: string;
  close: () => Promise<void>;
}> {
  let serverPort: number | undefined = undefined;
  if (httpServerPort !== undefined) {
    serverPort = parsePort(httpServerPort);
  }
  const app = express();

  let existingSession:
    | { instance: ProfilerInstance<unknown>; response: express.Response }
    | undefined;

  app.get(
    '/profile/cpu',
    asyncHandler(async (req, res) => {
      if (existingSession) {
        res.status(409).json({ error: 'Profile session already in progress' });
        return;
      }
      const durationParam = req.query['duration'];
      const seconds = Number.parseFloat(durationParam as string);
      if (!Number.isFinite(seconds) || seconds < 0) {
        res.status(400).json({ error: `Invalid 'duration' query parameter "${durationParam}"` });
        return;
      }
      const samplingIntervalParam = req.query['sampling_interval'];
      let samplingInterval: number | undefined;
      if (samplingIntervalParam !== undefined) {
        samplingInterval = Number.parseFloat(samplingIntervalParam as string);
        if (!Number.isInteger(samplingInterval) || samplingInterval < 0) {
          res.status(400).json({
            error: `Invalid 'sampling_interval' query parameter "${samplingIntervalParam}"`,
          });
          return;
        }
      }
      const cpuProfiler = initCpuProfiling(samplingInterval);
      existingSession = { instance: cpuProfiler, response: res };
      try {
        const filename = `cpu_${Math.round(Date.now() / 1000)}_${seconds}-seconds.cpuprofile`;
        res.setHeader('Cache-Control', 'no-store');
        res.setHeader('Transfer-Encoding', 'chunked');
        res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
        res.setHeader('Content-Type', 'application/json; charset=utf-8');
        res.flushHeaders();
        await cpuProfiler.start();
        const ac = new AbortController();
        const timeoutPromise = timeout(seconds * 1000, ac);
        await Promise.race([timeoutPromise, once(res, 'close')]);
        if (res.writableEnded || res.destroyed) {
          // session was cancelled
          ac.abort();
          return;
        }
        const result = await cpuProfiler.stop();
        const resultString = JSON.stringify(result);
        logger.info(
          `[CpuProfiler] Completed, total profile report JSON string length: ${resultString.length}`
        );
        res.end(resultString);
      } finally {
        const session = existingSession;
        existingSession = undefined;
        await session?.instance.dispose().catch();
      }
    })
  );

  let neonProfilerRunning: boolean = false;

  app.get(
    '/profile/cpu/start',
    asyncHandler(async (req, res) => {
      if (existingSession) {
        res.status(409).json({ error: 'Profile session already in progress' });
        return;
      }
      const samplingIntervalParam = req.query['sampling_interval'];
      let samplingInterval: number | undefined;
      if (samplingIntervalParam !== undefined) {
        samplingInterval = Number.parseFloat(samplingIntervalParam as string);
        if (!Number.isInteger(samplingInterval) || samplingInterval < 0) {
          res.status(400).json({
            error: `Invalid 'sampling_interval' query parameter "${samplingIntervalParam}"`,
          });
          return;
        }
      }
      const cpuProfiler = initCpuProfiling(samplingInterval);
      existingSession = { instance: cpuProfiler, response: res };
      await cpuProfiler.start();
      const profilerRunningLogger = setInterval(() => {
        if (existingSession) {
          logger.error(`CPU profiler has been enabled for a long time`);
        } else {
          clearInterval(profilerRunningLogger);
        }
      }, 10_000).unref();
      res.end('CPU profiler started');
    })
  );

  app.get('/profile/native/cpu/start', (req, res) => {
    if (neonProfilerRunning) {
      res.status(500).end('error: profiler already started');
      return;
    }
    neonProfilerRunning = true;
    try {
      const startResponse = startProfiler();
      console.log(startResponse);
      res.end(startResponse);
    } catch (error) {
      console.error(error);
      res.status(500).end(error);
    }
  });

  app.get('/profile/native/cpu/stop', (req, res) => {
    if (!neonProfilerRunning) {
      res.status(500).end('error: no profiler running');
      return;
    }
    neonProfilerRunning = false;
    let profilerResults: Buffer;
    try {
      profilerResults = stopProfiler();
    } catch (error: any) {
      console.error(error);
      res.status(500).end(error);
      return;
    }
    const fileName = `profile-${Date.now()}.svg`;
    res.setHeader('Cache-Control', 'no-store');
    res.setHeader('Transfer-Encoding', 'chunked');
    res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
    res.setHeader('Content-Type', 'image/svg+xml');
    res.status(200).send(profilerResults);
  });

  app.get(
    '/profile/cpu/stop',
    asyncHandler(async (req, res) => {
      if (!existingSession) {
        res.status(409).json({ error: 'No profile session in progress' });
        return;
      }
      if (existingSession.instance.sessionType !== 'cpu') {
        res.status(409).json({ error: 'No CPU profile session in progress' });
        return;
      }
      try {
        const elapsedSeconds = existingSession.instance.stopwatch.getElapsedSeconds();
        const timestampSeconds = Math.round(Date.now() / 1000);
        const filename = `cpu_${timestampSeconds}_${elapsedSeconds}-seconds.cpuprofile`;
        const result = await (existingSession.instance as ProfilerInstance<inspector.Profiler.Profile>).stop();
        const resultString = JSON.stringify(result);
        logger.info(
          `[CpuProfiler] Completed, total profile report JSON string length: ${resultString.length}`
        );

        res.setHeader('Cache-Control', 'no-store');
        res.setHeader('Transfer-Encoding', 'chunked');
        res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
        res.setHeader('Content-Type', 'application/json; charset=utf-8');
        // await new Promise<void>(resolve => res.end(resultString, () => resolve()));
        res.end(resultString);
      } finally {
        const session = existingSession;
        existingSession = undefined;
        await session?.instance.dispose().catch();
      }
    })
  );

  app.get(
    '/profile/heap_snapshot',
    asyncHandler(async (req, res) => {
      if (existingSession) {
        res.status(409).json({ error: 'Profile session already in progress' });
        return;
      }
      const filename = `heap_${Math.round(Date.now() / 1000)}.heapsnapshot`;
      const tmpFile = path.join(os.tmpdir(), filename);
      const fileWriteStream = fs.createWriteStream(tmpFile);
      const heapProfiler = initHeapSnapshot(fileWriteStream);
      existingSession = { instance: heapProfiler, response: res };
      try {
        res.setHeader('Cache-Control', 'no-store');
        res.setHeader('Transfer-Encoding', 'chunked');
        res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
        res.setHeader('Content-Type', 'application/json; charset=utf-8');
        res.flushHeaders();
        // Taking a heap snapshot (with current implementation) is a one-shot process ran to get the
        // applications current heap memory usage, rather than something done over time. So start and
        // stop without waiting.
        await heapProfiler.start();
        const result = await heapProfiler.stop();
        logger.info(
          `[HeapProfiler] Completed, total snapshot byte size: ${result.totalSnapshotByteSize}`
        );
        await pipelineAsync(fs.createReadStream(tmpFile), res);
      } finally {
        const session = existingSession;
        existingSession = undefined;
        await session?.instance.dispose().catch();
        try {
          fileWriteStream.destroy();
        } catch (_) {}
        try {
          logger.info(`[HeapProfiler] Cleaning up tmp file ${tmpFile}`);
          fs.unlinkSync(tmpFile);
        } catch (_) {}
      }
    })
  );

  app.get(
    '/profile/cancel',
    asyncHandler(async (req, res) => {
      if (!existingSession) {
        res.status(409).json({ error: 'No existing profile session is exists to cancel' });
        return;
      }
      const session = existingSession;
      await session.instance.stop().catch();
      await session.instance.dispose().catch();
      session.response.destroy();
      existingSession = undefined;
      await Promise.resolve();
      res.json({ ok: 'existing profile session stopped' });
    })
  );

  const server = createServer(app);

  const serverSockets = new Set<Socket>();
  server.on('connection', socket => {
    serverSockets.add(socket);
    socket.once('close', () => {
      serverSockets.delete(socket);
    });
  });

  await new Promise<void>((resolve, reject) => {
    try {
      server.once('error', error => {
        reject(error);
      });
      server.listen(serverPort, '0.0.0.0', () => {
        resolve();
      });
    } catch (error) {
      reject(error);
    }
  });

  const addr = server.address();
  if (addr === null) {
    throw new Error('server missing address');
  }
  const addrStr = typeof addr === 'string' ? addr : `${addr.address}:${addr.port}`;
  logger.info(`Started profiler server on: http://${addrStr}`);

  const closeServer = async () => {
    const closePromise = new Promise<void>((resolve, reject) => {
      if (!server.listening) {
        // Server already closed (can happen when server is shared between cluster workers)
        return resolve();
      }
      server.close(error => (error ? reject(error) : resolve()));
    });
    for (const socket of serverSockets) {
      socket.destroy();
    }
    await closePromise;
  };

  return { server, address: addrStr, close: closeServer };
}
Example #18
Source File: layer.ts    From phaneron with GNU General Public License v3.0 5 votes vote down vote up
async stop(): Promise<void> {
		if (this.curSrcSpec.source) {
			this.curSrcSpec.source.release()
			await once(this.endEvent, 'end')
		}
		this.autoPlay = false
	}
Example #19
Source File: layer.ts    From phaneron with GNU General Public License v3.0 5 votes vote down vote up
async load(
		producer: Producer,
		mixer: Mixer,
		transitionSpec: TransitionSpec,
		preview: boolean,
		autoPlay: boolean,
		channelUpdate: () => void
	): Promise<boolean> {
		this.nextSrcSpec = {
			source: producer,
			mixer: mixer,
			transition: transitionSpec,
			firstTs: undefined
		}
		this.autoPlay = autoPlay
		this.channelUpdate = channelUpdate

		if (this.autoPlay) {
			if (this.curSrcSpec.source) {
				this.endEvent.once('end', () => {
					this.curSrcSpec.source = undefined
					this.play()
				})
			} else {
				this.play()
			}
		} else if (preview) {
			if (this.curSrcSpec.source) {
				this.curSrcSpec.source.release()
				this.curSrcSpec.source = undefined
				this.curSrcSpec.mixer = undefined
				await once(this.endEvent, 'end')
			}
			this.curSrcSpec.source = this.nextSrcSpec.source
			this.curSrcSpec.mixer = this.nextSrcSpec.mixer
			this.nextSrcSpec.source = undefined
			this.nextSrcSpec.mixer = undefined
			await this.update()
			this.channelUpdate()
		}
		return true
	}
Example #20
Source File: entry.spec.ts    From hoprnet with GNU General Public License v3.0 4 votes vote down vote up
describe('entry node functionality', function () {
  const peerId = createPeerId()
  it('add public nodes', function () {
    const entryNodes = new TestingEntryNodes(
      peerId,
      {
        connectionManager: new FakeConnectionManager(true)
      },
      // Make sure that call is indeed asynchronous
      (async () => new Promise((resolve) => setImmediate(resolve))) as any,
      {}
    )

    entryNodes.start()

    const peerStoreEntry = getPeerStoreEntry(`/ip4/127.0.0.1/tcp/0`)

    entryNodes.onNewRelay(peerStoreEntry)
    // Should filter duplicate
    entryNodes.onNewRelay(peerStoreEntry)

    const uncheckedNodes = entryNodes.getUncheckedEntryNodes()

    assert(uncheckedNodes.length == 1, `Unchecked nodes must contain one entry`)
    assert(uncheckedNodes[0].id.equals(peerStoreEntry.id), `id must match the generated one`)
    assert(uncheckedNodes[0].multiaddrs.length == peerStoreEntry.multiaddrs.length, `must not contain more multiaddrs`)

    const usedRelays = entryNodes.getUsedRelayAddresses()
    assert(usedRelays == undefined || usedRelays.length == 0, `must not expose any internal addrs`)

    entryNodes.stop()
  })

  it('remove an offline node', function () {
    const entryNodes = new TestingEntryNodes(
      peerId,
      {
        connectionManager: new FakeConnectionManager()
      },
      (async () => new Promise((resolve) => setImmediate(resolve))) as any,
      {}
    )

    entryNodes.start()

    const peerStoreEntry = getPeerStoreEntry(`/ip4/127.0.0.1/tcp/0`)

    entryNodes.availableEntryNodes.push({
      ...peerStoreEntry,
      latency: 23
    })

    entryNodes.onRemoveRelay(peerStoreEntry.id)

    const availablePublicNodes = entryNodes.getAvailabeEntryNodes()
    assert(availablePublicNodes.length == 0, `must remove node from public nodes`)

    const usedRelays = entryNodes.getUsedRelayAddresses()
    assert(usedRelays == undefined || usedRelays.length == 0, `must not expose any internal addrs`)

    entryNodes.stop()
  })

  it('contact potential relays and update relay addresses', async function () {
    const network = createFakeNetwork()

    const relay = getPeerStoreEntry(`/ip4/127.0.0.1/tcp/12345`)

    const relayListener = network.listen(relay.multiaddrs[0].toString())

    const connectPromise = once(relayListener, 'connected')

    const entryNodes = new TestingEntryNodes(
      peerId,
      {
        connectionManager: new FakeConnectionManager(true)
      },
      network.connect as any,
      {
        initialNodes: [relay]
      }
    )

    entryNodes.start()

    await entryNodes.updatePublicNodes()

    await connectPromise

    const availableEntryNodes = entryNodes.getAvailabeEntryNodes()
    assert(availableEntryNodes.length == 1, `must contain exactly one public node`)
    assert(availableEntryNodes[0].id.equals(relay.id), `must contain correct peerId`)
    assert(availableEntryNodes[0].latency >= 0, `latency must be non-negative`)

    const usedRelays = entryNodes.getUsedRelayAddresses()
    assert(usedRelays != undefined, `must expose relay addrs`)
    assert(usedRelays.length == 1, `must expose exactly one relay addrs`)
    assert(
      usedRelays[0].toString() === `/p2p/${relay.id.toB58String()}/p2p-circuit/p2p/${peerId.toB58String()}`,
      `must expose the right relay address`
    )

    relayListener.removeAllListeners()
    network.stop()
    entryNodes.stop()
  })

  it('expose limited number of relay addresses', async function () {
    const network = createFakeNetwork()

    const relayNodes = Array.from<undefined, [Promise<any>, PeerStoreType, EventEmitter]>(
      { length: ENTRY_NODES_MAX_PARALLEL_DIALS + 1 },
      (_value: undefined, index: number) => {
        const relay = getPeerStoreEntry(`/ip4/127.0.0.1/tcp/${index}`)

        const relayListener = network.listen(relay.multiaddrs[0].toString())

        const connectPromise = once(relayListener, 'connected')

        return [connectPromise, relay, relayListener]
      }
    )

    const additionalOfflineNodes = [getPeerStoreEntry(`/ip4/127.0.0.1/tcp/23`)]

    const entryNodes = new TestingEntryNodes(
      peerId,
      {
        connectionManager: new FakeConnectionManager(true)
      },
      network.connect as any,
      {
        initialNodes: relayNodes.map((relayNode) => relayNode[1]).concat(additionalOfflineNodes)
      }
    )

    entryNodes.start()

    await entryNodes.updatePublicNodes()

    await Promise.all(relayNodes.map((relayNode) => relayNode[0]))

    const usedRelays = entryNodes.getUsedRelayAddresses()
    assert(usedRelays != undefined, `must expose relay addresses`)
    assert(usedRelays.length == MAX_RELAYS_PER_NODE, `must expose ${MAX_RELAYS_PER_NODE} relay addresses`)

    const availableEntryNodes = entryNodes.getAvailabeEntryNodes()
    assert(availableEntryNodes.length == ENTRY_NODES_MAX_PARALLEL_DIALS + 1)
    assert(
      relayNodes.every((relayNode) =>
        availableEntryNodes.some((availableEntryNode) => availableEntryNode.id.equals(relayNode[1].id))
      ),
      `must contain all relay nodes`
    )

    // cleanup
    relayNodes.forEach((relayNode) => relayNode[2].removeAllListeners())
    network.stop()
    entryNodes.stop()
  })

  it('update nodes once node became offline', async function () {
    const network = createFakeNetwork()

    const newNode = getPeerStoreEntry(`/ip4/127.0.0.1/tcp/1`)
    const relay = getPeerStoreEntry(`/ip4/127.0.0.1/tcp/2`)

    const newNodeListener = network.listen(newNode.multiaddrs[0].toString())

    const entryNodes = new TestingEntryNodes(
      peerId,
      {
        connectionManager: new FakeConnectionManager(true)
      },
      network.connect as any,
      {}
    )

    entryNodes.start()

    entryNodes.uncheckedEntryNodes.push(newNode)

    let usedRelay = {
      relayDirectAddress: new Multiaddr('/ip4/127.0.0.1/tcp/1234'),
      ourCircuitAddress: new Multiaddr(`/p2p/${relay.id.toB58String()}/p2p-circuit/p2p/${peerId.toB58String()}`)
    }

    entryNodes.usedRelays.push(usedRelay)

    // Should have one unchecked node and one relay node
    assert(entryNodes.getUsedRelayAddresses().length == 1)
    assert(entryNodes.getUncheckedEntryNodes().length == 1)

    const connectPromise = once(newNodeListener, 'connected')

    const updatePromise = once(entryNodes, RELAY_CHANGED_EVENT)

    entryNodes.onRemoveRelay(relay.id)

    await Promise.all([connectPromise, updatePromise])

    assert(entryNodes.getAvailabeEntryNodes().length == 1)

    const usedRelays = entryNodes.getUsedRelayAddresses()
    assert(entryNodes.getUsedRelayAddresses().length == 1)

    assert(
      usedRelays[0].equals(new Multiaddr(`/p2p/${newNode.id.toB58String()}/p2p-circuit/p2p/${peerId.toB58String()}`))
    )

    newNodeListener.removeAllListeners()
    network.stop()
    entryNodes.stop()
  })

  it('take those nodes that are online', async function () {
    const network = createFakeNetwork()

    const relay = getPeerStoreEntry(`/ip4/127.0.0.1/tcp/1`)
    const relayListener = network.listen(relay.multiaddrs[0].toString())

    const connectPromise = once(relayListener, 'connected')

    const entryNodes = new TestingEntryNodes(
      peerId,
      {
        connectionManager: new FakeConnectionManager(true)
      },
      network.connect as any,
      {}
    )

    entryNodes.start()

    const fakeNode = getPeerStoreEntry(`/ip4/127.0.0.1/tcp/2`)

    entryNodes.uncheckedEntryNodes.push(relay)
    entryNodes.uncheckedEntryNodes.push(fakeNode)

    await entryNodes.updatePublicNodes()

    await connectPromise

    const availableEntryNodes = entryNodes.getAvailabeEntryNodes()
    assert(availableEntryNodes.length == 1)
    assert(availableEntryNodes[0].id.equals(relay.id))

    const usedRelays = entryNodes.getUsedRelayAddresses()
    assert(usedRelays.length == 1)
    assert(
      usedRelays[0].equals(new Multiaddr(`/p2p/${relay.id.toB58String()}/p2p-circuit/p2p/${peerId.toB58String()}`))
    )

    network.stop()
    relayListener.removeAllListeners()
    entryNodes.stop()
  })

  it('no available entry nodes', async function () {
    const network = createFakeNetwork()

    const offlineRelay = getPeerStoreEntry(`/ip4/127.0.0.1/tcp/1`)

    const entryNodes = new TestingEntryNodes(
      peerId,
      {
        connectionManager: new FakeConnectionManager(true)
      },
      network.connect as any,
      {}
    )

    entryNodes.start()

    entryNodes.uncheckedEntryNodes.push(offlineRelay)

    await entryNodes.updatePublicNodes()

    const usedRelays = entryNodes.getUsedRelayAddresses()
    assert(usedRelays.length == 0)

    network.stop()
    entryNodes.stop()
  })

  it('do not emit listening event if nothing has changed', async function () {
    const entryNodes = new TestingEntryNodes(
      peerId,
      {
        connectionManager: new FakeConnectionManager(true)
      },
      (async () => {}) as any,
      {}
    )

    entryNodes.start()

    const relay = getPeerStoreEntry(`/ip4/127.0.0.1/tcp/1`)

    let usedRelay = {
      relayDirectAddress: new Multiaddr(`/ip4/127.0.0.1/tcp/1`),
      ourCircuitAddress: new Multiaddr(`/p2p/${relay.id.toB58String()}/p2p-circuit/p2p/${peerId.toB58String()}`)
    }

    entryNodes.availableEntryNodes.push({ ...relay, latency: 23 })
    entryNodes.usedRelays.push(usedRelay)

    entryNodes.once('listening', () =>
      assert.fail(`must not throw listening event if list of entry nodes has not changed`)
    )

    await entryNodes.updatePublicNodes()

    const availableEntryNodes = entryNodes.getAvailabeEntryNodes()
    assert(availableEntryNodes.length == 0)

    const usedRelays = entryNodes.getUsedRelayAddresses()
    assert(usedRelays.length == 0)

    entryNodes.stop()
  })

  it('events should trigger actions', async function () {
    const network = createFakeNetwork()

    const relay = getPeerStoreEntry(`/ip4/127.0.0.1/tcp/1`)
    const relayListener = network.listen(relay.multiaddrs[0].toString())

    const publicNodes = new EventEmitter() as PublicNodesEmitter
    const entryNodes = new TestingEntryNodes(
      peerId,
      {
        connectionManager: new FakeConnectionManager(true)
      },
      network.connect as any,
      {
        publicNodes
      }
    )

    entryNodes.start()

    publicNodes.emit('addPublicNode', relay)

    await once(entryNodes, RELAY_CHANGED_EVENT)

    publicNodes.emit('removePublicNode', relay.id)

    await once(entryNodes, RELAY_CHANGED_EVENT)

    entryNodes.once(RELAY_CHANGED_EVENT, () => assert.fail('Must not throw the relay:changed event'))

    await entryNodes.updatePublicNodes()

    relayListener.removeAllListeners()
    entryNodes.stop()
    network.stop()
  })

  it('renew DHT entry', async function () {
    const network = createFakeNetwork()

    const relay = getPeerStoreEntry(`/ip4/127.0.0.1/tcp/1`)

    const connectEmitter = network.listen(relay.multiaddrs[0].toString())

    let renews = 0

    connectEmitter.on('connected', () => renews++)

    const publicNodes: PublicNodesEmitter = new EventEmitter()

    const CUSTOM_DHT_RENEWAL_TIMEOUT = 100 // very short timeout

    const entryNodes = new TestingEntryNodes(
      peerId,
      {
        connectionManager: new FakeConnectionManager(true)
      },
      network.connect as any,
      {
        dhtRenewalTimeout: CUSTOM_DHT_RENEWAL_TIMEOUT,
        publicNodes
      }
    )

    entryNodes.start()

    publicNodes.emit('addPublicNode', relay)

    await new Promise((resolve) => setTimeout(resolve, 1e3))

    // depends on scheduler
    assert([9, 10].includes(renews), `Should capture at least 9 renews but not more than 10`)

    connectEmitter.removeAllListeners()
    entryNodes.stop()
    network.stop()
  })

  it('do not contact nodes we are already connected to', async function () {
    const entryNodes = new TestingEntryNodes(
      peerId,
      {
        connectionManager: new FakeConnectionManager(true)
      },
      // Make sure that call is indeed asynchronous
      (async () => new Promise((resolve) => setImmediate(resolve))) as any,
      {}
    )

    const ma = new Multiaddr('/ip4/8.8.8.8/tcp/9091')

    const peerStoreEntry = getPeerStoreEntry(ma.toString())

    entryNodes.usedRelays.push({
      relayDirectAddress: ma,
      ourCircuitAddress: new Multiaddr(
        `/p2p/${peerStoreEntry.id.toB58String()}/p2p-circuit/p2p/${peerId.toB58String()}`
      )
    })

    entryNodes.start()

    entryNodes.onNewRelay(peerStoreEntry)

    const uncheckedNodes = entryNodes.getUncheckedEntryNodes()

    assert(uncheckedNodes.length == 1, `Unchecked nodes must contain one entry`)
    assert(uncheckedNodes[0].id.equals(peerStoreEntry.id), `id must match the generated one`)
    assert(uncheckedNodes[0].multiaddrs.length == peerStoreEntry.multiaddrs.length, `must not contain more multiaddrs`)

    const usedRelays = entryNodes.getUsedRelayAddresses()
    assert(usedRelays.length == 1, `must not expose any relay addrs`)

    entryNodes.stop()
  })

  // it('reconnect on disconnect - temporarily offline', async function () {
  //   const network = createFakeNetwork()

  //   const relay = getPeerStoreEntry(`/ip4/1.2.3.4/tcp/1`)
  //   const relayListener = network.listen(relay.multiaddrs[0].toString())

  //   let secondAttempt = defer<void>()
  //   let connectAttempt = 0
  //   const entryNodes = new TestingEntryNodes(
  //     peerId,
  //     {
  //       connectionManager: new FakeConnectionManager(true)
  //     },
  //     (async (ma: Multiaddr, opts: any) => {
  //       switch (connectAttempt++) {
  //         case 0:
  //           return network.connect(ma, opts)
  //         case 1:
  //           return
  //         case 2:
  //           secondAttempt.resolve()
  //           return network.connect(ma, opts)
  //         default:
  //           return
  //       }
  //     }) as any,
  //     // Should be successful after second try
  //     {
  //       entryNodeReconnectBaseTimeout: 1,
  //       entryNodeReconnectBackoff: 5
  //     }
  //   )

  //   entryNodes.start()

  //   const updated = once(entryNodes, RELAY_CHANGED_EVENT)

  //   entryNodes.onNewRelay(relay)

  //   await updated

  //   // Should eventually remove relay from list
  //   network.close(relay.multiaddrs[0])

  //   await secondAttempt.promise

  //   // Wait for end of event loop
  //   await new Promise((resolve) => setImmediate(resolve))

  //   const availablePublicNodes = entryNodes.getAvailabeEntryNodes()
  //   assert(availablePublicNodes.length == 1, `must keep entry node after reconnect`)

  //   const usedRelays = entryNodes.getUsedRelayAddresses()
  //   assert(usedRelays.length == 1, `must keep relay address after reconnect`)

  //   relayListener.removeAllListeners()
  //   entryNodes.stop()
  // })

  // it('reconnect on disconnect - permanently offline', async function () {
  //   const network = createFakeNetwork()

  //   const relay = getPeerStoreEntry(`/ip4/1.2.3.4/tcp/1`)
  //   const relayListener = network.listen(relay.multiaddrs[0].toString())

  //   let connectAttempt = 0

  //   const entryNodes = new TestingEntryNodes(
  //     peerId,
  //     {
  //       connectionManager: new FakeConnectionManager(true)
  //     },
  //     (async (ma: Multiaddr, opts: any) => {
  //       switch (connectAttempt++) {
  //         case 0:
  //           return network.connect(ma, opts)
  //         default:
  //           return
  //       }
  //     }) as any,
  //     // Should fail after second try
  //     {
  //       entryNodeReconnectBaseTimeout: 1,
  //       entryNodeReconnectBackoff: 5
  //     }
  //   )

  //   entryNodes.start()

  //   const firstUpdate = once(entryNodes, RELAY_CHANGED_EVENT)

  //   entryNodes.onNewRelay(relay)

  //   await firstUpdate

  //   const secondUpdate = once(entryNodes, RELAY_CHANGED_EVENT)

  //   // Should eventually remove relay from list
  //   network.close(relay.multiaddrs[0])

  //   await secondUpdate

  //   const availablePublicNodes = entryNodes.getAvailabeEntryNodes()
  //   assert(availablePublicNodes.length == 0, `must remove node from public nodes`)

  //   const usedRelays = entryNodes.getUsedRelayAddresses()
  //   assert(usedRelays == undefined || usedRelays.length == 0, `must not expose any relay addrs`)

  //   relayListener.removeAllListeners()
  //   entryNodes.stop()
  // })
})
Example #21
Source File: websocket-tests.ts    From stacks-blockchain-api with GNU General Public License v3.0 4 votes vote down vote up
describe('websocket notifications', () => {
  let apiServer: ApiServer;

  let db: PgDataStore;
  let dbClient: PoolClient;

  beforeEach(async () => {
    process.env.PG_DATABASE = 'postgres';
    await cycleMigrations();
    db = await PgDataStore.connect({ usageName: 'tests' });
    dbClient = await db.pool.connect();
    apiServer = await startApiServer({
      datastore: db,
      chainId: ChainID.Testnet,
      httpLogLevel: 'silly',
    });
  });

  test('websocket rpc - tx subscription updates', async () => {
    const addr = apiServer.address;
    const wsAddress = `ws://${addr}/extended/v1/ws`;
    const socket = new WebSocket(wsAddress);
    const txId = '0x8912000000000000000000000000000000000000000000000000000000000000';

    try {
      await once(socket, 'open');
      const client = new RpcWebSocketClient();

      client.changeSocket(socket);
      client.listenMessages();

      // Subscribe to particular tx
      const subParams1: RpcTxUpdateSubscriptionParams = {
        event: 'tx_update',
        tx_id: txId,
      };
      const result = await client.call('subscribe', subParams1);
      expect(result).toEqual({ tx_id: txId });

      // Subscribe to mempool
      const subParams2: RpcMempoolSubscriptionParams = {
        event: 'mempool',
      };
      const result2 = await client.call('subscribe', subParams2);
      expect(result2).toEqual({});

      // watch for update to this tx
      let updateIndex = 0;
      const txUpdates: Waiter<TransactionStatus | MempoolTransactionStatus>[] = [
        waiter(),
        waiter(),
        waiter(),
        waiter(),
      ];
      const mempoolWaiter: Waiter<MempoolTransaction> = waiter();
      client.onNotification.push(msg => {
        if (msg.method === 'tx_update') {
          const txUpdate: RpcTxUpdateNotificationParams = msg.params;
          txUpdates[updateIndex++]?.finish(txUpdate.tx_status);
        }
        if (msg.method === 'mempool') {
          const mempoolTx: MempoolTransaction = msg.params;
          mempoolWaiter.finish(mempoolTx);
        }
      });

      const block = new TestBlockBuilder().addTx().build();
      await db.update(block);

      const mempoolTx = testMempoolTx({ tx_id: txId, status: DbTxStatus.Pending });
      await db.updateMempoolTxs({ mempoolTxs: [mempoolTx] });

      const microblock = new TestMicroblockStreamBuilder()
        .addMicroblock()
        .addTx({ tx_id: txId })
        .build();
      await db.updateMicroblocks(microblock);

      // check for tx update notification
      const txStatus1 = await txUpdates[0];
      expect(txStatus1).toBe('pending');

      // check for mempool update
      const mempoolUpdate = await mempoolWaiter;
      expect(mempoolUpdate.tx_id).toBe(txId);

      // check for microblock tx update notification
      const txStatus2 = await txUpdates[1];
      expect(txStatus2).toBe('pending');

      // update DB with TX after WS server is sent txid to monitor
      db.emit('txUpdate', txId);

      // check for tx update notification
      const txStatus3 = await txUpdates[2];
      expect(txStatus3).toBe('pending');

      // unsubscribe from notifications for this tx
      const unsubscribeResult = await client.call('unsubscribe', subParams1);
      expect(unsubscribeResult).toEqual({ tx_id: txId });

      // ensure tx updates no longer received
      db.emit('txUpdate', txId);
      await new Promise(resolve => setImmediate(resolve));
      expect(txUpdates[3].isFinished).toBe(false);
    } finally {
      socket.terminate();
    }
  });

  test('websocket rpc - block updates', async () => {
    const addr = apiServer.address;
    const wsAddress = `ws://${addr}/extended/v1/ws`;
    const socket = new WebSocket(wsAddress);

    await once(socket, 'open');
    const client = new RpcWebSocketClient();
    client.changeSocket(socket);
    client.listenMessages();

    const subParams: RpcBlockSubscriptionParams = {
      event: 'block',
    };
    const subResult = await client.call('subscribe', subParams);
    expect(subResult).toEqual({});

    const updateWaiter: Waiter<Block> = waiter();
    client.onNotification.push(msg => {
      if (msg.method === 'block') {
        const blockUpdate: Block = msg.params;
        updateWaiter.finish(blockUpdate);
      }
    });

    const block = new TestBlockBuilder({ block_hash: '0x1234', burn_block_hash: '0x5454' })
      .addTx({ tx_id: '0x4321' })
      .build();
    await db.update(block);

    const result = await updateWaiter;
    try {
      expect(result.hash).toEqual('0x1234');
      expect(result.burn_block_hash).toEqual('0x5454');
      expect(result.txs[0]).toEqual('0x4321');
    } finally {
      socket.terminate();
    }
  });

  test('websocket rpc - microblock updates', async () => {
    const addr = apiServer.address;
    const wsAddress = `ws://${addr}/extended/v1/ws`;
    const socket = new WebSocket(wsAddress);

    await once(socket, 'open');
    const client = new RpcWebSocketClient();
    client.changeSocket(socket);
    client.listenMessages();

    const subParams: RpcMicroblockSubscriptionParams = {
      event: 'microblock',
    };
    const subResult = await client.call('subscribe', subParams);
    expect(subResult).toEqual({});

    const updateWaiter: Waiter<Microblock> = waiter();
    client.onNotification.push(msg => {
      if (msg.method === 'microblock') {
        const microblockUpdate: Microblock = msg.params;
        updateWaiter.finish(microblockUpdate);
      }
    });

    const block = new TestBlockBuilder({ block_hash: '0x1212', index_block_hash: '0x4343' })
      .addTx()
      .build();
    await db.update(block);
    const microblocks = new TestMicroblockStreamBuilder()
      .addMicroblock({
        microblock_hash: '0xff01',
        microblock_parent_hash: '0x1212',
        parent_index_block_hash: '0x4343',
      })
      .addTx({ tx_id: '0xf6f6' })
      .build();
    await db.updateMicroblocks(microblocks);

    const result = await updateWaiter;
    try {
      expect(result.microblock_hash).toEqual('0xff01');
      expect(result.microblock_parent_hash).toEqual('0x1212');
      expect(result.txs[0]).toEqual('0xf6f6');
    } finally {
      socket.terminate();
    }
  });

  test('websocket rpc - address tx subscription updates', async () => {
    const wsAddress = `ws://${apiServer.address}/extended/v1/ws`;
    const socket = new WebSocket(wsAddress);
    const client = new RpcWebSocketClient();
    const addr = 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6';
    const subParams: RpcAddressTxSubscriptionParams = {
      event: 'address_tx_update',
      address: addr,
    };

    try {
      await once(socket, 'open');
      client.changeSocket(socket);
      client.listenMessages();
      const result = await client.call('subscribe', subParams);
      expect(result).toEqual({ address: addr });

      let updateIndex = 0;
      const addrTxUpdates: Waiter<RpcAddressTxNotificationParams>[] = [waiter(), waiter()];
      client.onNotification.push(msg => {
        if (msg.method === 'address_tx_update') {
          const txUpdate: RpcAddressTxNotificationParams = msg.params;
          addrTxUpdates[updateIndex++]?.finish(txUpdate);
        } else {
          fail(msg.method);
        }
      });

      const block = new TestBlockBuilder({
        block_height: 1,
        block_hash: '0x01',
        index_block_hash: '0x01',
      })
        .addTx({
          tx_id: '0x8912000000000000000000000000000000000000000000000000000000000000',
          sender_address: addr,
          type_id: DbTxTypeId.TokenTransfer,
          status: DbTxStatus.Success,
        })
        .addTxStxEvent({ sender: addr })
        .build();
      await db.update(block);
      const txUpdate1 = await addrTxUpdates[0];
      expect(txUpdate1).toEqual({
        address: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6',
        tx_id: '0x8912000000000000000000000000000000000000000000000000000000000000',
        tx_status: 'success',
        tx_type: 'token_transfer',
      });

      const microblock = new TestMicroblockStreamBuilder()
        .addMicroblock({
          microblock_hash: '0x11',
          parent_index_block_hash: '0x01',
        })
        .addTx({
          tx_id: '0x8913',
          sender_address: addr,
          token_transfer_amount: 150n,
          fee_rate: 50n,
          type_id: DbTxTypeId.TokenTransfer,
        })
        .addTxStxEvent({ sender: addr, amount: 150n })
        .build();
      await db.updateMicroblocks(microblock);
      const txUpdate2 = await addrTxUpdates[1];
      expect(txUpdate2).toEqual({
        address: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6',
        tx_id: '0x8913',
        tx_status: 'success',
        tx_type: 'token_transfer',
      });
    } finally {
      await client.call('unsubscribe', subParams);
      socket.terminate();
    }
  });

  test('websocket rpc - address balance subscription updates', async () => {
    const addr = apiServer.address;
    const wsAddress = `ws://${addr}/extended/v1/ws`;
    const socket = new WebSocket(wsAddress);

    try {
      await once(socket, 'open');
      const client = new RpcWebSocketClient();
      const addr2 = 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6';

      client.changeSocket(socket);
      client.listenMessages();
      const subParams1: RpcAddressBalanceSubscriptionParams = {
        event: 'address_balance_update',
        address: addr2,
      };
      const result = await client.call('subscribe', subParams1);
      expect(result).toEqual({ address: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6' });

      // watch for update to this tx
      let updateIndex = 0;
      const balanceUpdates: Waiter<RpcAddressBalanceNotificationParams>[] = [
        waiter(),
        waiter(),
        waiter(),
      ];
      client.onNotification.push(msg => {
        if (msg.method === 'address_balance_update') {
          const txUpdate: RpcAddressBalanceNotificationParams = msg.params;
          balanceUpdates[updateIndex++]?.finish(txUpdate);
        }
      });

      const block = new TestBlockBuilder()
        .addTx({
          token_transfer_recipient_address: addr2,
          token_transfer_amount: 100n,
        })
        .addTxStxEvent({ recipient: addr2, amount: 100n })
        .build();
      await db.update(block);

      // check for balance update notification
      const txUpdate1 = await balanceUpdates[0];
      expect(txUpdate1).toEqual({
        address: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6',
        balance: '100',
      });

      const unsubscribeResult = await client.call('unsubscribe', subParams1);
      expect(unsubscribeResult).toEqual({ address: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6' });
    } finally {
      socket.terminate();
    }
  });

  test('websocket rpc client lib', async () => {
    const addr = apiServer.address;
    const wsAddress = `ws://${addr}/extended/v1/ws`;
    const client = await connectWebSocketClient(wsAddress);
    try {
      const addrTxUpdates: Waiter<RpcAddressTxNotificationParams> = waiter();
      const subscription = await client.subscribeAddressTransactions(
        'ST3GQB6WGCWKDNFNPSQRV8DY93JN06XPZ2ZE9EVMA',
        event => addrTxUpdates.finish(event)
      );

      const block = new TestBlockBuilder()
        .addTx({
          tx_id: '0x8912000000000000000000000000000000000000000000000000000000000000',
          sender_address: 'ST3GQB6WGCWKDNFNPSQRV8DY93JN06XPZ2ZE9EVMA',
          type_id: DbTxTypeId.TokenTransfer,
          status: DbTxStatus.Success,
        })
        .addTxStxEvent({ sender: addr })
        .build();
      await db.update(block);

      // check for tx update notification
      const txUpdate1 = await addrTxUpdates;
      expect(txUpdate1).toEqual({
        address: 'ST3GQB6WGCWKDNFNPSQRV8DY93JN06XPZ2ZE9EVMA',
        tx_id: '0x8912000000000000000000000000000000000000000000000000000000000000',
        tx_status: 'success',
        tx_type: 'token_transfer',
      });
      await subscription.unsubscribe();
    } finally {
      client.webSocket.close();
    }
  });

  afterEach(async () => {
    await apiServer.terminate();
    dbClient.release();
    await db?.close();
    await runMigrations(undefined, 'down');
  });
});
Example #22
Source File: tcp.spec.ts    From hoprnet with GNU General Public License v3.0 4 votes vote down vote up
describe('test TCP connection', function () {
  it('should test TCPConnection against Node.js APIs', async function () {
    const msgReceived = defer<void>()

    const testMessage = new TextEncoder().encode('test')
    const testMessageReply = new TextEncoder().encode('reply')

    const peerId = createPeerId()

    const server = createServer((socket: Socket) => {
      socket.on('data', (data: Uint8Array) => {
        assert(u8aEquals(data, testMessage))
        socket.write(testMessageReply)

        msgReceived.resolve()
      })
    })

    await waitUntilListening<undefined | number>(server, undefined)

    const conn = await TCPConnection.create(
      new Multiaddr(`/ip4/127.0.0.1/tcp/${(server.address() as AddressInfo).port}`),
      peerId
    )

    await conn.sink(
      (async function* () {
        yield testMessage
      })()
    )

    for await (const msg of conn.source) {
      assert(u8aEquals(msg.slice(), testMessageReply))
    }

    await msgReceived.promise

    conn.close()

    await once(conn.conn as EventEmitter, 'close')

    assert(conn.conn.destroyed)

    assert(
      conn.timeline.close != undefined &&
        conn.timeline.close <= Date.now() &&
        conn.timeline.open <= conn.timeline.close,
      `TCPConnection must populate timeline object`
    )

    await stopNode(server)
  })

  it('trigger a socket close timeout', async function () {
    this.timeout(SOCKET_CLOSE_TIMEOUT + 2e3)

    const testMessage = new TextEncoder().encode('test')

    const server = createServer()

    server.on('close', console.log)
    server.on('error', console.log)
    const sockets: Socket[] = []
    server.on('connection', sockets.push.bind(sockets))

    await waitUntilListening(server, undefined)

    const peerId = createPeerId()
    const conn = await TCPConnection.create(
      new Multiaddr(`/ip4/127.0.0.1/tcp/${(server.address() as AddressInfo).port}`),
      peerId
    )

    await conn.sink(
      (async function* () {
        yield testMessage
      })()
    )

    const start = Date.now()
    const closePromise = once(conn.conn, 'close')

    // Overwrite end method to mimic half-open stream
    Object.assign(conn.conn, {
      end: () => {}
    })

    // @dev produces a half-open socket on the other side
    conn.close()

    await closePromise

    // Destroy half-open sockets.
    for (const socket of sockets) {
      socket.destroy()
    }

    await stopNode(server)

    assert(Date.now() - start >= SOCKET_CLOSE_TIMEOUT)

    assert(
      conn.timeline.close != undefined &&
        conn.timeline.close <= Date.now() &&
        conn.timeline.open <= conn.timeline.close,
      `TCPConnection must populate timeline object`
    )
  })

  it('tcp socket timeout and error cases', async function () {
    const INVALID_PORT = 54221
    const peerId = createPeerId()

    await assert.rejects(
      async () => {
        await TCPConnection.create(new Multiaddr(`/ip4/127.0.0.1/tcp/${INVALID_PORT}`), peerId)
      },
      {
        name: 'Error',
        code: 'ECONNREFUSED'
      }
    )
  })

  it('use abortController to abort streams', async function () {
    const msgReceived = defer<void>()

    const testMessage = new TextEncoder().encode('test')
    const testMessageReply = new TextEncoder().encode('reply')

    const peerId = createPeerId()

    const server = createServer((socket: Socket) => {
      socket.on('data', (data: Uint8Array) => {
        assert(u8aEquals(data, testMessage))
        socket.write(testMessageReply)

        msgReceived.resolve()
      })
    })

    await waitUntilListening<undefined | number>(server, undefined)

    const abort = new AbortController()

    const conn = await TCPConnection.create(
      new Multiaddr(`/ip4/127.0.0.1/tcp/${(server.address() as AddressInfo).port}`),
      peerId,
      {
        signal: abort.signal
      }
    )

    await assert.doesNotReject(
      async () =>
        await conn.sink(
          (async function* () {
            abort.abort()
            yield testMessage
          })()
        )
    )

    await stopNode(server)
  })
})
Example #23
Source File: connection.spec.ts    From hoprnet with GNU General Public License v3.0 4 votes vote down vote up
describe('relay connection', function () {
  const Alice: PeerId = createPeerId()
  const Relay: PeerId = createPeerId()
  const Bob: PeerId = createPeerId()

  it('ping message', async function () {
    const [AliceRelay, RelayAlice] = DuplexPair<StreamType>()

    new RelayConnection({
      stream: AliceRelay,
      self: Alice,
      relay: Relay,
      counterparty: Bob,
      onReconnect: async () => {}
    })

    const relayShaker = handshake(RelayAlice)

    relayShaker.write(Uint8Array.of(RelayPrefix.STATUS_MESSAGE, StatusMessages.PING))
    const msg = await relayShaker.read()
    const expectedMsg = Uint8Array.of(RelayPrefix.STATUS_MESSAGE, StatusMessages.PONG)

    assert(u8aEquals(msg.slice(), expectedMsg))
  })

  it('forward payload', async function () {
    const [AliceRelay, RelayAlice] = DuplexPair<StreamType>()

    const alice = new RelayConnection({
      stream: AliceRelay,
      self: Alice,
      relay: Relay,
      counterparty: Bob
    })

    const relayShaker = handshake(RelayAlice)

    const aliceShaker = handshake({
      source: alice.source,
      sink: alice.sink
    })

    const AMOUNT = 5
    for (let i = 0; i < AMOUNT; i++) {
      const relayHello = new TextEncoder().encode('Hello from Relay')
      relayShaker.write(Uint8Array.from([RelayPrefix.PAYLOAD, ...relayHello]))

      assert(u8aEquals((await aliceShaker.read()).slice(), relayHello))

      const aliceHello = new TextEncoder().encode('Hello from Alice')
      aliceShaker.write(aliceHello)

      assert(u8aEquals((await relayShaker.read()).slice(), Uint8Array.from([RelayPrefix.PAYLOAD, ...aliceHello])))
    }
  })

  it('stop a relayed connection from the relay', async function () {
    const [AliceRelay, RelayAlice] = DuplexPair<StreamType>()

    const alice = new RelayConnection({
      stream: AliceRelay,
      self: Alice,
      relay: Relay,
      counterparty: Bob
    })

    const relayShaker = handshake(RelayAlice)

    relayShaker.write(Uint8Array.of(RelayPrefix.CONNECTION_STATUS, ConnectionStatusMessages.STOP))

    relayShaker.rest()

    for await (const _msg of alice.source) {
      assert.fail(`Stream should be closed`)
    }

    for await (const _msg of relayShaker.stream.source as AsyncIterable<StreamType>) {
      assert.fail(`Stream should be closed`)
    }

    assert(alice.destroyed, `Stream must be destroyed`)

    assert(
      alice.timeline.close != undefined && Date.now() >= alice.timeline.close,
      `Timeline object must have been populated`
    )
  })

  it('stop a relayed connection from the client', async function () {
    const [AliceRelay, RelayAlice] = DuplexPair<StreamType>()

    const alice = new RelayConnection({
      stream: AliceRelay,
      self: Alice,
      relay: Relay,
      counterparty: Bob
    })

    const relayShaker = handshake(RelayAlice)

    alice.close()

    assert(
      u8aEquals(
        (await relayShaker.read()).slice(),
        Uint8Array.of(RelayPrefix.CONNECTION_STATUS, ConnectionStatusMessages.STOP)
      )
    )

    relayShaker.rest()

    for await (const _msg of relayShaker.stream.source as AsyncIterable<StreamType>) {
      assert.fail(`Stream must have ended`)
    }

    for await (const _msg of alice.source) {
      assert.fail(`Stream must have ended`)
    }

    assert(alice.destroyed, `Stream must be destroyed`)

    assert(
      alice.timeline.close != undefined && Date.now() >= alice.timeline.close,
      `Timeline object must have been populated`
    )
  })

  it('reconnect before using stream and use new stream', async function () {
    const [AliceRelay, RelayAlice] = DuplexPair<StreamType>()

    let aliceAfterReconnect: RelayConnection | undefined

    const alice = new RelayConnection({
      stream: AliceRelay,
      self: Alice,
      relay: Relay,
      counterparty: Bob,
      onReconnect: async (newStream: RelayConnection) => {
        aliceAfterReconnect = newStream
      }
    })

    const aliceShakerBeforeReconnect = handshake({
      source: alice.source,
      sink: alice.sink
    })

    const relayShaker = handshake(RelayAlice)

    // try to read something
    aliceShakerBeforeReconnect.read()

    relayShaker.write(Uint8Array.of(RelayPrefix.CONNECTION_STATUS, ConnectionStatusMessages.RESTART))

    await once(alice, 'restart')

    aliceShakerBeforeReconnect.write(new TextEncoder().encode('Hello from Alice before reconnect'))

    assert(aliceAfterReconnect != undefined)

    const aliceShaker = handshake({
      sink: aliceAfterReconnect.sink,
      source: aliceAfterReconnect.source
    })

    const relayHelloAfterReconnect = new TextEncoder().encode('Hello after reconnect!')
    relayShaker.write(Uint8Array.from([RelayPrefix.PAYLOAD, ...relayHelloAfterReconnect]))

    assert(u8aEquals((await aliceShaker.read()).slice(), relayHelloAfterReconnect))

    const aliceHelloAfterReconnect = new TextEncoder().encode('Hello from Alice after reconnect!')

    aliceShaker.write(aliceHelloAfterReconnect)
    assert(
      u8aEquals((await relayShaker.read()).slice(), Uint8Array.from([RelayPrefix.PAYLOAD, ...aliceHelloAfterReconnect]))
    )
  })

  it('reconnect before using stream and use new stream', async function () {
    const [AliceRelay, RelayAlice] = DuplexPair<StreamType>()

    let aliceAfterReconnect: RelayConnection | undefined

    const alice = new RelayConnection({
      stream: AliceRelay,
      self: Alice,
      relay: Relay,
      counterparty: Bob,
      onReconnect: async (newStream: RelayConnection) => {
        aliceAfterReconnect = newStream
      }
    })

    const aliceShakerBeforeReconnect = handshake({
      source: alice.source,
      sink: alice.sink
    })

    const relayShaker = handshake(RelayAlice)

    let aliceHelloBeforeReconnect = new TextEncoder().encode(`Hello from Alice before reconnecting`)
    aliceShakerBeforeReconnect.write(aliceHelloBeforeReconnect)

    assert(
      u8aEquals(
        (await relayShaker.read()).slice(),
        Uint8Array.from([RelayPrefix.PAYLOAD, ...aliceHelloBeforeReconnect])
      )
    )

    let relayHelloBeforeReconnect = new TextEncoder().encode(`Hello from relay before reconnecting`)
    relayShaker.write(Uint8Array.from([RelayPrefix.PAYLOAD, ...relayHelloBeforeReconnect]))

    assert(u8aEquals((await aliceShakerBeforeReconnect.read()).slice(), relayHelloBeforeReconnect))

    const ATTEMPTS = 5
    for (let i = 0; i < ATTEMPTS; i++) {
      relayShaker.write(Uint8Array.of(RelayPrefix.CONNECTION_STATUS, ConnectionStatusMessages.RESTART))

      await once(alice, 'restart')

      aliceShakerBeforeReconnect.write(new TextEncoder().encode('Hello from Alice before reconnect'))

      assert(aliceAfterReconnect != undefined)

      const aliceShaker = handshake({
        sink: aliceAfterReconnect.sink,
        source: aliceAfterReconnect.source
      })

      const relayHelloAfterReconnect = new TextEncoder().encode('Hello after reconnect!')
      relayShaker.write(Uint8Array.from([RelayPrefix.PAYLOAD, ...relayHelloAfterReconnect]))

      assert(u8aEquals((await aliceShaker.read()).slice(), relayHelloAfterReconnect))

      const aliceHelloAfterReconnect = new TextEncoder().encode('Hello from Alice after reconnect!')

      aliceShaker.write(aliceHelloAfterReconnect)
      assert(
        u8aEquals(
          (await relayShaker.read()).slice(),
          Uint8Array.from([RelayPrefix.PAYLOAD, ...aliceHelloAfterReconnect])
        )
      )
    }
  })

  it('forward and prefix WebRTC messages', async function () {
    class WebRTC extends EventEmitter {
      signal(args: any) {
        this.emit('incoming msg', args)
      }
    }

    const webRTC = new WebRTC()

    const [AliceRelay, RelayAlice] = DuplexPair<StreamType>()

    new RelayConnection({
      stream: AliceRelay,
      self: Alice,
      relay: Relay,
      counterparty: Bob,
      webRTC: {
        channel: webRTC as any,
        upgradeInbound: (): any => {}
      }
    })

    const relayShaker = handshake(RelayAlice)

    const webRTCHello = 'WebRTC Hello'

    relayShaker.write(
      Uint8Array.from([
        RelayPrefix.WEBRTC_SIGNALLING,
        ...new TextEncoder().encode(
          JSON.stringify({
            message: webRTCHello
          })
        )
      ])
    )

    const result = await once(webRTC, 'incoming msg')
    assert(result != undefined && result.length > 0 && result[0]?.message === webRTCHello)

    const webRTCResponse = 'webRTC hello back'
    webRTC.emit('signal', {
      message: webRTCResponse
    })

    assert(JSON.parse(new TextDecoder().decode((await relayShaker.read()).slice(1))).message === webRTCResponse)
  })

  it('forward and prefix WebRTC messages after reconnect', async function () {
    class WebRTC extends EventEmitter {
      signal(args: any) {
        this.emit('incoming msg', args)
      }
    }

    const webRTC = new WebRTC()

    const [AliceRelay, RelayAlice] = DuplexPair<StreamType>()

    let webRTCAfterReconnect: WebRTC | undefined

    const alice = new RelayConnection({
      stream: AliceRelay,
      self: Alice,
      relay: Relay,
      counterparty: Bob,
      webRTC: {
        channel: webRTC as any,
        upgradeInbound: (): any => {
          webRTCAfterReconnect = new WebRTC()
          return webRTCAfterReconnect
        }
      }
    })

    const relayShaker = handshake(RelayAlice)

    const webRTCHello = 'WebRTC Hello'

    relayShaker.write(
      Uint8Array.from([
        RelayPrefix.WEBRTC_SIGNALLING,
        ...new TextEncoder().encode(
          JSON.stringify({
            message: webRTCHello
          })
        )
      ])
    )

    const result = await once(webRTC, 'incoming msg')
    assert(result != undefined && result.length > 0 && result[0]?.message === webRTCHello)

    const webRTCResponse = 'webRTC hello back'
    webRTC.emit('signal', {
      message: webRTCResponse
    })

    assert(JSON.parse(new TextDecoder().decode((await relayShaker.read()).slice(1))).message === webRTCResponse)

    const ATTEMPTS = 5
    for (let i = 0; i < ATTEMPTS; i++) {
      relayShaker.write(Uint8Array.of(RelayPrefix.CONNECTION_STATUS, ConnectionStatusMessages.RESTART))

      await once(alice, 'restart')

      webRTC.emit('signal', {
        message: webRTCResponse
      })

      const correctWebRTCResponseAfterReconnect = 'webRTC hello back after response'

      assert(webRTCAfterReconnect != undefined)

      // Emitting unnecessary event that must not come through
      webRTCAfterReconnect.emit('signal', {
        message: correctWebRTCResponseAfterReconnect
      })

      assert(
        JSON.parse(new TextDecoder().decode((await relayShaker.read()).slice(1))).message ===
          correctWebRTCResponseAfterReconnect
      )
    }
  })
})
Example #24
Source File: submit.ts    From Corsace with MIT License 4 votes vote down vote up
async function command (m: Message) {
    if (!(await mappoolFunctions.privilegeChecks(m, true, false, true)))
        return;

    const waiting = await m.channel.send("Submitting...");
    let message: Message | undefined = undefined;
    let success = false;
    try {
        const { pool, slot, round, link, diffName } = await mappoolFunctions.parseParams(m);

        if (link === "") {
            message = await m.channel.send("No attachment is provided");
            return;
        }
        if (round === "") {
            message = await m.channel.send("No round is provided");
            return;
        }

        // Obtain beatmap data
        let diff = diffName.replace(/_/g, " ");
        let artist = "";
        let title = "";
        let length = "";
        let bpm = "";
        let sr = "";
        let cs = 0;
        let ar = 0;
        let od = 0;
        let hp = 0;
        const { data } = await Axios.get(link, { responseType: "stream" });
        const zip = data.pipe(Parse({ forceStream: true }));
        const osuParser = new osu.parser();
        for await (const _entry of zip) {
            const entry = _entry as Entry;
    
            if (entry.type === "File" && entry.props.path.endsWith(".osu")) {
                const writableBeatmapParser = new BeatmapParser(osuParser);
                entry.pipe(writableBeatmapParser);
                await once(writableBeatmapParser, "finish");
                
                if (diff !== "" && osuParser.map.version.toLowerCase() !== diff.toLowerCase())
                    continue;

                const beatmap = osuParser.map;
                artist = beatmap.artist;
                title = beatmap.title;
                diff = beatmap.version;
                cs = beatmap.cs;
                ar = beatmap.ar!;
                od = beatmap.od;
                hp = beatmap.hp;

                // Obtaining length
                const lengthMs = beatmap.objects[beatmap.objects.length - 1].time - beatmap.objects[0].time;
                const lengthSec = lengthMs / 1000;
                const lengthMin = Math.round(lengthSec / 60);
                const lengthMod = lengthSec % 60;
                length = `${lengthMin}:${lengthMod >= 10 ? lengthMod.toFixed(0) : "0" + lengthMod.toFixed(0)}`;

                // Obtaining bpm
                const timingPoints = beatmap.timing_points.filter(line => line.change === true).map(line => {
                    return 60000 / line.ms_per_beat;
                }).sort((a,b) => a - b);
                if (timingPoints.length === 1 || timingPoints[timingPoints.length - 1].toFixed() === timingPoints[0].toFixed())
                    bpm = timingPoints[0] % 1 !== 0 ? timingPoints[0].toFixed(3) : timingPoints[0].toFixed();
                else
                    bpm = `${timingPoints[0].toFixed()}-${timingPoints[timingPoints.length - 1].toFixed()}`;

                sr = new osu.std_diff().calc({map: beatmap}).total.toFixed(2);
                break;
            }

            entry.autodrain();
        }

        if (artist === "") {
            message = await m.channel.send(`Could not find **${diffName !== "" ? `[${diff}]` : "a single difficulty(?)"}** in your osz`);
            return;
        }


        // Get pool data and iterate thru
        const rows = await getPoolData(pool, round.toUpperCase());
        if (!rows) {
            message = await m.channel.send(`Could not find round **${round.toUpperCase()}** in the **${pool === "openMappool" ? "Corsace Open" : "Corsace Closed"}** pool`);
            return;
        }
        for (let i = 0; i < rows.length; i++) {
            const row = rows[i];
            if (row.some(val => val === m.author.id)) {
                if (
                    (slot === "") || // no slot given
                    (slot !== "" && slot.toLowerCase() === row[0].toLowerCase()) // slot given
                ) {
                    await Promise.all([
                        updatePoolRow(pool, `'${round}'!C${i + 2}:N${i + 2}`, [ artist, title, diff, length, bpm, sr, cs, ar, od, hp, "", "" ]),
                        updatePoolRow(pool, `'${round}'!O${i + 2}`, [ link ]),
                        appendToHistory(pool, [ (new Date).toUTCString(), `${round.toUpperCase()}${slot ? slot.toUpperCase() : row[0].toUpperCase()}`, artist, title, m.member?.nickname ?? m.author.username, link ]),
                    ]);
                    message = await m.channel.send(`${m.author.toString()} Submitted your map for the slot **${row[0].toUpperCase()}** in **${round.toUpperCase()}** on **${pool === "openMappool" ? "Corsace Open" : "Corsace Closed"}**\n${m.attachments.first() ? "**DO NOT DELETE YOUR MESSAGE, YOUR LINK IS THE ATTACHMENT.**" : ""}`);
                    success = true;
                    return;
                }
            }
        }
        message = await m.channel.send(`Could not find ${slot !== "" ? `the slot **${slot.toUpperCase()}**` : "a slot"} in **${round.toUpperCase()}** on **${pool === "openMappool" ? "Corsace Open" : "Corsace Closed"}** which you were also assigned to.`);
    } finally {
        waiting.delete();
        if (message)
            setTimeout(() => message!.delete(), 5000);
        if (!success)
            setTimeout(() => m.delete(), 5000);
        else
            m.react("✅");
    }
}