diff --git a/packages/client/package.json b/packages/client/package.json index 3a8e314..3980f24 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -51,13 +51,17 @@ "devDependencies": { "@ethersproject/experimental": "^5.7.0", "@size-limit/preset-small-lib": "^7.0.8", + "@types/cors": "^2.8.8", "@types/express": "^4.17.8", + "@types/node-cron": "^3.0.1", "bigint-buffer": "^1.1.5", + "cors": "^2.8.5", "express": "^4.17.1", "express-validator": "^6.14.0", "ganache": "^7.9.1", "glob": "^8.0.3", "husky": "^7.0.4", + "node-cron": "^3.0.0", "size-limit": "^7.0.8", "solc": "0.4.17", "tsdx": "^0.14.1", diff --git a/packages/client/src/client-common/interfaces/core.ts b/packages/client/src/client-common/interfaces/core.ts index 4594a9d..cdc33ff 100644 --- a/packages/client/src/client-common/interfaces/core.ts +++ b/packages/client/src/client-common/interfaces/core.ts @@ -3,6 +3,8 @@ import { Signer } from "@ethersproject/abstract-signer"; import { Contract, ContractInterface } from "@ethersproject/contracts"; import { JsonRpcProvider } from "@ethersproject/providers"; +import { GenericRecord } from "./common"; +import { UnfetchResponse } from "unfetch"; export interface IClientWeb3Core { useSigner: (signer: Signer) => void; @@ -14,6 +16,10 @@ export interface IClientWeb3Core { ensureOnline: () => Promise; attachContract: (address: string, abi: ContractInterface) => Contract & T; getLinkCollectionAddress: () => string; + isRelayUp: () => Promise; + assignValidatorEndpoint: () => Promise; + get: (path: string, data?: GenericRecord) => Promise; + post: (path: string, data?: GenericRecord) => Promise; } export interface IClientCore { diff --git a/packages/client/src/client-common/modules/web3.ts b/packages/client/src/client-common/modules/web3.ts index 826e113..2759936 100644 --- a/packages/client/src/client-common/modules/web3.ts +++ b/packages/client/src/client-common/modules/web3.ts @@ -4,7 +4,12 @@ import { JsonRpcProvider } from "@ethersproject/providers"; import { Contract, ContractInterface } from "@ethersproject/contracts"; import { Signer } from "@ethersproject/abstract-signer"; import { IClientWeb3Core } from "../interfaces/core"; -import { NoLinkCollection } from "del-sdk-common"; +import { NoLinkCollection, NoProviderError, NoSignerError } from "del-sdk-common"; +import { GenericRecord, IHttpConfig } from "../interfaces/common"; +import { LinkCollection__factory } from "del-osx-lib"; +import { NoValidator } from "../../utils/errors"; +import { Network } from "../../utils/network"; +import { UnfetchResponse } from "unfetch"; const linkCollectionAddressMap = new Map(); const providersMap = new Map(); @@ -28,6 +33,10 @@ export class Web3Module implements IClientWeb3Core { linkCollectionAddressMap.set(this, context.linkCollectionAddress); } + this.config = { + url: new URL("http://localhost"), + headers: {}, + }; Object.freeze(Web3Module.prototype); Object.freeze(this); } @@ -155,4 +164,39 @@ export class Web3Module implements IClientWeb3Core { } return this.linkCollectionAddress; } + public config: IHttpConfig; + + public async assignValidatorEndpoint(): Promise { + const signer = this.getConnectedSigner(); + if (!signer) { + throw new NoSignerError(); + } else if (!signer.provider) { + throw new NoProviderError(); + } + + const contract = LinkCollection__factory.connect(this.getLinkCollectionAddress(), signer); + const validators = await contract.getValidators(); + if (validators.length === 0) { + throw new NoValidator(); + } + const idx = Math.floor(Math.random() * validators.length); + this.config.url = new URL(validators[idx].endpoint); + } + + public async isRelayUp(): Promise { + try { + const res = await this.get("/"); + return (res.status === 200 && (await res.json())) === "OK"; + } catch { + return false; + } + } + + public async get(path: string, data?: GenericRecord): Promise { + return Network.get(this.config, path, data); + } + + public async post(path: string, data?: GenericRecord): Promise { + return Network.post(this.config, path, data); + } } diff --git a/packages/client/src/interfaces.ts b/packages/client/src/interfaces.ts index 97b0f4a..5334061 100644 --- a/packages/client/src/interfaces.ts +++ b/packages/client/src/interfaces.ts @@ -5,9 +5,13 @@ import { BigNumber } from "ethers"; export interface IClientMethods extends IClientCore { addRequest: (email: string) => AsyncGenerator; toAddress: (email: string) => Promise; - toEmail: (wallet: string) => Promise; - nonceOf: (wallet: string) => Promise; + toEmail: (address: string) => Promise; + nonceOf: (address: string) => Promise; getValidators: () => Promise; + isRelayUp: () => Promise; + assignValidatorEndpoint: () => Promise; + register: (email: string) => AsyncGenerator; + getRegisterStatus: (id: string) => Promise; } export interface IClient { @@ -44,10 +48,24 @@ export type AddRequestValue = email: string; id: string; emailHash: string; - wallet: string; + address: string; }; export enum AddRequestSteps { ADDING = "adding", DONE = "done", } + +export type RegisterValue = + | { key: RegisterSteps.DOING; requestId: string; email: string; address: string } + | { + key: RegisterSteps.DONE; + requestId: string; + email: string; + address: string; + }; + +export enum RegisterSteps { + DOING = "doing", + DONE = "done", +} diff --git a/packages/client/src/internal/client/methods.ts b/packages/client/src/internal/client/methods.ts index 5662681..38fe061 100644 --- a/packages/client/src/internal/client/methods.ts +++ b/packages/client/src/internal/client/methods.ts @@ -1,12 +1,29 @@ import { LinkCollection__factory } from "del-osx-lib"; import { NoProviderError, NoSignerError } from "del-sdk-common"; -import { AddRequestSteps, AddRequestValue, IClientMethods, ValidatorInfoValue } from "../../interfaces"; +import { + AddRequestSteps, + AddRequestValue, + IClientMethods, + RegisterSteps, + RegisterValue, + ValidatorInfoValue, +} from "../../interfaces"; import { ClientCore, Context } from "../../client-common"; import { ContractUtils } from "../../utils/ContractUtils"; import { BigNumber } from "ethers"; import { Contract } from "@ethersproject/contracts"; +import { + AlreadyRegisteredAddress, + AlreadyRegisteredEmail, + FailedParameterValidation, + NotValidSignature, + ServerError, + UnknownError, +} from "../../utils/errors"; + +import { handleNetworkError } from "../../utils/network/ErrorTypes"; /** * Methods module the SDK Generic Client @@ -14,15 +31,23 @@ import { Contract } from "@ethersproject/contracts"; export class ClientMethods extends ClientCore implements IClientMethods { constructor(context: Context) { super(context); + Object.freeze(ClientMethods.prototype); Object.freeze(this); } + public async assignValidatorEndpoint(): Promise { + await this.web3.assignValidatorEndpoint(); + } + + public async isRelayUp(): Promise { + return await this.web3.isRelayUp(); + } /** * Add a request * * @param {email} email - * @return {*} {AsyncGenerator} + * @return {*} {AsyncGenerator} * @memberof ClientMethods */ public async *addRequest(email: string): AsyncGenerator { @@ -65,11 +90,18 @@ export class ClientMethods extends ClientCore implements IClientMethods { id: events[0].args[0], email: email, emailHash: events[0].args[1], - wallet: events[0].args[2], + address: events[0].args[2], }; } - public async toAddress(email: string): Promise { + /** + * Register email & address + * + * @param {email} email + * @return {*} {AsyncGenerator} + * @memberof ClientMethods + */ + public async *register(email: string): AsyncGenerator { const signer = this.web3.getConnectedSigner(); if (!signer) { throw new NoSignerError(); @@ -78,32 +110,90 @@ export class ClientMethods extends ClientCore implements IClientMethods { } const contract = LinkCollection__factory.connect(this.web3.getLinkCollectionAddress(), signer); + const address = await signer.getAddress(); + const nonce = await contract.nonceOf(address); + const signature = await ContractUtils.signRequestData(signer, email, nonce); + const res = await this.web3.post("request", { + email, + address, + signature, + }); + + if (!res.ok) { + throw handleNetworkError(res); + } + + const response = await res.json(); + if (response.code === 200) { + } else if (response.code === 400) { + throw new FailedParameterValidation(); + } else if (response.code === 401) { + throw new NotValidSignature(); + } else if (response.code === 402) { + throw new AlreadyRegisteredEmail(); + } else if (response.code === 403) { + throw new AlreadyRegisteredAddress(); + } else if (response.code === 500) { + throw new ServerError(); + } else { + throw new UnknownError(); + } + + yield { + key: RegisterSteps.DOING, + requestId: response.data.requestId, + email, + address, + }; + + const start = ContractUtils.getTimeStamp(); + let done = false; + while (!done) { + const status = await this.getRegisterStatus(response.data.requestId); + if (status !== 0 || ContractUtils.getTimeStamp() - start > 60) { + done = true; + } else { + await ContractUtils.delay(3000); + } + } + + yield { + key: RegisterSteps.DONE, + requestId: response.data.requestId, + email, + address, + }; + } + + public async toAddress(email: string): Promise { + const provider = this.web3.getProvider(); + if (!provider) { + throw new NoProviderError(); + } + + const contract = LinkCollection__factory.connect(this.web3.getLinkCollectionAddress(), provider); return await contract.toAddress(email); } - public async toEmail(wallet: string): Promise { - const signer = this.web3.getConnectedSigner(); - if (!signer) { - throw new NoSignerError(); - } else if (!signer.provider) { + public async toEmail(address: string): Promise { + const provider = this.web3.getProvider(); + if (!provider) { throw new NoProviderError(); } - const contract = LinkCollection__factory.connect(this.web3.getLinkCollectionAddress(), signer); + const contract = LinkCollection__factory.connect(this.web3.getLinkCollectionAddress(), provider); - return await contract.toEmail(wallet); + return await contract.toEmail(address); } public async nonceOf(wallet: string): Promise { - const signer = this.web3.getConnectedSigner(); - if (!signer) { - throw new NoSignerError(); - } else if (!signer.provider) { + const provider = this.web3.getProvider(); + if (!provider) { throw new NoProviderError(); } - const contract = LinkCollection__factory.connect(this.web3.getLinkCollectionAddress(), signer); + const contract = LinkCollection__factory.connect(this.web3.getLinkCollectionAddress(), provider); return await contract.nonceOf(wallet); } @@ -122,22 +212,31 @@ export class ClientMethods extends ClientCore implements IClientMethods { } public async getValidators(): Promise { - const signer = this.web3.getConnectedSigner(); - if (!signer) { - throw new NoSignerError(); - } else if (!signer.provider) { + const provider = this.web3.getProvider(); + if (!provider) { throw new NoProviderError(); } - const contract = LinkCollection__factory.connect(this.web3.getLinkCollectionAddress(), signer); - const validators = await contract.getValidators() + const contract = LinkCollection__factory.connect(this.web3.getLinkCollectionAddress(), provider); + const validators = await contract.getValidators(); return validators.map(m => { return { address: m.validator, index: m.index.toNumber(), endpoint: m.endpoint, - status: m.status - } - }) + status: m.status, + }; + }); + } + + public async getRegisterStatus(id: string): Promise { + const provider = this.web3.getProvider(); + if (!provider) { + throw new NoProviderError(); + } + + const contract = LinkCollection__factory.connect(this.web3.getLinkCollectionAddress(), provider); + const res = await contract.getRequestItem(id); + return res.status; } } diff --git a/packages/client/src/utils/ContractUtils.ts b/packages/client/src/utils/ContractUtils.ts index cae45bf..f286f4e 100644 --- a/packages/client/src/utils/ContractUtils.ts +++ b/packages/client/src/utils/ContractUtils.ts @@ -50,6 +50,16 @@ export class ContractUtils { return "0x" + data.toString("hex"); } + public static getTimeStamp(): number { + return Math.floor(new Date().getTime() / 1000); + } + + public static delay(interval: number): Promise { + return new Promise((resolve, _) => { + setTimeout(resolve, interval); + }); + } + public static getRequestId(emailHash: string, address: string, nonce: BigNumberish): string { const encodedResult = ethers.utils.defaultAbiCoder.encode( ["bytes32", "address", "uint256", "bytes32"], diff --git a/packages/client/src/utils/errors.ts b/packages/client/src/utils/errors.ts index c65c17f..bcde885 100644 --- a/packages/client/src/utils/errors.ts +++ b/packages/client/src/utils/errors.ts @@ -55,3 +55,50 @@ export class BodyParseError extends ClientError { this.message = "Error parsing body"; } } + +export class NoValidator extends Error { + constructor() { + super("No Validators"); + } +} + +export class FailedParameterValidation extends Error { + constructor() { + super("Parameter validation failed"); + } +} + +export class NotValidSignature extends Error { + constructor() { + super("Signature is not valid"); + } +} + +export class AlreadyRegisteredEmail extends Error { + constructor() { + super("Email is already registered"); + } +} + +export class AlreadyRegisteredAddress extends Error { + constructor() { + super("Address is already registered"); + } +} + +export class ServerError extends Error { + constructor() { + super("Failed request"); + } +} + +export class UnknownError extends Error { + constructor() { + super("Unknown error occurred"); + } +} +export class EVMError extends Error { + constructor() { + super("Error in EVM"); + } +} diff --git a/packages/client/src/client-common/interfaces/network.ts b/packages/client/src/utils/network.ts similarity index 67% rename from packages/client/src/client-common/interfaces/network.ts rename to packages/client/src/utils/network.ts index 1f5a8be..f1fd1a1 100644 --- a/packages/client/src/client-common/interfaces/network.ts +++ b/packages/client/src/utils/network.ts @@ -1,13 +1,12 @@ import fetch, { UnfetchResponse } from "unfetch"; -import { GenericRecord, IHttpConfig } from "./common"; -import { InvalidResponseError } from "../../utils/errors"; +import { GenericRecord, IHttpConfig } from "../client-common/interfaces/common"; export namespace Network { /** * Performs a request and returns a JSON object with the response */ - export async function get(config: IHttpConfig, path: string, data?: GenericRecord) { + export async function get(config: IHttpConfig, path: string, data?: GenericRecord): Promise { const { url, headers } = config; const endpoint: URL = new URL(path, url); for (const [key, value] of Object.entries(data ?? {})) { @@ -17,12 +16,9 @@ export namespace Network { } const response: UnfetchResponse = await fetch(endpoint.href, { method: "GET", - headers + headers, }); - if (!response.ok) { - throw new InvalidResponseError(response); - } - return response.json(); + return response; } export async function post(config: IHttpConfig, path: string, data?: any) { @@ -32,14 +28,11 @@ export namespace Network { method: "POST", headers: { "Content-Type": "application/json", - ...headers + ...headers, }, - body: JSON.stringify(data) + body: JSON.stringify(data), }); - if (!response.ok) { - throw new InvalidResponseError(response); - } - return response.json(); + return response; } } diff --git a/packages/client/src/utils/network/ErrorTypes.ts b/packages/client/src/utils/network/ErrorTypes.ts index fefa94f..655dad1 100644 --- a/packages/client/src/utils/network/ErrorTypes.ts +++ b/packages/client/src/utils/network/ErrorTypes.ts @@ -22,23 +22,16 @@ export class NetworkError extends Error { */ public statusText: string; - /** - * The message of response - */ - public statusMessage: string; - /** * Constructor * @param status The status code * @param statusText The status text - * @param statusMessage The message of response */ - constructor(status: number, statusText: string, statusMessage: string) { + constructor(status: number, statusText: string) { super(statusText); this.name = "NetworkError"; this.status = status; this.statusText = statusText; - this.statusMessage = statusMessage; } } @@ -50,10 +43,9 @@ export class NotFoundError extends NetworkError { * Constructor * @param status The status code * @param statusText The status text - * @param statusMessage The message of response */ - constructor(status: number, statusText: string, statusMessage: string) { - super(status, statusText, statusMessage); + constructor(status: number, statusText: string) { + super(status, statusText); this.name = "NotFoundError"; } } @@ -66,10 +58,9 @@ export class BadRequestError extends NetworkError { * Constructor * @param status The status code * @param statusText The status text - * @param statusMessage The message of response */ - constructor(status: number, statusText: string, statusMessage: string) { - super(status, statusText, statusMessage); + constructor(status: number, statusText: string) { + super(status, statusText); this.name = "BadRequestError"; } } @@ -86,23 +77,13 @@ export function handleNetworkError(error: any): Error { error.response.status !== undefined && error.response.statusText !== undefined ) { - let statusMessage: string; - if (error.response.data !== undefined) { - if (typeof error.response.data === "string") statusMessage = error.response.data; - else if (typeof error.response.data === "object" && error.response.data.statusMessage !== undefined) - statusMessage = error.response.data.statusMessage; - else if (typeof error.response.data === "object" && error.response.data.errorMessage !== undefined) - statusMessage = error.response.data.errorMessage; - else statusMessage = error.response.data.toString(); - } else statusMessage = ""; - switch (error.response.status) { case 400: - return new BadRequestError(error.response.status, error.response.statusText, statusMessage); + return new BadRequestError(error.response.status, error.response.statusText); case 404: - return new NotFoundError(error.response.status, error.response.statusText, statusMessage); + return new NotFoundError(error.response.status, error.response.statusText); default: - return new NetworkError(error.response.status, error.response.statusText, statusMessage); + return new NetworkError(error.response.status, error.response.statusText); } } else { if (error.message !== undefined) return new Error(error.message); diff --git a/packages/client/test/helper/FakerValidator.ts b/packages/client/test/helper/FakerValidator.ts index dc9c980..0dd57c2 100644 --- a/packages/client/test/helper/FakerValidator.ts +++ b/packages/client/test/helper/FakerValidator.ts @@ -1,44 +1,87 @@ import { ContractUtils } from "../../src"; +import * as cron from "node-cron"; import * as bodyParser from "body-parser"; +// @ts-ignore +import cors from "cors"; import * as http from "http"; // @ts-ignore -import * as express from "express"; +import e, * as express from "express"; import { body, validationResult } from "express-validator"; import { LinkCollection, LinkCollection__factory } from "del-osx-lib"; import { AddressZero, HashZero } from "@ethersproject/constants"; import { NonceManager } from "@ethersproject/experimental"; -import { BigNumber, BigNumberish, Signer } from "ethers"; +import { BigNumberish, Signer } from "ethers"; import { GasPriceManager } from "./GasPriceManager"; import { GanacheServer } from "./GanacheServer"; import { Deployment } from "./ContractDeployer"; -export class FakerValidator { - private readonly port: number; +export enum JobType { + REGISTER, + VOTE1, + VOTE2, + VOTE3, + COUNT, +} +export interface IJob { + type: JobType; + requestId: string; + registerData?: { + emailHash: string; + address: string; + signature: string; + }; +} +export class FakerValidator { + public static INIT_WAITING_SECONDS: number = 2; + public static INTERVAL_SECONDS: number = 12; protected _app: express.Application; - protected _server: http.Server | null = null; - protected _deployment: Deployment; + private readonly port: number; + private readonly _accounts: Signer[]; + private readonly _worker: Worker; - private _accounts: Signer[]; + private _jobList: IJob[] = []; constructor(port: number | string, deployment: Deployment) { if (typeof port === "string") this.port = parseInt(port, 10); else this.port = port; - this._app = express(); + this._app = e(); this._deployment = deployment; this._accounts = GanacheServer.accounts(); + this._worker = new Worker("*/1 * * * * *", this); + } + + private get validator1(): Signer { + return new NonceManager(new GasPriceManager(this._accounts[2])); + } + + private get validator2(): Signer { + return new NonceManager(new GasPriceManager(this._accounts[3])); + } + + private get validator3(): Signer { + return new NonceManager(new GasPriceManager(this._accounts[4])); } public start(): Promise { this._app.use(bodyParser.urlencoded({ extended: false })); this._app.use(bodyParser.json()); + this._app.use( + cors({ + allowedHeaders: "*", + credentials: true, + methods: "GET, POST", + origin: "*", + preflightContinue: false, + }) + ); this._app.get("/", [], this.getHealthStatus.bind(this)); this._app.post( @@ -62,11 +105,12 @@ export class FakerValidator { // Listen on provided this.port on this.address. return new Promise((resolve, reject) => { - // Create HTTP _server. + // Create HTTP server. this._server = http.createServer(this._app); this._server.on("error", reject); this._server.listen(this.port, async () => { await this.onStart(); + await this._worker.start(); resolve(); }); }); @@ -86,6 +130,8 @@ export class FakerValidator { public stop(): Promise { return new Promise(async (resolve, reject) => { + await this._worker.stop(); + await this._worker.waitForStop(); if (this._server != null) { this._server.close((err?) => { if (err) reject(err); @@ -108,27 +154,15 @@ export class FakerValidator { return contract; } - private get validator1(): Signer { - return new NonceManager(new GasPriceManager(this._accounts[2])); - } - - private get validator2(): Signer { - return new NonceManager(new GasPriceManager(this._accounts[3])); - } - - private get validator3(): Signer { - return new NonceManager(new GasPriceManager(this._accounts[4])); - } - private async getRequestId(emailHash: string, address: string, nonce: BigNumberish): Promise { while (true) { const id = ContractUtils.getRequestId(emailHash, address, nonce); - if (await (await this.getContract()).isAvailable(id)) return id; + if (await this.getContract().isAvailable(id)) return id; } } - private async getHealthStatus(req: express.Request, res: express.Response) { - return res.json("OK"); + private async getHealthStatus(_: express.Request, res: express.Response) { + return res.status(200).json("OK"); } private async postRequest(req: express.Request, res: express.Response) { @@ -160,7 +194,7 @@ export class FakerValidator { if (emailToAddress !== AddressZero) { return res.json( this.makeResponseData(402, undefined, { - message: "This email is already registered.", + message: "The email is already registered.", }) ); } @@ -169,50 +203,31 @@ export class FakerValidator { if (addressToEmail !== HashZero) { return res.json( this.makeResponseData(403, undefined, { - message: "This address is already registered.", + message: "The address is already registered.", }) ); } const requestId = await this.getRequestId(emailHash, address, nonce); - setTimeout(async () => { - await (await this.getContract()) - .connect(this.validator1) - .addRequest(requestId, emailHash, address, signature); - }, 1000); - - setTimeout(async () => { - await this.getContract() - .connect(this.validator1) - .voteRequest(requestId, BigNumber.from(1)); - await this.getContract() - .connect(this.validator2) - .voteRequest(requestId, BigNumber.from(1)); - }, 2000); - - setTimeout(async () => { - await this.getContract() - .connect(this.validator1) - .countVote(requestId); - }, 3000); - - try { - return res.json( - this.makeResponseData(200, { - requestId, - }) - ); - } catch (error) { - const message = error.message !== undefined ? error.message : "Failed save request"; - return res.json( - this.makeResponseData(800, undefined, { - message, - }) - ); - } + this.addJob({ + type: JobType.REGISTER, + requestId, + registerData: { + emailHash, + address, + signature, + }, + }); + + return res.json( + this.makeResponseData(200, { + requestId, + }) + ); } catch (error) { - const message = error.message !== undefined ? error.message : "Failed save request"; - console.error(message); + const message = + error instanceof Error && error.message !== undefined ? error.message : "Failed save request"; + // console.error(message); return res.json( this.makeResponseData(500, undefined, { message, @@ -220,4 +235,224 @@ export class FakerValidator { ); } } + + private async addRequest(requestId: string, emailHash: string, address: string, signature: string) { + try { + await this.getContract() + .connect(this.validator1) + .addRequest(requestId, emailHash, address, signature); + } catch (e) { + const message = + e instanceof Error && e.message !== undefined + ? e.message + : "Error when saving a request to the contract."; + console.error(message); + } + } + + private async voteAgreement(signer: Signer, requestId: string) { + try { + await (await this.getContract()).connect(signer).voteRequest(requestId, 1); + } catch (e) { + const message = e instanceof Error && e.message !== undefined ? e.message : "Error when calling contract"; + console.error(message); + } + } + + private async countVote(requestId: string) { + try { + await (await this.getContract()).connect(this.validator1).countVote(requestId); + } catch (e) { + const message = e instanceof Error && e.message !== undefined ? e.message : "Error when calling contract"; + console.error(message); + } + } + public async onWork() { + const job = this.getJob(); + if (job !== undefined) { + switch (job.type) { + case JobType.REGISTER: + console.info(`JobType.REGISTER ${job.requestId}`); + if (job.registerData !== undefined) { + await this.addRequest( + job.requestId, + job.registerData.emailHash, + job.registerData.address, + job.registerData.signature + ); + } + + this.addJob({ + type: JobType.VOTE1, + requestId: job.requestId, + }); + break; + + case JobType.VOTE1: + console.info(`JobType.VOTE1 ${job.requestId}`); + await this.voteAgreement(this.validator1, job.requestId); + + this.addJob({ + type: JobType.VOTE2, + requestId: job.requestId, + }); + break; + + case JobType.VOTE2: + console.info(`JobType.VOTE2 ${job.requestId}`); + await this.voteAgreement(this.validator2, job.requestId); + + this.addJob({ + type: JobType.VOTE3, + requestId: job.requestId, + }); + break; + + case JobType.VOTE3: + console.info(`JobType.VOTE3 ${job.requestId}`); + await this.voteAgreement(this.validator3, job.requestId); + + this.addJob({ + type: JobType.COUNT, + requestId: job.requestId, + }); + break; + + case JobType.COUNT: + const res = await (await this.getContract()).canCountVote(job.requestId); + if (res === 1) { + console.info(`JobType.COUNT, Counting is possible. ${job.requestId}`); + await this.countVote(job.requestId); + } else if (res === 2) { + console.info(`JobType.COUNT, Counting is impossible. ${job.requestId}`); + this.addJob({ + type: JobType.COUNT, + requestId: job.requestId, + }); + } else { + console.info(`JobType.COUNT, Counting has already been completed. ${job.requestId}`); + } + break; + } + } + } + + private addJob(job: IJob) { + this._jobList.push(job); + } + + private getJob(): IJob | undefined { + return this._jobList.shift(); + } +} + +export enum WorkerState { + NONE = 0, + STARTING = 2, + RUNNING = 3, + STOPPING = 4, + STOPPED = 5, +} + +export class Worker { + protected task: cron.ScheduledTask | null = null; + private readonly _validator: FakerValidator; + + protected state: WorkerState; + + protected expression: string; + + private is_working: boolean = false; + + constructor(expression: string, validator: FakerValidator) { + this._validator = validator; + this.expression = expression; + this.state = WorkerState.NONE; + } + + public async start() { + this.state = WorkerState.STARTING; + this.is_working = false; + this.task = cron.schedule(this.expression, this.workTask.bind(this)); + this.state = WorkerState.RUNNING; + await this.onStart(); + } + + public async onStart() { + // + } + + public async stop() { + this.state = WorkerState.STOPPING; + + if (!this.is_working) { + this.state = WorkerState.STOPPED; + } + + await this.onStop(); + } + + public async onStop() { + // + } + + private stopTask() { + if (this.task !== null) { + this.task.stop(); + this.task = null; + } + } + + public waitForStop(timeout: number = 60000): Promise { + return new Promise(resolve => { + const start = Math.floor(new Date().getTime() / 1000); + const wait = () => { + if (this.state === WorkerState.STOPPED) { + this.stopTask(); + resolve(true); + } else { + const now = Math.floor(new Date().getTime() / 1000); + if (now - start < timeout) setTimeout(wait, 10); + else { + this.stopTask(); + resolve(false); + } + } + }; + wait(); + }); + } + + public isRunning(): boolean { + return this.task !== null; + } + + public isWorking(): boolean { + return this.is_working; + } + + private async workTask() { + if (this.state === WorkerState.STOPPED) return; + if (this.is_working) return; + + this.is_working = true; + try { + await this.work(); + } catch (error) { + console.error({ + validatorIndex: "none", + method: "Worker.workTask()", + message: `Failed to execute a scheduler: ${error}`, + }); + } + this.is_working = false; + + if (this.state === WorkerState.STOPPING) { + this.state = WorkerState.STOPPED; + } + } + + protected async work() { + await this._validator.onWork(); + } } diff --git a/packages/client/test/helper/GanacheServer.ts b/packages/client/test/helper/GanacheServer.ts index f151898..a30ace6 100644 --- a/packages/client/test/helper/GanacheServer.ts +++ b/packages/client/test/helper/GanacheServer.ts @@ -124,7 +124,10 @@ export class GanacheServer { } public static createTestProvider(): JsonRpcProvider { - return new JsonRpcProvider(`http://localhost:${GanacheServer.PORT}`, GanacheServer.CHAIN_ID); + return new JsonRpcProvider(`http://localhost:${GanacheServer.PORT}`, { + chainId: GanacheServer.CHAIN_ID, + name: "bosagora_devnet", + }); } public static setTestProvider(provider: JsonRpcProvider) { diff --git a/packages/client/test/helper/Utils.ts b/packages/client/test/helper/Utils.ts index 4100df8..05e69bb 100644 --- a/packages/client/test/helper/Utils.ts +++ b/packages/client/test/helper/Utils.ts @@ -38,12 +38,6 @@ export class Utils { public static isNegative(value: string): boolean { return /^-?[0-9]\d*(\.\d+)?$/.test(value); } - - public static delay(interval: number): Promise { - return new Promise((resolve, reject) => { - setTimeout(resolve, interval); - }); - } } /** diff --git a/packages/client/test/methods.test.ts b/packages/client/test/methods.test.ts index 9e7022c..858dd5c 100644 --- a/packages/client/test/methods.test.ts +++ b/packages/client/test/methods.test.ts @@ -6,10 +6,11 @@ import { Client, Context, ContractUtils } from "../src"; import { AddRequestSteps } from "../src/interfaces"; import { BigNumber } from "ethers"; import { ContractDeployer, Deployment } from "./helper/ContractDeployer"; +import { RegisterSteps } from "../src/interfaces"; describe("SDK Client", () => { let deployment: Deployment; - const [, , validator1, validator2, , user1] = GanacheServer.accounts(); + const [, , validator1, validator2, , user1, user2] = GanacheServer.accounts(); let fakerValidator: FakerValidator; describe("SDK Client", () => { @@ -20,9 +21,7 @@ describe("SDK Client", () => { deployment = await ContractDeployer.deploy(); - GanacheServer.setTestWeb3Signer(user1); - - fakerValidator = new FakerValidator(7070, deployment); + fakerValidator = new FakerValidator(7080, deployment); await fakerValidator.start(); }); @@ -31,11 +30,13 @@ describe("SDK Client", () => { await fakerValidator.stop(); }); - describe("Method Check", () => { + describe("Method Check - Not Use FakerValidator", () => { let client: Client; beforeAll(async () => { + GanacheServer.setTestWeb3Signer(user1); const context = new Context(contextParamsLocalChain); client = new Client(context); + await client.methods.assignValidatorEndpoint(); }); const userEmail = "a@example.com"; @@ -52,10 +53,10 @@ describe("SDK Client", () => { expect(step.id).toMatch(/^0x[A-Fa-f0-9]{64}$/i); expect(step.email).toEqual(userEmail); expect(step.emailHash).toEqual(ContractUtils.sha256String(userEmail)); - expect(step.wallet).toEqual(await user1.getAddress()); + expect(step.address).toEqual(await user1.getAddress()); requestId = step.id; emailHash = step.emailHash; - address = step.wallet; + address = step.address; break; default: throw new Error("Unexpected step: " + JSON.stringify(step, null, 2)); @@ -76,10 +77,51 @@ describe("SDK Client", () => { await expect(await client.methods.toAddress(emailHash)).toEqual(address); await expect(await client.methods.toEmail(address)).toEqual(emailHash); }); + }); + + describe("Method Check - Use FakerValidator", () => { + let client: Client; + beforeAll(async () => { + GanacheServer.setTestWeb3Signer(user2); + const context = new Context(contextParamsLocalChain); + client = new Client(context); + await client.methods.assignValidatorEndpoint(); + }); + + it("Server Health Checking", async () => { + const isUp = await client.methods.isRelayUp(); + expect(isUp).toEqual(true); + }); + + const userEmail = "b@example.com"; + it("register", async () => { + for await (const step of client.methods.register(userEmail)) { + switch (step.key) { + case RegisterSteps.DOING: + expect(step.requestId).toMatch(/^0x[A-Fa-f0-9]{64}$/i); + expect(step.email).toEqual(userEmail); + expect(step.address).toEqual(await user2.getAddress()); + break; + case RegisterSteps.DONE: + expect(step.requestId).toMatch(/^0x[A-Fa-f0-9]{64}$/i); + expect(step.email).toEqual(userEmail); + expect(step.address).toEqual(await user2.getAddress()); + break; + default: + throw new Error("Unexpected step: " + JSON.stringify(step, null, 2)); + } + } + }); + + it("Wait", async () => { + await ContractUtils.delay(3000); + }); - it("getValidators", async () => { - const infos = await client.methods.getValidators(); - console.log(infos); + it("Check", async () => { + const emailHash = ContractUtils.sha256String(userEmail); + const address = await user2.getAddress(); + await expect(await client.methods.toAddress(emailHash)).toEqual(address); + await expect(await client.methods.toEmail(address)).toEqual(emailHash); }); }); }); diff --git a/yarn.lock b/yarn.lock index fea968f..57dfb53 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1841,6 +1841,13 @@ dependencies: "@types/node" "*" +"@types/cors@^2.8.8": + version "2.8.14" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.14.tgz#94eeb1c95eda6a8ab54870a3bf88854512f43a92" + integrity sha512-RXHUvNWYICtbP6s18PnOCaqToK8y14DnLd75c6HfyKf228dxy7pHNOQkxPtvXKp/hINFMDjbYzsj63nnpPMSRQ== + dependencies: + "@types/node" "*" + "@types/eslint-visitor-keys@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" @@ -1953,6 +1960,11 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== +"@types/node-cron@^3.0.1": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@types/node-cron/-/node-cron-3.0.8.tgz#c4d774b86bf8250d1e9046e08b17875c21ae64eb" + integrity sha512-+z5VrCvLwiJUohbRSgHdyZnHzAaLuD/E2bBANw+NQ1l05Crj8dIxb/kKK+OEqRitV2Wr/LYLuEBenGDsHZVV5Q== + "@types/node@*": version "20.6.0" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.6.0.tgz#9d7daa855d33d4efec8aea88cd66db1c2f0ebe16" @@ -3267,7 +3279,7 @@ core-util-is@1.0.2: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== -cors@^2.8.1: +cors@^2.8.1, cors@^2.8.5: version "2.8.5" resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== @@ -6896,6 +6908,13 @@ node-addon-api@^2.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32" integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA== +node-cron@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/node-cron/-/node-cron-3.0.2.tgz#bb0681342bd2dfb568f28e464031280e7f06bd01" + integrity sha512-iP8l0yGlNpE0e6q1o185yOApANRe47UPbLf4YxfbiNHt/RU5eBcGB/e0oudruheSf+LQeDMezqC5BVAb5wwRcQ== + dependencies: + uuid "8.3.2" + node-fetch@^2.6.12: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" @@ -9176,6 +9195,11 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== +uuid@8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + uuid@^3.3.2: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"