diff --git a/packages/client/package.json b/packages/client/package.json index 8e597f0..3f44e58 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -49,9 +49,12 @@ } ], "devDependencies": { + "@ethersproject/experimental": "^5.7.0", "@size-limit/preset-small-lib": "^7.0.8", "@types/express": "^4.17.8", "bigint-buffer": "^1.1.5", + "express": "^4.17.1", + "express-validator": "^6.14.0", "ganache": "^7.9.1", "glob": "^8.0.3", "husky": "^7.0.4", diff --git a/packages/client/test/helper/FakerValidator.ts b/packages/client/test/helper/FakerValidator.ts new file mode 100644 index 0000000..dc9c980 --- /dev/null +++ b/packages/client/test/helper/FakerValidator.ts @@ -0,0 +1,223 @@ +import { ContractUtils } from "../../src"; + +import * as bodyParser from "body-parser"; +import * as http from "http"; +// @ts-ignore +import * 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 { GasPriceManager } from "./GasPriceManager"; +import { GanacheServer } from "./GanacheServer"; +import { Deployment } from "./ContractDeployer"; + +export class FakerValidator { + private readonly port: number; + + protected _app: express.Application; + + protected _server: http.Server | null = null; + + protected _deployment: Deployment; + + private _accounts: Signer[]; + + constructor(port: number | string, deployment: Deployment) { + if (typeof port === "string") this.port = parseInt(port, 10); + else this.port = port; + + this._app = express(); + this._deployment = deployment; + this._accounts = GanacheServer.accounts(); + } + + public start(): Promise { + this._app.use(bodyParser.urlencoded({ extended: false })); + this._app.use(bodyParser.json()); + + this._app.get("/", [], this.getHealthStatus.bind(this)); + this._app.post( + "/request", + [ + body("email") + .exists() + .trim() + .isEmail(), + body("address") + .exists() + .trim() + .isEthereumAddress(), + body("signature") + .exists() + .trim() + .matches(/^(0x)[0-9a-f]{130}$/i), + ], + this.postRequest.bind(this) + ); + + // Listen on provided this.port on this.address. + return new Promise((resolve, reject) => { + // Create HTTP _server. + this._server = http.createServer(this._app); + this._server.on("error", reject); + this._server.listen(this.port, async () => { + await this.onStart(); + resolve(); + }); + }); + } + + private async onStart() { + await this.getContract() + .connect(this.validator1) + .updateEndpoint(`http://127.0.0.1:${this.port}`); + await this.getContract() + .connect(this.validator2) + .updateEndpoint(`http://127.0.0.1:${this.port}`); + await this.getContract() + .connect(this.validator3) + .updateEndpoint(`http://127.0.0.1:${this.port}`); + } + + public stop(): Promise { + return new Promise(async (resolve, reject) => { + if (this._server != null) { + this._server.close((err?) => { + if (err) reject(err); + else resolve(); + }); + } else resolve(); + }); + } + + private makeResponseData(code: number, data: any, error?: any): any { + return { + code, + data, + error, + }; + } + + private getContract(): LinkCollection { + const contract = LinkCollection__factory.connect(this._deployment.linkCollection.address, this.validator1); + 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; + } + } + + private async getHealthStatus(req: express.Request, res: express.Response) { + return res.json("OK"); + } + + private async postRequest(req: express.Request, res: express.Response) { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.json( + this.makeResponseData(400, undefined, { + message: "Failed to check the validity of parameters.", + validation: errors.array(), + }) + ); + } + + try { + const email: string = String(req.body.email).trim(); // 이메일 해시 + const address: string = String(req.body.address).trim(); // 주소 + const signature: string = String(req.body.signature).trim(); // 서명 + const nonce = await (await this.getContract()).nonceOf(address); + const emailHash = ContractUtils.sha256String(email); + if (!ContractUtils.verifyRequestData(address, email, nonce, signature)) { + return res.json( + this.makeResponseData(401, undefined, { + message: "The signature value entered is not valid.", + }) + ); + } + + const emailToAddress: string = await this.getContract().toAddress(emailHash); + if (emailToAddress !== AddressZero) { + return res.json( + this.makeResponseData(402, undefined, { + message: "This email is already registered.", + }) + ); + } + + const addressToEmail: string = await this.getContract().toEmail(address); + if (addressToEmail !== HashZero) { + return res.json( + this.makeResponseData(403, undefined, { + message: "This 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, + }) + ); + } + } catch (error) { + const message = error.message !== undefined ? error.message : "Failed save request"; + console.error(message); + return res.json( + this.makeResponseData(500, undefined, { + message, + }) + ); + } + } +} diff --git a/packages/client/test/methods.test.ts b/packages/client/test/methods.test.ts index 3f45ff2..b23d662 100644 --- a/packages/client/test/methods.test.ts +++ b/packages/client/test/methods.test.ts @@ -1,6 +1,7 @@ import { Server } from "ganache"; import { GanacheServer } from "./helper/GanacheServer"; import { contextParamsLocalChain } from "./helper/constants"; +import { FakerValidator } from "./helper/FakerValidator"; import { Client, Context, ContractUtils } from "../src"; import { AddRequestSteps } from "../src/interfaces"; import { BigNumber } from "ethers"; @@ -9,6 +10,7 @@ import { ContractDeployer, Deployment } from "./helper/ContractDeployer"; describe("SDK Client", () => { let deployment: Deployment; const [, , validator1, validator2, , user1] = GanacheServer.accounts(); + let fakerValidator: FakerValidator; describe("SDK Client", () => { let server: Server; @@ -19,10 +21,14 @@ describe("SDK Client", () => { deployment = await ContractDeployer.deploy(); GanacheServer.setTestWeb3Signer(user1); + + fakerValidator = new FakerValidator(7070, deployment); + await fakerValidator.start(); }); afterAll(async () => { await server.close(); + await fakerValidator.stop(); }); describe("Method Check", () => { diff --git a/yarn.lock b/yarn.lock index 62c680e..14ceb53 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1123,6 +1123,15 @@ "@ethersproject/properties" "^5.7.0" "@ethersproject/transactions" "^5.7.0" +"@ethersproject/experimental@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/experimental/-/experimental-5.7.0.tgz#9759639434d37beaedfd8acab6f3af7db246b92d" + integrity sha512-DWvhuw7Dg8JPyhMbh/CNYOwsTLjXRx/HGkacIL5rBocG8jJC0kmixwoK/J3YblO4vtcyBLMa+sV74RJZK2iyHg== + dependencies: + "@ethersproject/web" "^5.7.0" + ethers "^5.7.0" + scrypt-js "3.0.1" + "@ethersproject/hash@5.7.0", "@ethersproject/hash@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.7.0.tgz#eb7aca84a588508369562e16e514b539ba5240a7" @@ -4366,7 +4375,15 @@ expect@^25.5.0: jest-message-util "^25.5.0" jest-regex-util "^25.2.6" -express@^4.14.0: +express-validator@^6.14.0: + version "6.15.0" + resolved "https://registry.yarnpkg.com/express-validator/-/express-validator-6.15.0.tgz#5e4601428960b0d66f5f4ae09cb32ed2077374a4" + integrity sha512-r05VYoBL3i2pswuehoFSy+uM8NBuVaY7avp5qrYjQBDzagx2Z5A77FZqPT8/gNLF3HopWkIzaTFaC4JysWXLqg== + dependencies: + lodash "^4.17.21" + validator "^13.9.0" + +express@^4.14.0, express@^4.17.1: version "4.18.2" resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== @@ -6430,7 +6447,7 @@ lodash.sortby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== -lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19: +lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -9186,6 +9203,11 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +validator@^13.9.0: + version "13.11.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.11.0.tgz#23ab3fd59290c61248364eabf4067f04955fbb1b" + integrity sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ== + varint@^5.0.0: version "5.0.2" resolved "https://registry.yarnpkg.com/varint/-/varint-5.0.2.tgz#5b47f8a947eb668b848e034dcfa87d0ff8a7f7a4"