diff --git a/lib/cli/commands/Rescan.ts b/lib/cli/commands/Rescan.ts new file mode 100644 index 00000000..fa496b83 --- /dev/null +++ b/lib/cli/commands/Rescan.ts @@ -0,0 +1,24 @@ +import { Arguments } from 'yargs'; +import { RescanRequest } from '../../proto/boltzrpc_pb'; +import BuilderComponents from '../BuilderComponents'; +import { callback, loadBoltzClient } from '../Command'; + +export const command = 'rescan '; + +export const describe = 'rescans the chain of a symbol'; + +export const builder = { + symbol: BuilderComponents.symbol, + startHeight: { + describe: 'block height to start the rescan from', + type: 'number', + }, +}; + +export const handler = (argv: Arguments): void => { + const request = new RescanRequest(); + request.setSymbol(argv.symbol); + request.setStartHeight(argv.startHeight); + + loadBoltzClient(argv).rescan(request, callback()); +}; diff --git a/lib/grpc/GrpcServer.ts b/lib/grpc/GrpcServer.ts index 4a438b35..5082fa9e 100644 --- a/lib/grpc/GrpcServer.ts +++ b/lib/grpc/GrpcServer.ts @@ -27,6 +27,7 @@ class GrpcServer { updateTimeoutBlockDelta: grpcService.updateTimeoutBlockDelta, addReferral: grpcService.addReferral, sweepSwaps: grpcService.sweepSwaps, + rescan: grpcService.rescan, }); } diff --git a/lib/grpc/GrpcService.ts b/lib/grpc/GrpcService.ts index 95e94406..3685c90e 100644 --- a/lib/grpc/GrpcService.ts +++ b/lib/grpc/GrpcService.ts @@ -203,6 +203,23 @@ class GrpcService { }); }; + public rescan: handleUnaryCall< + boltzrpc.RescanRequest, + boltzrpc.RescanResponse + > = async (call, callback) => { + await this.handleCallback(call, callback, async () => { + const { symbol, startHeight } = call.request.toObject(); + + const endHeight = await this.service.rescan(symbol, startHeight); + + const response = new boltzrpc.RescanResponse(); + response.setStartHeight(startHeight); + response.setEndHeight(endHeight); + + return response; + }); + }; + private handleCallback = async ( call: R, callback: (error: any, res: T | null) => void, diff --git a/lib/proto/boltzrpc_grpc_pb.d.ts b/lib/proto/boltzrpc_grpc_pb.d.ts index e98f8d8c..9a503fa5 100644 --- a/lib/proto/boltzrpc_grpc_pb.d.ts +++ b/lib/proto/boltzrpc_grpc_pb.d.ts @@ -18,6 +18,7 @@ interface IBoltzService extends grpc.ServiceDefinition { @@ -110,6 +111,15 @@ interface IBoltzService_ISweepSwaps extends grpc.MethodDefinition; responseDeserialize: grpc.deserialize; } +interface IBoltzService_IRescan extends grpc.MethodDefinition { + path: "/boltzrpc.Boltz/Rescan"; + requestStream: false; + responseStream: false; + requestSerialize: grpc.serialize; + requestDeserialize: grpc.deserialize; + responseSerialize: grpc.serialize; + responseDeserialize: grpc.deserialize; +} export const BoltzService: IBoltzService; @@ -124,6 +134,7 @@ export interface IBoltzServer extends grpc.UntypedServiceImplementation { updateTimeoutBlockDelta: grpc.handleUnaryCall; addReferral: grpc.handleUnaryCall; sweepSwaps: grpc.handleUnaryCall; + rescan: grpc.handleUnaryCall; } export interface IBoltzClient { @@ -157,6 +168,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; + 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; } export class BoltzClient extends grpc.Client implements IBoltzClient { @@ -191,4 +205,7 @@ 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 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 ad484ecf..d1c10d0b 100644 --- a/lib/proto/boltzrpc_grpc_pb.js +++ b/lib/proto/boltzrpc_grpc_pb.js @@ -136,6 +136,28 @@ function deserialize_boltzrpc_GetInfoResponse(buffer_arg) { return boltzrpc_pb.GetInfoResponse.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'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_boltzrpc_RescanRequest(buffer_arg) { + return boltzrpc_pb.RescanRequest.deserializeBinary(new Uint8Array(buffer_arg)); +} + +function serialize_boltzrpc_RescanResponse(arg) { + if (!(arg instanceof boltzrpc_pb.RescanResponse)) { + throw new Error('Expected argument of type boltzrpc.RescanResponse'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_boltzrpc_RescanResponse(buffer_arg) { + return boltzrpc_pb.RescanResponse.deserializeBinary(new Uint8Array(buffer_arg)); +} + function serialize_boltzrpc_SendCoinsRequest(arg) { if (!(arg instanceof boltzrpc_pb.SendCoinsRequest)) { throw new Error('Expected argument of type boltzrpc.SendCoinsRequest'); @@ -343,6 +365,17 @@ addReferral: { responseSerialize: serialize_boltzrpc_SweepSwapsResponse, responseDeserialize: deserialize_boltzrpc_SweepSwapsResponse, }, + rescan: { + path: '/boltzrpc.Boltz/Rescan', + requestStream: false, + responseStream: false, + requestType: boltzrpc_pb.RescanRequest, + responseType: boltzrpc_pb.RescanResponse, + requestSerialize: serialize_boltzrpc_RescanRequest, + requestDeserialize: deserialize_boltzrpc_RescanRequest, + responseSerialize: serialize_boltzrpc_RescanResponse, + responseDeserialize: deserialize_boltzrpc_RescanResponse, + }, }; exports.BoltzClient = grpc.makeGenericClientConstructor(BoltzService); diff --git a/lib/proto/boltzrpc_pb.d.ts b/lib/proto/boltzrpc_pb.d.ts index a491c052..d7ac9a2d 100644 --- a/lib/proto/boltzrpc_pb.d.ts +++ b/lib/proto/boltzrpc_pb.d.ts @@ -749,6 +749,52 @@ export namespace SweepSwapsResponse { } +export class RescanRequest extends jspb.Message { + getSymbol(): string; + setSymbol(value: string): RescanRequest; + getStartHeight(): number; + setStartHeight(value: number): RescanRequest; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): RescanRequest.AsObject; + static toObject(includeInstance: boolean, msg: RescanRequest): RescanRequest.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: RescanRequest, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): RescanRequest; + static deserializeBinaryFromReader(message: RescanRequest, reader: jspb.BinaryReader): RescanRequest; +} + +export namespace RescanRequest { + export type AsObject = { + symbol: string, + startHeight: number, + } +} + +export class RescanResponse extends jspb.Message { + getStartHeight(): number; + setStartHeight(value: number): RescanResponse; + getEndHeight(): number; + setEndHeight(value: number): RescanResponse; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): RescanResponse.AsObject; + static toObject(includeInstance: boolean, msg: RescanResponse): RescanResponse.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: RescanResponse, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): RescanResponse; + static deserializeBinaryFromReader(message: RescanResponse, reader: jspb.BinaryReader): RescanResponse; +} + +export namespace RescanResponse { + export type AsObject = { + startHeight: number, + endHeight: number, + } +} + export enum OutputType { BECH32 = 0, COMPATIBILITY = 1, diff --git a/lib/proto/boltzrpc_pb.js b/lib/proto/boltzrpc_pb.js index 67fc3cee..561b29a8 100644 --- a/lib/proto/boltzrpc_pb.js +++ b/lib/proto/boltzrpc_pb.js @@ -41,6 +41,8 @@ goog.exportSymbol('proto.boltzrpc.GetInfoResponse', null, global); goog.exportSymbol('proto.boltzrpc.LightningInfo', null, global); goog.exportSymbol('proto.boltzrpc.LightningInfo.Channels', null, global); goog.exportSymbol('proto.boltzrpc.OutputType', null, global); +goog.exportSymbol('proto.boltzrpc.RescanRequest', null, global); +goog.exportSymbol('proto.boltzrpc.RescanResponse', null, global); goog.exportSymbol('proto.boltzrpc.SendCoinsRequest', null, global); goog.exportSymbol('proto.boltzrpc.SendCoinsResponse', null, global); goog.exportSymbol('proto.boltzrpc.SweepSwapsRequest', null, global); @@ -661,6 +663,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.RescanRequest = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.boltzrpc.RescanRequest, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.boltzrpc.RescanRequest.displayName = 'proto.boltzrpc.RescanRequest'; +} +/** + * 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.RescanResponse = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.boltzrpc.RescanResponse, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.boltzrpc.RescanResponse.displayName = 'proto.boltzrpc.RescanResponse'; +} @@ -5776,6 +5820,326 @@ proto.boltzrpc.SweepSwapsResponse.prototype.clearClaimedSymbolsMap = function() return this;}; + + + +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.RescanRequest.prototype.toObject = function(opt_includeInstance) { + return proto.boltzrpc.RescanRequest.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.RescanRequest} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.boltzrpc.RescanRequest.toObject = function(includeInstance, msg) { + var f, obj = { + symbol: jspb.Message.getFieldWithDefault(msg, 1, ""), + startHeight: 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.RescanRequest} + */ +proto.boltzrpc.RescanRequest.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.boltzrpc.RescanRequest; + return proto.boltzrpc.RescanRequest.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.boltzrpc.RescanRequest} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.boltzrpc.RescanRequest} + */ +proto.boltzrpc.RescanRequest.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.setSymbol(value); + break; + case 2: + var value = /** @type {number} */ (reader.readUint64()); + msg.setStartHeight(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.boltzrpc.RescanRequest.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.boltzrpc.RescanRequest.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.RescanRequest} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.boltzrpc.RescanRequest.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getSymbol(); + if (f.length > 0) { + writer.writeString( + 1, + f + ); + } + f = message.getStartHeight(); + if (f !== 0) { + writer.writeUint64( + 2, + f + ); + } +}; + + +/** + * optional string symbol = 1; + * @return {string} + */ +proto.boltzrpc.RescanRequest.prototype.getSymbol = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** + * @param {string} value + * @return {!proto.boltzrpc.RescanRequest} returns this + */ +proto.boltzrpc.RescanRequest.prototype.setSymbol = function(value) { + return jspb.Message.setProto3StringField(this, 1, value); +}; + + +/** + * optional uint64 start_height = 2; + * @return {number} + */ +proto.boltzrpc.RescanRequest.prototype.getStartHeight = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 2, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.boltzrpc.RescanRequest} returns this + */ +proto.boltzrpc.RescanRequest.prototype.setStartHeight = function(value) { + return jspb.Message.setProto3IntField(this, 2, value); +}; + + + + + +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.RescanResponse.prototype.toObject = function(opt_includeInstance) { + return proto.boltzrpc.RescanResponse.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.RescanResponse} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.boltzrpc.RescanResponse.toObject = function(includeInstance, msg) { + var f, obj = { + startHeight: jspb.Message.getFieldWithDefault(msg, 1, 0), + endHeight: 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.RescanResponse} + */ +proto.boltzrpc.RescanResponse.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.boltzrpc.RescanResponse; + return proto.boltzrpc.RescanResponse.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.boltzrpc.RescanResponse} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.boltzrpc.RescanResponse} + */ +proto.boltzrpc.RescanResponse.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {number} */ (reader.readUint64()); + msg.setStartHeight(value); + break; + case 2: + var value = /** @type {number} */ (reader.readUint64()); + msg.setEndHeight(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.boltzrpc.RescanResponse.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.boltzrpc.RescanResponse.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.RescanResponse} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.boltzrpc.RescanResponse.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getStartHeight(); + if (f !== 0) { + writer.writeUint64( + 1, + f + ); + } + f = message.getEndHeight(); + if (f !== 0) { + writer.writeUint64( + 2, + f + ); + } +}; + + +/** + * optional uint64 start_height = 1; + * @return {number} + */ +proto.boltzrpc.RescanResponse.prototype.getStartHeight = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 1, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.boltzrpc.RescanResponse} returns this + */ +proto.boltzrpc.RescanResponse.prototype.setStartHeight = function(value) { + return jspb.Message.setProto3IntField(this, 1, value); +}; + + +/** + * optional uint64 end_height = 2; + * @return {number} + */ +proto.boltzrpc.RescanResponse.prototype.getEndHeight = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 2, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.boltzrpc.RescanResponse} returns this + */ +proto.boltzrpc.RescanResponse.prototype.setEndHeight = function(value) { + return jspb.Message.setProto3IntField(this, 2, value); +}; + + /** * @enum {number} */ diff --git a/lib/service/Errors.ts b/lib/service/Errors.ts index a278835d..d5b75c8c 100644 --- a/lib/service/Errors.ts +++ b/lib/service/Errors.ts @@ -153,4 +153,8 @@ export default { message: 'swap not eligible for a cooperative claim broadcast', code: concatErrorCode(ErrorCodePrefix.Service, 39), }), + NO_CHAIN_FOR_SYMBOL: (): Error => ({ + message: 'no chain for symbol', + code: concatErrorCode(ErrorCodePrefix.Service, 40), + }), }; diff --git a/lib/service/Service.ts b/lib/service/Service.ts index 04fc1880..cfe7397b 100644 --- a/lib/service/Service.ts +++ b/lib/service/Service.ts @@ -346,6 +346,34 @@ class Service { return response; }; + public rescan = async ( + symbol: string, + startHeight: number, + ): Promise => { + const currency = this.getCurrency(symbol); + + let endHeight: number; + + if (currency.chainClient) { + endHeight = (await currency.chainClient.getBlockchainInfo()).blocks; + await currency.chainClient.rescanChain(startHeight); + } else if (currency.provider) { + const manager = this.walletManager.ethereumManagers.find((manager) => + manager.hasSymbol(symbol), + ); + if (manager === undefined) { + throw Errors.NO_CHAIN_FOR_SYMBOL(); + } + + endHeight = await manager.provider.getBlockNumber(); + await manager.contractEventHandler.rescan(startHeight); + } else { + throw Errors.NO_CHAIN_FOR_SYMBOL(); + } + + return endHeight; + }; + /** * Gets the balance for either all wallets or just a single one if specified */ diff --git a/package-lock.json b/package-lock.json index da7a0adb..43cf7168 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "dependencies": { "@boltz/bolt11": "^1.2.7", "@google-cloud/storage": "^7.7.0", - "@grpc/grpc-js": "^1.10.0", + "@grpc/grpc-js": "^1.10.1", "@iarna/toml": "^2.2.5", "@mattermost/client": "^9.4.0", "@mattermost/types": "^9.4.0", @@ -23,14 +23,14 @@ "bip39": "^3.1.0", "bitcoinjs-lib": "^6.1.5", "bolt11": "^1.4.1", - "boltz-core": "^2.1.0", + "boltz-core": "^2.1.1", "colors": "^1.4.0", "cors": "^2.8.5", "cross-os": "^1.5.0", "csv-parse": "^5.5.3", "discord.js": "^14.14.1", "ecpair": "^2.1.0", - "ethers": "^6.11.0", + "ethers": "^6.11.1", "express": "^4.18.2", "google-protobuf": "^3.21.2", "ip-address": "^9.0.5", @@ -41,7 +41,7 @@ "pg": "^8.11.3", "pg-hstore": "^2.3.4", "prom-client": "^15.1.0", - "sequelize": "^6.37.0", + "sequelize": "^6.37.1", "slip77": "^0.2.0", "sqlite3": "^5.1.7", "tiny-secp256k1": "^2.2.3", @@ -72,7 +72,7 @@ "@typescript-eslint/parser": "^7.0.1", "eslint": "^8.56.0", "eslint-plugin-import": "^2.29.1", - "eslint-plugin-jest": "^27.6.3", + "eslint-plugin-jest": "^27.9.0", "eslint-plugin-node": "^11.1.0", "git-cliff": "^1.4.0", "grpc_tools_node_protoc_ts": "^5.3.3", @@ -1022,9 +1022,9 @@ } }, "node_modules/@grpc/grpc-js": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.10.0.tgz", - "integrity": "sha512-tx+eoEsqkMkLCHR4OOplwNIaJ7SVZWzeVKzEMBz8VR+TbssgBYOP4a0P+KQiQ6LaTG4SGaIEu7YTS8xOmkOWLA==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.10.1.tgz", + "integrity": "sha512-55ONqFytZExfOIjF1RjXPcVmT/jJqFzbbDqxK9jmRV4nxiYWtL9hENSW1Jfx0SdZfrvoqd44YJ/GJTqfRrawSQ==", "dependencies": { "@grpc/proto-loader": "^0.7.8", "@types/node": ">=12.12.47" @@ -3998,9 +3998,9 @@ } }, "node_modules/boltz-core": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/boltz-core/-/boltz-core-2.1.0.tgz", - "integrity": "sha512-QxpIqOxalgEqEhwofgQgWM0cp7XNe/fMsfg7tZq9Zd8vqep8DfCDgd9hKRjQgD+BRAmXUqWc7ydgA8Ty4obolg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/boltz-core/-/boltz-core-2.1.1.tgz", + "integrity": "sha512-nbbMQbWcpJKoPvf1KNKZOlcOoflIrcretVhdt4rQ+QXVRULj+lCbl8t3FCfYLNKurWxl7OC3j/f+ILFCU7tkIw==", "dependencies": { "@boltz/bitcoin-ops": "^2.0.0", "@openzeppelin/contracts": "^5.0.1", @@ -4845,7 +4845,8 @@ }, "node_modules/discord.js": { "version": "14.14.1", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.14.1.tgz", + "integrity": "sha512-/hUVzkIerxKHyRKopJy5xejp4MYKDPTszAnpYxzVVv4qJYf+Tkt+jnT2N29PIPschicaEEpXwF2ARrTYHYwQ5w==", "dependencies": { "@discordjs/builders": "^1.7.0", "@discordjs/collection": "1.5.3", @@ -5323,9 +5324,9 @@ } }, "node_modules/eslint-plugin-jest": { - "version": "27.6.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.6.3.tgz", - "integrity": "sha512-+YsJFVH6R+tOiO3gCJon5oqn4KWc+mDq2leudk8mrp8RFubLOo9CVyi3cib4L7XMpxExmkmBZQTPDYVBzgpgOA==", + "version": "27.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.9.0.tgz", + "integrity": "sha512-QIT7FH7fNmd9n4se7FFKHbsLKGQiw885Ds6Y/sxKgCZ6natwCsXdgPOADnYVxN2QrRweF0FZWbJ6S7Rsn7llug==", "dev": true, "dependencies": { "@typescript-eslint/utils": "^5.10.0" @@ -5334,7 +5335,7 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^5.0.0 || ^6.0.0", + "@typescript-eslint/eslint-plugin": "^5.0.0 || ^6.0.0 || ^7.0.0", "eslint": "^7.0.0 || ^8.0.0", "jest": "*" }, @@ -5599,9 +5600,9 @@ } }, "node_modules/ethers": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.11.0.tgz", - "integrity": "sha512-kPHNTnhVWiWU6AVo6CAeTjXEK24SpCXyZvwG9ROFjT0Vlux0EOhWKBAeC+45iDj80QNJTYaT1SDEmeunT0vDNw==", + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.11.1.tgz", + "integrity": "sha512-mxTAE6wqJQAbp5QAe/+o+rXOID7Nw91OZXvgpjDa1r4fAbq2Nu314oEZSbjoRLacuCzs7kUC3clEvkCQowffGg==", "funding": [ { "type": "individual", @@ -10493,9 +10494,9 @@ "license": "MIT" }, "node_modules/sequelize": { - "version": "6.37.0", - "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.0.tgz", - "integrity": "sha512-MS6j6aXqWzB3fe9FhmfpQMgVC16bBdYroJCqIqR0l9M2ko8pZdKoi/0PiNWgMyFQDXUHxXyAOG3K07CbnOhteQ==", + "version": "6.37.1", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.1.tgz", + "integrity": "sha512-vIKKzQ9dGp2aBOxQRD1FmUYViuQiKXSJ8yah8TsaBx4U3BokJt+Y2A0qz2C4pj08uX59qpWxRqSLEfRmVOEgQw==", "funding": [ { "type": "opencollective", diff --git a/package.json b/package.json index dba22f83..acf53c01 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "dependencies": { "@boltz/bolt11": "^1.2.7", "@google-cloud/storage": "^7.7.0", - "@grpc/grpc-js": "^1.10.0", + "@grpc/grpc-js": "^1.10.1", "@iarna/toml": "^2.2.5", "@mattermost/client": "^9.4.0", "@mattermost/types": "^9.4.0", @@ -81,14 +81,14 @@ "bip39": "^3.1.0", "bitcoinjs-lib": "^6.1.5", "bolt11": "^1.4.1", - "boltz-core": "^2.1.0", + "boltz-core": "^2.1.1", "colors": "^1.4.0", "cors": "^2.8.5", "cross-os": "^1.5.0", "csv-parse": "^5.5.3", "discord.js": "^14.14.1", "ecpair": "^2.1.0", - "ethers": "^6.11.0", + "ethers": "^6.11.1", "express": "^4.18.2", "google-protobuf": "^3.21.2", "ip-address": "^9.0.5", @@ -99,7 +99,7 @@ "pg": "^8.11.3", "pg-hstore": "^2.3.4", "prom-client": "^15.1.0", - "sequelize": "^6.37.0", + "sequelize": "^6.37.1", "slip77": "^0.2.0", "sqlite3": "^5.1.7", "tiny-secp256k1": "^2.2.3", @@ -126,7 +126,7 @@ "@typescript-eslint/parser": "^7.0.1", "eslint": "^8.56.0", "eslint-plugin-import": "^2.29.1", - "eslint-plugin-jest": "^27.6.3", + "eslint-plugin-jest": "^27.9.0", "eslint-plugin-node": "^11.1.0", "git-cliff": "^1.4.0", "grpc_tools_node_protoc_ts": "^5.3.3", diff --git a/proto/boltzrpc.proto b/proto/boltzrpc.proto index 6c66767a..e8db9bcb 100644 --- a/proto/boltzrpc.proto +++ b/proto/boltzrpc.proto @@ -28,6 +28,7 @@ service Boltz { rpc AddReferral (AddReferralRequest) returns (AddReferralResponse); rpc SweepSwaps (SweepSwapsRequest) returns (SweepSwapsResponse); + rpc Rescan (RescanRequest) returns (RescanResponse); } enum OutputType { @@ -183,3 +184,12 @@ message SweepSwapsResponse { map claimed_symbols = 1; } + +message RescanRequest { + string symbol = 1; + uint64 start_height = 2; +} +message RescanResponse { + uint64 start_height = 1; + uint64 end_height = 2; +} diff --git a/test/unit/grpc/GrpcService.spec.ts b/test/unit/grpc/GrpcService.spec.ts index dbda579b..fb3671fa 100644 --- a/test/unit/grpc/GrpcService.spec.ts +++ b/test/unit/grpc/GrpcService.spec.ts @@ -126,8 +126,9 @@ jest.mock('../../../lib/service/Service', () => { deriveKeys: mockDeriveKeys, getAddress: mockGetAddress, sendCoins: mockSendCoins, - updateTimeoutBlockDelta: mockUpdateTimeoutBlockDelta, addReferral: mockAddReferral, + rescan: jest.fn().mockResolvedValue(831106), + updateTimeoutBlockDelta: mockUpdateTimeoutBlockDelta, }; }); }); @@ -442,6 +443,28 @@ describe('GrpcService', () => { expect(service.swapManager.deferredClaimer.sweep).toHaveBeenCalledTimes(1); }); + test('should rescan', async () => { + const symbol = 'BTC'; + const startHeight = 420; + + await new Promise((resolve) => { + grpcService.rescan( + createCall({ symbol, startHeight }), + createCallback((error, response: boltzrpc.RescanResponse) => { + expect(error).toEqual(null); + expect(response.toObject()).toEqual({ + startHeight, + endHeight: 831106, + }); + resolve(); + }), + ); + }); + + expect(service.rescan).toHaveBeenCalledTimes(1); + expect(service.rescan).toHaveBeenCalledWith(symbol, startHeight); + }); + test('should handle resolved callbacks', async () => { const call = randomBytes(32); const cb = jest.fn(); diff --git a/test/unit/service/Service.spec.ts b/test/unit/service/Service.spec.ts index 4d3d9853..0c3da23b 100644 --- a/test/unit/service/Service.spec.ts +++ b/test/unit/service/Service.spec.ts @@ -237,6 +237,8 @@ const tokenTransaction = { const mockSendToken = jest.fn().mockResolvedValue(tokenTransaction); const mockSweepToken = jest.fn().mockResolvedValue(tokenTransaction); +const mockRescan = jest.fn().mockResolvedValue(undefined); + jest.mock('../../../lib/wallet/WalletManager', () => { return jest.fn().mockImplementation(() => ({ ethereumManagers: [ @@ -245,6 +247,9 @@ jest.mock('../../../lib/wallet/WalletManager', () => { provider: mockedProvider(), tokenAddresses: new Map(), hasSymbol: jest.fn().mockReturnValue(true), + contractEventHandler: { + rescan: mockRescan, + }, }, ], wallets: new Map([ @@ -454,9 +459,12 @@ const mockGetRawTransactionVerbose = jest.fn().mockImplementation(async () => { }; }); +const mockRescanChain = jest.fn().mockResolvedValue(undefined); + jest.mock('../../../lib/chain/ChainClient', () => { return jest.fn().mockImplementation(() => ({ on: () => {}, + rescanChain: mockRescanChain, estimateFee: mockEstimateFee, getNetworkInfo: mockGetNetworkInfo, getBlockchainInfo: mockGetBlockchainInfo, @@ -760,6 +768,29 @@ describe('Service', () => { ]); }); + 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 () => { const response = await service.getBalance(); const balances = response.getBalancesMap();