diff --git a/lib/cli/commands/ListSwaps.ts b/lib/cli/commands/ListSwaps.ts new file mode 100644 index 00000000..c33aaae5 --- /dev/null +++ b/lib/cli/commands/ListSwaps.ts @@ -0,0 +1,34 @@ +import { Arguments } from 'yargs'; +import { ListSwapsRequest } from '../../proto/boltzrpc_pb'; +import { ApiType, BuilderTypes } from '../BuilderComponents'; +import { callback, loadBoltzClient } from '../Command'; + +export const command = 'listswaps [status] [limit]'; + +export const describe = 'lists swaps'; + +export const builder = { + status: { + type: 'string', + describe: 'status of the swaps to list', + }, + limit: { + type: 'number', + describe: 'limit of the swaps to list per type', + default: 100, + }, +}; + +export const handler = ( + argv: Arguments & ApiType>, +): void => { + const request = new ListSwapsRequest(); + + request.setLimit(argv.limit); + + if (argv.status) { + request.setStatus(argv.status); + } + + loadBoltzClient(argv).listSwaps(request, callback()); +}; diff --git a/lib/db/repositories/ChainSwapRepository.ts b/lib/db/repositories/ChainSwapRepository.ts index 03c2d596..f61ca4a8 100644 --- a/lib/db/repositories/ChainSwapRepository.ts +++ b/lib/db/repositories/ChainSwapRepository.ts @@ -1,4 +1,4 @@ -import { Op, WhereOptions } from 'sequelize'; +import { Op, Order, WhereOptions } from 'sequelize'; import { getHexString, getSendingReceivingCurrency, @@ -102,13 +102,19 @@ class ChainSwapRepository { public static getChainSwaps = async ( options?: WhereOptions, + order?: Order, + limit?: number, ): Promise => { - const chainSwaps = await ChainSwap.findAll({ where: options }); + const chainSwaps = await ChainSwap.findAll({ + limit, + order, + where: options, + }); return Promise.all(chainSwaps.map(this.fetchChainSwapData)); }; /** - * Get a chain swap to with **both** options applies + * Get a chain swap to with **both** options apply */ public static getChainSwapByData = async ( dataOptions: WhereOptions, diff --git a/lib/db/repositories/ReverseSwapRepository.ts b/lib/db/repositories/ReverseSwapRepository.ts index 132dfef8..5963c3da 100644 --- a/lib/db/repositories/ReverseSwapRepository.ts +++ b/lib/db/repositories/ReverseSwapRepository.ts @@ -1,12 +1,16 @@ -import { Op, WhereOptions } from 'sequelize'; +import { Op, Order, WhereOptions } from 'sequelize'; import { SwapUpdateEvent } from '../../consts/Enums'; import ReverseSwap, { ReverseSwapType } from '../models/ReverseSwap'; class ReverseSwapRepository { public static getReverseSwaps = ( options?: WhereOptions, + order?: Order, + limit?: number, ): Promise => { return ReverseSwap.findAll({ + limit, + order, where: options, }); }; diff --git a/lib/db/repositories/SwapRepository.ts b/lib/db/repositories/SwapRepository.ts index 4d761fff..764a31c8 100644 --- a/lib/db/repositories/SwapRepository.ts +++ b/lib/db/repositories/SwapRepository.ts @@ -1,10 +1,16 @@ -import { Op, WhereOptions } from 'sequelize'; +import { Op, Order, WhereOptions } from 'sequelize'; import { SwapUpdateEvent } from '../../consts/Enums'; import Swap, { SwapType } from '../models/Swap'; class SwapRepository { - public static getSwaps = (options?: WhereOptions): Promise => { + public static getSwaps = ( + options?: WhereOptions, + order?: Order, + limit?: number, + ): Promise => { return Swap.findAll({ + limit, + order, where: options, }); }; diff --git a/lib/grpc/GrpcServer.ts b/lib/grpc/GrpcServer.ts index fa638e87..09104cfd 100644 --- a/lib/grpc/GrpcServer.ts +++ b/lib/grpc/GrpcServer.ts @@ -5,6 +5,7 @@ import { BoltzService } from '../proto/boltzrpc_grpc_pb'; import { CertificatePrefix, getCertificate } from './Certificates'; import Errors from './Errors'; import GrpcService from './GrpcService'; +import { loggingInterceptor } from './Interceptors'; class GrpcServer { public static readonly certificateSubject = 'boltz'; @@ -16,7 +17,9 @@ class GrpcServer { private config: GrpcConfig, grpcService: GrpcService, ) { - this.server = new Server(); + this.server = new Server({ + interceptors: [loggingInterceptor(this.logger)], + }); this.server.addService(BoltzService, { getInfo: grpcService.getInfo, @@ -29,6 +32,7 @@ class GrpcServer { updateTimeoutBlockDelta: grpcService.updateTimeoutBlockDelta, addReferral: grpcService.addReferral, sweepSwaps: grpcService.sweepSwaps, + listSwaps: grpcService.listSwaps, rescan: grpcService.rescan, setSwapStatus: grpcService.setSwapStatus, devHeapDump: grpcService.devHeapDump, diff --git a/lib/grpc/GrpcService.ts b/lib/grpc/GrpcService.ts index ac4e7eab..44ad2d4d 100644 --- a/lib/grpc/GrpcService.ts +++ b/lib/grpc/GrpcService.ts @@ -289,6 +289,27 @@ class GrpcService { }); }; + public listSwaps: handleUnaryCall< + boltzrpc.ListSwapsRequest, + boltzrpc.ListSwapsResponse + > = async (call, callback) => { + await this.handleCallback(call, callback, async () => { + const { status, limit } = call.request.toObject(); + + const swaps = await this.service.listSwaps( + status !== undefined && status !== '' ? status : undefined, + limit, + ); + + const response = new boltzrpc.ListSwapsResponse(); + response.setChainSwapsList(swaps.chain); + response.setReverseSwapsList(swaps.reverse); + response.setSubmarineSwapsList(swaps.submarine); + + return response; + }); + }; + public rescan: handleUnaryCall< boltzrpc.RescanRequest, boltzrpc.RescanResponse diff --git a/lib/grpc/Interceptors.ts b/lib/grpc/Interceptors.ts new file mode 100644 index 00000000..9f96f649 --- /dev/null +++ b/lib/grpc/Interceptors.ts @@ -0,0 +1,12 @@ +import { ServerInterceptingCall, ServerInterceptor } from '@grpc/grpc-js'; +import Logger from '../Logger'; + +export const loggingInterceptor = + (logger: Logger): ServerInterceptor => + (methodDescriptor, call) => + new ServerInterceptingCall(call, { + start: (next) => { + logger.debug(`Got gRPC call ${methodDescriptor.path}`); + return next(); + }, + }); diff --git a/lib/notifications/CommandHandler.ts b/lib/notifications/CommandHandler.ts index b60dd6c0..15d6e01e 100644 --- a/lib/notifications/CommandHandler.ts +++ b/lib/notifications/CommandHandler.ts @@ -36,6 +36,7 @@ enum Command { GetFees = 'getfees', SwapInfo = 'swapinfo', GetStats = 'getstats', + ListSwaps = 'listswaps', GetBalance = 'getbalance', LockedFunds = 'lockedfunds', PendingSwaps = 'pendingswaps', @@ -119,6 +120,13 @@ class CommandHandler { ], }, ], + [ + Command.ListSwaps, + { + executor: this.listSwaps, + description: 'lists swaps', + }, + ], [ Command.GetBalance, { @@ -197,7 +205,16 @@ class CommandHandler { this.logger.debug( `Executing ${this.notificationClient.serviceName} command: ${command} ${args.join(', ')}`, ); - await commandInfo.executor(args); + try { + await commandInfo.executor(args); + } catch (e) { + this.logger.warn( + `${this.notificationClient.serviceName} command failed: ${formatError(e)}`, + ); + await this.notificationClient.sendMessage( + `Command failed: ${formatError(e)}`, + ); + } } } }); @@ -364,6 +381,28 @@ class CommandHandler { ); }; + private listSwaps = async (args: string[]) => { + let status: string | undefined; + let limit: number = 100; + + if (args.length > 0) { + status = args[0]; + } + + if (args.length > 1) { + limit = Number(args[1]); + if (isNaN(limit)) { + throw 'invalid limit'; + } + + limit = Math.round(limit); + } + + await this.notificationClient.sendMessage( + `${codeBlock}${stringify(await this.service.listSwaps(status, limit))}${codeBlock}`, + ); + }; + private getBalance = async () => { const balances = (await this.service.getBalance()).toObject().balancesMap; diff --git a/lib/proto/boltzrpc_grpc_pb.d.ts b/lib/proto/boltzrpc_grpc_pb.d.ts index 2323717c..4d8b4209 100644 --- a/lib/proto/boltzrpc_grpc_pb.d.ts +++ b/lib/proto/boltzrpc_grpc_pb.d.ts @@ -21,6 +21,7 @@ interface IBoltzService extends grpc.ServiceDefinition; responseDeserialize: grpc.deserialize; } +interface IBoltzService_IListSwaps extends grpc.MethodDefinition { + path: "/boltzrpc.Boltz/ListSwaps"; + requestStream: false; + responseStream: false; + requestSerialize: grpc.serialize; + requestDeserialize: grpc.deserialize; + responseSerialize: grpc.serialize; + responseDeserialize: grpc.deserialize; +} interface IBoltzService_IRescan extends grpc.MethodDefinition { path: "/boltzrpc.Boltz/Rescan"; requestStream: false; @@ -177,6 +187,7 @@ export interface IBoltzServer extends grpc.UntypedServiceImplementation { getLockedFunds: grpc.handleUnaryCall; getPendingSweeps: grpc.handleUnaryCall; sweepSwaps: grpc.handleUnaryCall; + listSwaps: grpc.handleUnaryCall; rescan: grpc.handleUnaryCall; devHeapDump: grpc.handleUnaryCall; } @@ -221,6 +232,9 @@ export interface IBoltzClient { sweepSwaps(request: boltzrpc_pb.SweepSwapsRequest, callback: (error: grpc.ServiceError | null, response: boltzrpc_pb.SweepSwapsResponse) => void): grpc.ClientUnaryCall; sweepSwaps(request: boltzrpc_pb.SweepSwapsRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: boltzrpc_pb.SweepSwapsResponse) => void): grpc.ClientUnaryCall; sweepSwaps(request: boltzrpc_pb.SweepSwapsRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: boltzrpc_pb.SweepSwapsResponse) => void): grpc.ClientUnaryCall; + listSwaps(request: boltzrpc_pb.ListSwapsRequest, callback: (error: grpc.ServiceError | null, response: boltzrpc_pb.ListSwapsResponse) => void): grpc.ClientUnaryCall; + listSwaps(request: boltzrpc_pb.ListSwapsRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: boltzrpc_pb.ListSwapsResponse) => void): grpc.ClientUnaryCall; + listSwaps(request: boltzrpc_pb.ListSwapsRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: boltzrpc_pb.ListSwapsResponse) => void): grpc.ClientUnaryCall; rescan(request: boltzrpc_pb.RescanRequest, callback: (error: grpc.ServiceError | null, response: boltzrpc_pb.RescanResponse) => void): grpc.ClientUnaryCall; rescan(request: boltzrpc_pb.RescanRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: boltzrpc_pb.RescanResponse) => void): grpc.ClientUnaryCall; rescan(request: boltzrpc_pb.RescanRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: boltzrpc_pb.RescanResponse) => void): grpc.ClientUnaryCall; @@ -270,6 +284,9 @@ export class BoltzClient extends grpc.Client implements IBoltzClient { public sweepSwaps(request: boltzrpc_pb.SweepSwapsRequest, callback: (error: grpc.ServiceError | null, response: boltzrpc_pb.SweepSwapsResponse) => void): grpc.ClientUnaryCall; public sweepSwaps(request: boltzrpc_pb.SweepSwapsRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: boltzrpc_pb.SweepSwapsResponse) => void): grpc.ClientUnaryCall; public sweepSwaps(request: boltzrpc_pb.SweepSwapsRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: boltzrpc_pb.SweepSwapsResponse) => void): grpc.ClientUnaryCall; + public listSwaps(request: boltzrpc_pb.ListSwapsRequest, callback: (error: grpc.ServiceError | null, response: boltzrpc_pb.ListSwapsResponse) => void): grpc.ClientUnaryCall; + public listSwaps(request: boltzrpc_pb.ListSwapsRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: boltzrpc_pb.ListSwapsResponse) => void): grpc.ClientUnaryCall; + public listSwaps(request: boltzrpc_pb.ListSwapsRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: boltzrpc_pb.ListSwapsResponse) => void): grpc.ClientUnaryCall; public rescan(request: boltzrpc_pb.RescanRequest, callback: (error: grpc.ServiceError | null, response: boltzrpc_pb.RescanResponse) => void): grpc.ClientUnaryCall; public rescan(request: boltzrpc_pb.RescanRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: boltzrpc_pb.RescanResponse) => void): grpc.ClientUnaryCall; public rescan(request: boltzrpc_pb.RescanRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: boltzrpc_pb.RescanResponse) => void): grpc.ClientUnaryCall; diff --git a/lib/proto/boltzrpc_grpc_pb.js b/lib/proto/boltzrpc_grpc_pb.js index 4fc16f52..678519ab 100644 --- a/lib/proto/boltzrpc_grpc_pb.js +++ b/lib/proto/boltzrpc_grpc_pb.js @@ -202,6 +202,28 @@ function deserialize_boltzrpc_GetPendingSweepsResponse(buffer_arg) { return boltzrpc_pb.GetPendingSweepsResponse.deserializeBinary(new Uint8Array(buffer_arg)); } +function serialize_boltzrpc_ListSwapsRequest(arg) { + if (!(arg instanceof boltzrpc_pb.ListSwapsRequest)) { + throw new Error('Expected argument of type boltzrpc.ListSwapsRequest'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_boltzrpc_ListSwapsRequest(buffer_arg) { + return boltzrpc_pb.ListSwapsRequest.deserializeBinary(new Uint8Array(buffer_arg)); +} + +function serialize_boltzrpc_ListSwapsResponse(arg) { + if (!(arg instanceof boltzrpc_pb.ListSwapsResponse)) { + throw new Error('Expected argument of type boltzrpc.ListSwapsResponse'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_boltzrpc_ListSwapsResponse(buffer_arg) { + return boltzrpc_pb.ListSwapsResponse.deserializeBinary(new Uint8Array(buffer_arg)); +} + function serialize_boltzrpc_RescanRequest(arg) { if (!(arg instanceof boltzrpc_pb.RescanRequest)) { throw new Error('Expected argument of type boltzrpc.RescanRequest'); @@ -489,6 +511,17 @@ getPendingSweeps: { responseSerialize: serialize_boltzrpc_SweepSwapsResponse, responseDeserialize: deserialize_boltzrpc_SweepSwapsResponse, }, + listSwaps: { + path: '/boltzrpc.Boltz/ListSwaps', + requestStream: false, + responseStream: false, + requestType: boltzrpc_pb.ListSwapsRequest, + responseType: boltzrpc_pb.ListSwapsResponse, + requestSerialize: serialize_boltzrpc_ListSwapsRequest, + requestDeserialize: deserialize_boltzrpc_ListSwapsRequest, + responseSerialize: serialize_boltzrpc_ListSwapsResponse, + responseDeserialize: deserialize_boltzrpc_ListSwapsResponse, + }, rescan: { path: '/boltzrpc.Boltz/Rescan', requestStream: false, diff --git a/lib/proto/boltzrpc_pb.d.ts b/lib/proto/boltzrpc_pb.d.ts index 907d6f4c..206df66c 100644 --- a/lib/proto/boltzrpc_pb.d.ts +++ b/lib/proto/boltzrpc_pb.d.ts @@ -761,6 +761,67 @@ export namespace SweepSwapsResponse { } +export class ListSwapsRequest extends jspb.Message { + + hasStatus(): boolean; + clearStatus(): void; + getStatus(): string | undefined; + setStatus(value: string): ListSwapsRequest; + + hasLimit(): boolean; + clearLimit(): void; + getLimit(): number | undefined; + setLimit(value: number): ListSwapsRequest; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): ListSwapsRequest.AsObject; + static toObject(includeInstance: boolean, msg: ListSwapsRequest): ListSwapsRequest.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: ListSwapsRequest, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): ListSwapsRequest; + static deserializeBinaryFromReader(message: ListSwapsRequest, reader: jspb.BinaryReader): ListSwapsRequest; +} + +export namespace ListSwapsRequest { + export type AsObject = { + status?: string, + limit?: number, + } +} + +export class ListSwapsResponse extends jspb.Message { + clearSubmarineSwapsList(): void; + getSubmarineSwapsList(): Array; + setSubmarineSwapsList(value: Array): ListSwapsResponse; + addSubmarineSwaps(value: string, index?: number): string; + clearReverseSwapsList(): void; + getReverseSwapsList(): Array; + setReverseSwapsList(value: Array): ListSwapsResponse; + addReverseSwaps(value: string, index?: number): string; + clearChainSwapsList(): void; + getChainSwapsList(): Array; + setChainSwapsList(value: Array): ListSwapsResponse; + addChainSwaps(value: string, index?: number): string; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): ListSwapsResponse.AsObject; + static toObject(includeInstance: boolean, msg: ListSwapsResponse): ListSwapsResponse.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: ListSwapsResponse, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): ListSwapsResponse; + static deserializeBinaryFromReader(message: ListSwapsResponse, reader: jspb.BinaryReader): ListSwapsResponse; +} + +export namespace ListSwapsResponse { + export type AsObject = { + submarineSwapsList: Array, + reverseSwapsList: Array, + chainSwapsList: Array, + } +} + export class RescanRequest extends jspb.Message { getSymbol(): string; setSymbol(value: string): RescanRequest; diff --git a/lib/proto/boltzrpc_pb.js b/lib/proto/boltzrpc_pb.js index be561d23..80885fe7 100644 --- a/lib/proto/boltzrpc_pb.js +++ b/lib/proto/boltzrpc_pb.js @@ -46,6 +46,8 @@ goog.exportSymbol('proto.boltzrpc.GetPendingSweepsRequest', null, global); goog.exportSymbol('proto.boltzrpc.GetPendingSweepsResponse', null, global); goog.exportSymbol('proto.boltzrpc.LightningInfo', null, global); goog.exportSymbol('proto.boltzrpc.LightningInfo.Channels', null, global); +goog.exportSymbol('proto.boltzrpc.ListSwapsRequest', null, global); +goog.exportSymbol('proto.boltzrpc.ListSwapsResponse', null, global); goog.exportSymbol('proto.boltzrpc.LockedFund', null, global); goog.exportSymbol('proto.boltzrpc.LockedFunds', null, global); goog.exportSymbol('proto.boltzrpc.OutputType', null, global); @@ -675,6 +677,48 @@ if (goog.DEBUG && !COMPILED) { */ proto.boltzrpc.SweepSwapsResponse.ClaimedSwaps.displayName = 'proto.boltzrpc.SweepSwapsResponse.ClaimedSwaps'; } +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.boltzrpc.ListSwapsRequest = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.boltzrpc.ListSwapsRequest, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.boltzrpc.ListSwapsRequest.displayName = 'proto.boltzrpc.ListSwapsRequest'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.boltzrpc.ListSwapsResponse = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, proto.boltzrpc.ListSwapsResponse.repeatedFields_, null); +}; +goog.inherits(proto.boltzrpc.ListSwapsResponse, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.boltzrpc.ListSwapsResponse.displayName = 'proto.boltzrpc.ListSwapsResponse'; +} /** * Generated by JsPbCodeGenerator. * @param {Array=} opt_data Optional initial data array, typically from a @@ -6195,6 +6239,456 @@ proto.boltzrpc.SweepSwapsResponse.prototype.clearClaimedSymbolsMap = function() +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.boltzrpc.ListSwapsRequest.prototype.toObject = function(opt_includeInstance) { + return proto.boltzrpc.ListSwapsRequest.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.boltzrpc.ListSwapsRequest} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.boltzrpc.ListSwapsRequest.toObject = function(includeInstance, msg) { + var f, obj = { + status: jspb.Message.getFieldWithDefault(msg, 1, ""), + limit: jspb.Message.getFieldWithDefault(msg, 2, 0) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.boltzrpc.ListSwapsRequest} + */ +proto.boltzrpc.ListSwapsRequest.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.boltzrpc.ListSwapsRequest; + return proto.boltzrpc.ListSwapsRequest.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.boltzrpc.ListSwapsRequest} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.boltzrpc.ListSwapsRequest} + */ +proto.boltzrpc.ListSwapsRequest.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.setStatus(value); + break; + case 2: + var value = /** @type {number} */ (reader.readUint64()); + msg.setLimit(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.boltzrpc.ListSwapsRequest.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.boltzrpc.ListSwapsRequest.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.boltzrpc.ListSwapsRequest} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.boltzrpc.ListSwapsRequest.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = /** @type {string} */ (jspb.Message.getField(message, 1)); + if (f != null) { + writer.writeString( + 1, + f + ); + } + f = /** @type {number} */ (jspb.Message.getField(message, 2)); + if (f != null) { + writer.writeUint64( + 2, + f + ); + } +}; + + +/** + * optional string status = 1; + * @return {string} + */ +proto.boltzrpc.ListSwapsRequest.prototype.getStatus = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** + * @param {string} value + * @return {!proto.boltzrpc.ListSwapsRequest} returns this + */ +proto.boltzrpc.ListSwapsRequest.prototype.setStatus = function(value) { + return jspb.Message.setField(this, 1, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.boltzrpc.ListSwapsRequest} returns this + */ +proto.boltzrpc.ListSwapsRequest.prototype.clearStatus = function() { + return jspb.Message.setField(this, 1, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.boltzrpc.ListSwapsRequest.prototype.hasStatus = function() { + return jspb.Message.getField(this, 1) != null; +}; + + +/** + * optional uint64 limit = 2; + * @return {number} + */ +proto.boltzrpc.ListSwapsRequest.prototype.getLimit = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 2, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.boltzrpc.ListSwapsRequest} returns this + */ +proto.boltzrpc.ListSwapsRequest.prototype.setLimit = function(value) { + return jspb.Message.setField(this, 2, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.boltzrpc.ListSwapsRequest} returns this + */ +proto.boltzrpc.ListSwapsRequest.prototype.clearLimit = function() { + return jspb.Message.setField(this, 2, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.boltzrpc.ListSwapsRequest.prototype.hasLimit = function() { + return jspb.Message.getField(this, 2) != null; +}; + + + +/** + * List of repeated fields within this message type. + * @private {!Array} + * @const + */ +proto.boltzrpc.ListSwapsResponse.repeatedFields_ = [1,2,3]; + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.boltzrpc.ListSwapsResponse.prototype.toObject = function(opt_includeInstance) { + return proto.boltzrpc.ListSwapsResponse.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.boltzrpc.ListSwapsResponse} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.boltzrpc.ListSwapsResponse.toObject = function(includeInstance, msg) { + var f, obj = { + submarineSwapsList: (f = jspb.Message.getRepeatedField(msg, 1)) == null ? undefined : f, + reverseSwapsList: (f = jspb.Message.getRepeatedField(msg, 2)) == null ? undefined : f, + chainSwapsList: (f = jspb.Message.getRepeatedField(msg, 3)) == null ? undefined : f + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.boltzrpc.ListSwapsResponse} + */ +proto.boltzrpc.ListSwapsResponse.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.boltzrpc.ListSwapsResponse; + return proto.boltzrpc.ListSwapsResponse.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.boltzrpc.ListSwapsResponse} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.boltzrpc.ListSwapsResponse} + */ +proto.boltzrpc.ListSwapsResponse.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.addSubmarineSwaps(value); + break; + case 2: + var value = /** @type {string} */ (reader.readString()); + msg.addReverseSwaps(value); + break; + case 3: + var value = /** @type {string} */ (reader.readString()); + msg.addChainSwaps(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.boltzrpc.ListSwapsResponse.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.boltzrpc.ListSwapsResponse.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.boltzrpc.ListSwapsResponse} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.boltzrpc.ListSwapsResponse.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getSubmarineSwapsList(); + if (f.length > 0) { + writer.writeRepeatedString( + 1, + f + ); + } + f = message.getReverseSwapsList(); + if (f.length > 0) { + writer.writeRepeatedString( + 2, + f + ); + } + f = message.getChainSwapsList(); + if (f.length > 0) { + writer.writeRepeatedString( + 3, + f + ); + } +}; + + +/** + * repeated string submarine_swaps = 1; + * @return {!Array} + */ +proto.boltzrpc.ListSwapsResponse.prototype.getSubmarineSwapsList = function() { + return /** @type {!Array} */ (jspb.Message.getRepeatedField(this, 1)); +}; + + +/** + * @param {!Array} value + * @return {!proto.boltzrpc.ListSwapsResponse} returns this + */ +proto.boltzrpc.ListSwapsResponse.prototype.setSubmarineSwapsList = function(value) { + return jspb.Message.setField(this, 1, value || []); +}; + + +/** + * @param {string} value + * @param {number=} opt_index + * @return {!proto.boltzrpc.ListSwapsResponse} returns this + */ +proto.boltzrpc.ListSwapsResponse.prototype.addSubmarineSwaps = function(value, opt_index) { + return jspb.Message.addToRepeatedField(this, 1, value, opt_index); +}; + + +/** + * Clears the list making it empty but non-null. + * @return {!proto.boltzrpc.ListSwapsResponse} returns this + */ +proto.boltzrpc.ListSwapsResponse.prototype.clearSubmarineSwapsList = function() { + return this.setSubmarineSwapsList([]); +}; + + +/** + * repeated string reverse_swaps = 2; + * @return {!Array} + */ +proto.boltzrpc.ListSwapsResponse.prototype.getReverseSwapsList = function() { + return /** @type {!Array} */ (jspb.Message.getRepeatedField(this, 2)); +}; + + +/** + * @param {!Array} value + * @return {!proto.boltzrpc.ListSwapsResponse} returns this + */ +proto.boltzrpc.ListSwapsResponse.prototype.setReverseSwapsList = function(value) { + return jspb.Message.setField(this, 2, value || []); +}; + + +/** + * @param {string} value + * @param {number=} opt_index + * @return {!proto.boltzrpc.ListSwapsResponse} returns this + */ +proto.boltzrpc.ListSwapsResponse.prototype.addReverseSwaps = function(value, opt_index) { + return jspb.Message.addToRepeatedField(this, 2, value, opt_index); +}; + + +/** + * Clears the list making it empty but non-null. + * @return {!proto.boltzrpc.ListSwapsResponse} returns this + */ +proto.boltzrpc.ListSwapsResponse.prototype.clearReverseSwapsList = function() { + return this.setReverseSwapsList([]); +}; + + +/** + * repeated string chain_swaps = 3; + * @return {!Array} + */ +proto.boltzrpc.ListSwapsResponse.prototype.getChainSwapsList = function() { + return /** @type {!Array} */ (jspb.Message.getRepeatedField(this, 3)); +}; + + +/** + * @param {!Array} value + * @return {!proto.boltzrpc.ListSwapsResponse} returns this + */ +proto.boltzrpc.ListSwapsResponse.prototype.setChainSwapsList = function(value) { + return jspb.Message.setField(this, 3, value || []); +}; + + +/** + * @param {string} value + * @param {number=} opt_index + * @return {!proto.boltzrpc.ListSwapsResponse} returns this + */ +proto.boltzrpc.ListSwapsResponse.prototype.addChainSwaps = function(value, opt_index) { + return jspb.Message.addToRepeatedField(this, 3, value, opt_index); +}; + + +/** + * Clears the list making it empty but non-null. + * @return {!proto.boltzrpc.ListSwapsResponse} returns this + */ +proto.boltzrpc.ListSwapsResponse.prototype.clearChainSwapsList = function() { + return this.setChainSwapsList([]); +}; + + + + + if (jspb.Message.GENERATE_TO_OBJECT) { /** * Creates an object representation of this proto. diff --git a/lib/service/Service.ts b/lib/service/Service.ts index abe34f66..980f7265 100644 --- a/lib/service/Service.ts +++ b/lib/service/Service.ts @@ -1,6 +1,6 @@ import { OutputType, SwapTreeSerializer } from 'boltz-core'; import { Provider } from 'ethers'; -import { Op } from 'sequelize'; +import { Op, Order } from 'sequelize'; import { ConfigType } from '../Config'; import { parseTransaction } from '../Core'; import Logger from '../Logger'; @@ -363,6 +363,35 @@ class Service { return response; }; + public listSwaps = async ( + status?: string, + limit?: number, + ): Promise<{ + submarine: string[]; + reverse: string[]; + chain: string[]; + }> => { + const statusOptions = + status !== undefined + ? { + status, + } + : {}; + const order: Order = [['createdAt', 'DESC']]; + + const [submarine, reverse, chain] = await Promise.all([ + SwapRepository.getSwaps(statusOptions, order, limit), + ReverseSwapRepository.getReverseSwaps(statusOptions, order, limit), + ChainSwapRepository.getChainSwaps(statusOptions, order, limit), + ]); + + return { + submarine: submarine.map((s) => s.id), + reverse: reverse.map((s) => s.id), + chain: chain.map((s) => s.id), + }; + }; + public rescan = async ( symbol: string, startHeight: number, diff --git a/proto/boltzrpc.proto b/proto/boltzrpc.proto index 2ea27aa2..b2ecc869 100644 --- a/proto/boltzrpc.proto +++ b/proto/boltzrpc.proto @@ -38,6 +38,8 @@ service Boltz { rpc SweepSwaps (SweepSwapsRequest) returns (SweepSwapsResponse); + rpc ListSwaps (ListSwapsRequest) returns (ListSwapsResponse); + rpc Rescan (RescanRequest) returns (RescanResponse); rpc DevHeapDump (DevHeapDumpRequest) returns (DevHeapDumpResponse); @@ -201,6 +203,17 @@ message SweepSwapsResponse { map claimed_symbols = 1; } +message ListSwapsRequest { + optional string status = 1; + optional uint64 limit = 2; +} + +message ListSwapsResponse { + repeated string submarine_swaps = 1; + repeated string reverse_swaps = 2; + repeated string chain_swaps = 3; +} + message RescanRequest { string symbol = 1; uint64 start_height = 2; diff --git a/test/integration/db/repositories/ChainSwapRepository.spec.ts b/test/integration/db/repositories/ChainSwapRepository.spec.ts index 31cabe81..bb43d3d9 100644 --- a/test/integration/db/repositories/ChainSwapRepository.spec.ts +++ b/test/integration/db/repositories/ChainSwapRepository.spec.ts @@ -206,17 +206,48 @@ describe('ChainSwapRepository', () => { expect(fetched).toBeNull(); }); - test('should get chain swaps by swap data', async () => { - const count = 10; - for (let i = 0; i < count; i++) { - await createChainSwap(); - } + describe('getChainSwaps', () => { + test('should get chain swaps by swap data', async () => { + const count = 10; + for (let i = 0; i < count; i++) { + await createChainSwap(); + } + + const chainSwaps = await ChainSwapRepository.getChainSwaps({ + pair: 'L-BTC/BTC', + orderSide: OrderSide.BUY, + }); + expect(chainSwaps).toHaveLength(count); + }); + + test('should get chain swaps by swap data in order', async () => { + const count = 10; + for (let i = 0; i < count; i++) { + await createChainSwap(); + } - const chainSwaps = await ChainSwapRepository.getChainSwaps({ - pair: 'L-BTC/BTC', - orderSide: OrderSide.BUY, + const swaps = await ChainSwapRepository.getChainSwaps(undefined, [ + ['id', 'ASC'], + ]); + expect(swaps.map((s) => s.id)).toStrictEqual( + [...swaps].map((s) => s.id).sort(), + ); + }); + + test('should get chain swaps by swap data with limit', async () => { + const count = 10; + for (let i = 0; i < count; i++) { + await createChainSwap(); + } + + const limit = 2; + const swaps = await ChainSwapRepository.getChainSwaps( + undefined, + undefined, + limit, + ); + expect(swaps).toHaveLength(limit); }); - expect(chainSwaps).toHaveLength(count); }); test('should get chain swap by data', async () => { diff --git a/test/unit/grpc/GrpcService.spec.ts b/test/unit/grpc/GrpcService.spec.ts index c6aa2b4f..0016f36d 100644 --- a/test/unit/grpc/GrpcService.spec.ts +++ b/test/unit/grpc/GrpcService.spec.ts @@ -450,6 +450,68 @@ describe('GrpcService', () => { expect(service.swapManager.deferredClaimer.sweep).toHaveBeenCalledTimes(1); }); + describe('listSwaps', () => { + const listedSwaps = { + submarine: ['sub', 'id'], + reverse: ['reverse'], + chain: ['some', 'ids'], + }; + service.listSwaps = jest.fn().mockResolvedValue(listedSwaps); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should list swaps', async () => { + const status = 'swap.created'; + const limit = 123; + + const res = await new Promise<{ + error: unknown; + response: boltzrpc.ListSwapsResponse; + }>((resolve) => { + grpcService.listSwaps( + createCall({ status, limit }), + createCallback((error, response: boltzrpc.ListSwapsResponse) => + resolve({ error, response }), + ), + ); + }); + expect(res.error).toBeNull(); + expect(res.response.toObject()).toEqual({ + chainSwapsList: listedSwaps.chain, + reverseSwapsList: listedSwaps.reverse, + submarineSwapsList: listedSwaps.submarine, + }); + + expect(service.listSwaps).toHaveBeenCalledTimes(1); + expect(service.listSwaps).toHaveBeenCalledWith(status, limit); + }); + + test.each` + status + ${''} + ${undefined} + `('should coalesce empty status to undefined', async ({ status }) => { + const limit = 1; + + await new Promise<{ + error: unknown; + response: boltzrpc.ListSwapsResponse; + }>((resolve) => { + grpcService.listSwaps( + createCall({ status, limit }), + createCallback((error, response: boltzrpc.ListSwapsResponse) => + resolve({ error, response }), + ), + ); + }); + + expect(service.listSwaps).toHaveBeenCalledTimes(1); + expect(service.listSwaps).toHaveBeenCalledWith(undefined, limit); + }); + }); + test('should rescan', async () => { const symbol = 'BTC'; const startHeight = 420; diff --git a/test/unit/notifications/CommandHandler.spec.ts b/test/unit/notifications/CommandHandler.spec.ts index 9fe7cbec..40be9150 100644 --- a/test/unit/notifications/CommandHandler.spec.ts +++ b/test/unit/notifications/CommandHandler.spec.ts @@ -238,6 +238,7 @@ describe('CommandHandler', () => { '**getfees**: gets accumulated fees\n' + '**swapinfo**: gets all available information about a swap\n' + '**getstats**: gets statistics grouped by year and month for the current and last 6 months\n' + + '**listswaps**: lists swaps\n' + '**getbalance**: gets the balance of the wallets and channels\n' + '**lockedfunds**: gets funds locked up by Boltz\n' + '**pendingswaps**: gets a list of pending swaps\n' + @@ -393,6 +394,64 @@ describe('CommandHandler', () => { expect(spy).toHaveBeenCalledTimes(2); }); + describe('listSwaps', () => { + const listedSwaps = { + some: 'data', + }; + service.listSwaps = jest.fn().mockResolvedValue(listedSwaps); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should list swaps', async () => { + sendMessage('listswaps'); + await wait(10); + + expect(service.listSwaps).toHaveBeenCalledTimes(1); + expect(service.listSwaps).toHaveBeenCalledWith(undefined, 100); + + expect(mockSendMessage).toHaveBeenCalledTimes(1); + expect(mockSendMessage).toHaveBeenCalledWith( + `${codeBlock}${stringify(listedSwaps)}${codeBlock}`, + ); + }); + + test('should list swaps with status', async () => { + const status = 'some.status'; + + sendMessage(`listswaps ${status}`); + await wait(10); + + expect(service.listSwaps).toHaveBeenCalledTimes(1); + expect(service.listSwaps).toHaveBeenCalledWith(status, 100); + }); + + test('should list swaps with limit', async () => { + const status = 'some.status'; + const limit = 123; + + sendMessage(`listswaps ${status} ${limit}`); + await wait(10); + + expect(service.listSwaps).toHaveBeenCalledTimes(1); + expect(service.listSwaps).toHaveBeenCalledWith(status, limit); + }); + + test('should send error when limit is invalid', async () => { + const status = 'some.status'; + const limit = 'not a number'; + + sendMessage(`listswaps ${status} ${limit}`); + await wait(10); + + expect(mockSendMessage).toHaveBeenCalledTimes(1); + expect(mockSendMessage).toHaveBeenCalledWith( + 'Command failed: invalid limit', + ); + }); + }); + test('should get balances', async () => { sendMessage('getbalance'); await wait(5); diff --git a/test/unit/service/Service.spec.ts b/test/unit/service/Service.spec.ts index c9288f12..bca3b830 100644 --- a/test/unit/service/Service.spec.ts +++ b/test/unit/service/Service.spec.ts @@ -783,27 +783,128 @@ describe('Service', () => { ]); }); - test('should rescan currencies with chain client', async () => { - const startHeight = 21; + describe('listSwaps', () => { + SwapRepository.getSwaps = jest + .fn() + .mockResolvedValue([{ id: 'submarine' }]); - await expect(service.rescan('BTC', startHeight)).resolves.toEqual(123); - expect(mockRescanChain).toHaveBeenCalledTimes(1); - expect(mockRescanChain).toHaveBeenCalledWith(startHeight); - }); + ReverseSwapRepository.getReverseSwaps = jest + .fn() + .mockResolvedValue([{ id: 'reverse' }]); + + ChainSwapRepository.getChainSwaps = jest + .fn() + .mockResolvedValue([{ id: 'chain' }]); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should list swaps', async () => { + await expect(service.listSwaps()).resolves.toEqual({ + chain: ['chain'], + reverse: ['reverse'], + submarine: ['submarine'], + }); + + expect(SwapRepository.getSwaps).toHaveBeenCalledTimes(1); + expect(SwapRepository.getSwaps).toHaveBeenCalledWith( + {}, + [['createdAt', 'DESC']], + undefined, + ); + expect(ReverseSwapRepository.getReverseSwaps).toHaveBeenCalledTimes(1); + expect(ReverseSwapRepository.getReverseSwaps).toHaveBeenCalledWith( + {}, + [['createdAt', 'DESC']], + undefined, + ); + expect(ChainSwapRepository.getChainSwaps).toHaveBeenCalledTimes(1); + expect(ChainSwapRepository.getChainSwaps).toHaveBeenCalledWith( + {}, + [['createdAt', 'DESC']], + undefined, + ); + }); - test('should rescan currencies with provider', async () => { - const startHeight = 21; + test('should list swaps with status filter', async () => { + const status = 'swap.created'; + await service.listSwaps(status); - await expect(service.rescan('ETH', startHeight)).resolves.toEqual(100); - expect(mockRescan).toHaveBeenCalledTimes(1); - expect(mockRescan).toHaveBeenCalledWith(startHeight); + expect(SwapRepository.getSwaps).toHaveBeenCalledTimes(1); + expect(SwapRepository.getSwaps).toHaveBeenCalledWith( + { + status, + }, + [['createdAt', 'DESC']], + undefined, + ); + expect(ReverseSwapRepository.getReverseSwaps).toHaveBeenCalledTimes(1); + expect(ReverseSwapRepository.getReverseSwaps).toHaveBeenCalledWith( + { + status, + }, + [['createdAt', 'DESC']], + undefined, + ); + expect(ChainSwapRepository.getChainSwaps).toHaveBeenCalledTimes(1); + expect(ChainSwapRepository.getChainSwaps).toHaveBeenCalledWith( + { + status, + }, + [['createdAt', 'DESC']], + undefined, + ); + }); + + test('should list swaps with limit', async () => { + const limit = 123; + await service.listSwaps(undefined, limit); + + expect(SwapRepository.getSwaps).toHaveBeenCalledTimes(1); + expect(SwapRepository.getSwaps).toHaveBeenCalledWith( + {}, + [['createdAt', 'DESC']], + limit, + ); + expect(ReverseSwapRepository.getReverseSwaps).toHaveBeenCalledTimes(1); + expect(ReverseSwapRepository.getReverseSwaps).toHaveBeenCalledWith( + {}, + [['createdAt', 'DESC']], + limit, + ); + expect(ChainSwapRepository.getChainSwaps).toHaveBeenCalledTimes(1); + expect(ChainSwapRepository.getChainSwaps).toHaveBeenCalledWith( + {}, + [['createdAt', 'DESC']], + limit, + ); + }); }); - test('should throw when rescanning currency that does not exist', async () => { - const symbol = 'no'; - await expect(service.rescan(symbol, 123)).rejects.toEqual( - Errors.CURRENCY_NOT_FOUND(symbol), - ); + describe('rescan', () => { + test('should rescan currencies with chain client', async () => { + const startHeight = 21; + + await expect(service.rescan('BTC', startHeight)).resolves.toEqual(123); + expect(mockRescanChain).toHaveBeenCalledTimes(1); + expect(mockRescanChain).toHaveBeenCalledWith(startHeight); + }); + + test('should rescan currencies with provider', async () => { + const startHeight = 21; + + await expect(service.rescan('ETH', startHeight)).resolves.toEqual(100); + expect(mockRescan).toHaveBeenCalledTimes(1); + expect(mockRescan).toHaveBeenCalledWith(startHeight); + }); + + test('should throw when rescanning currency that does not exist', async () => { + const symbol = 'no'; + await expect(service.rescan(symbol, 123)).rejects.toEqual( + Errors.CURRENCY_NOT_FOUND(symbol), + ); + }); }); test('should get balance', async () => {