From 42f28f66c2651d549abafd3395e01f370ce502ea Mon Sep 17 00:00:00 2001 From: Michael Kim Date: Mon, 30 Oct 2023 13:40:59 +0900 Subject: [PATCH] [Relay] Separate routes by task --- packages/relay/package.json | 10 +- packages/relay/src/DefaultServer.ts | 14 +- packages/relay/src/contract/Signers.ts | 78 +++++ packages/relay/src/routers/DefaultRouter.ts | 193 +---------- packages/relay/src/routers/PaymentRouter.ts | 305 ++++++++++++++++++ .../{UserInfo.test.ts => Payment.test.ts} | 6 +- 6 files changed, 409 insertions(+), 197 deletions(-) create mode 100644 packages/relay/src/contract/Signers.ts create mode 100644 packages/relay/src/routers/PaymentRouter.ts rename packages/relay/test/{UserInfo.test.ts => Payment.test.ts} (99%) diff --git a/packages/relay/package.json b/packages/relay/package.json index a0ef9edd..994bf518 100644 --- a/packages/relay/package.json +++ b/packages/relay/package.json @@ -17,11 +17,11 @@ "start": "NODE_ENV=production hardhat run src/main.ts --network production_net", "formatting:check": "prettier '**/*.{json,sol,ts,js,md}' -c", "formatting:write": "prettier '**/*.{json,sol,ts,js,md}' --write", - "debug:Endpoints": "NODE_ENV=test hardhat test test/Endpoints.test.ts", - "debug:Config": "NODE_ENV=test hardhat test test/Config.test.ts", - "debug:Shop": "NODE_ENV=test hardhat test test/Shop.test.ts", - "debug:ShopWithdraw": "NODE_ENV=test hardhat test test/ShopWithdraw.test.ts", - "debug:UserInfo": "NODE_ENV=test hardhat test test/UserInfo.test.ts" + "test:Endpoints": "NODE_ENV=test hardhat test test/Endpoints.test.ts", + "test:Config": "NODE_ENV=test hardhat test test/Config.test.ts", + "test:Shop": "NODE_ENV=test hardhat test test/Shop.test.ts", + "test:ShopWithdraw": "NODE_ENV=test hardhat test test/ShopWithdraw.test.ts", + "test:Payment": "NODE_ENV=test hardhat test test/Payment.test.ts" }, "repository": { "type": "git", diff --git a/packages/relay/src/DefaultServer.ts b/packages/relay/src/DefaultServer.ts index 0430cb6f..8fcdbf0e 100644 --- a/packages/relay/src/DefaultServer.ts +++ b/packages/relay/src/DefaultServer.ts @@ -5,6 +5,9 @@ import { cors_options } from "./option/cors"; import { DefaultRouter } from "./routers/DefaultRouter"; import { WebService } from "./service/WebService"; +import { RelaySigners } from "./contract/Signers"; +import { PaymentRouter } from "./routers/PaymentRouter"; + export class DefaultServer extends WebService { /** * The configuration of the database @@ -12,7 +15,9 @@ export class DefaultServer extends WebService { */ private readonly config: Config; - public readonly wallet_router: DefaultRouter; + public readonly defaultRouter: DefaultRouter; + public readonly paymentRouter: PaymentRouter; + public readonly relaySigners: RelaySigners; /** * Constructor @@ -22,7 +27,9 @@ export class DefaultServer extends WebService { super(config.server.port, config.server.address); this.config = config; - this.wallet_router = new DefaultRouter(this, this.config); + this.relaySigners = new RelaySigners(this.config); + this.defaultRouter = new DefaultRouter(this, this.config, this.relaySigners); + this.paymentRouter = new PaymentRouter(this, this.config, this.relaySigners); } /** @@ -35,7 +42,8 @@ export class DefaultServer extends WebService { this.app.use(bodyParser.json({ limit: "1mb" })); this.app.use(cors(cors_options)); - this.wallet_router.registerRoutes(); + this.defaultRouter.registerRoutes(); + this.paymentRouter.registerRoutes(); return super.start(); } diff --git a/packages/relay/src/contract/Signers.ts b/packages/relay/src/contract/Signers.ts new file mode 100644 index 00000000..e41192dd --- /dev/null +++ b/packages/relay/src/contract/Signers.ts @@ -0,0 +1,78 @@ +import "@nomiclabs/hardhat-ethers"; + +import { Signer, Wallet } from "ethers"; +import { Config } from "../common/Config"; + +import { NonceManager } from "@ethersproject/experimental"; +import { ContractUtils } from "../utils/ContractUtils"; +import { GasPriceManager } from "./GasPriceManager"; + +import * as hre from "hardhat"; + +export interface ISignerItem { + index: number; + signer: Signer; + using: boolean; +} + +export class RelaySigners { + private readonly _config: Config; + private readonly _signers: ISignerItem[]; + constructor(config: Config) { + this._config = config; + + let idx = 0; + this._signers = this._config.relay.managerKeys.map((m) => { + return { + index: idx++, + signer: new Wallet(m, hre.ethers.provider) as Signer, + using: false, + }; + }); + } + + /*** + * 트팬잭션을 중계할 때 사용될 서명자 + * @private + */ + public async getSigner(): Promise { + let signerItem: ISignerItem | undefined; + let done = false; + + const startTime = ContractUtils.getTimeStamp(); + while (done) { + for (signerItem of this._signers) { + if (!signerItem.using) { + signerItem.using = true; + done = true; + break; + } + } + if (ContractUtils.getTimeStamp() - startTime > 10) break; + await ContractUtils.delay(1000); + } + + if (signerItem !== undefined) { + signerItem.using = true; + signerItem.signer = new NonceManager( + new GasPriceManager(new Wallet(this._config.relay.managerKeys[signerItem.index], hre.ethers.provider)) + ); + } else { + signerItem = this._signers[0]; + signerItem.using = true; + signerItem.signer = new NonceManager( + new GasPriceManager(new Wallet(this._config.relay.managerKeys[signerItem.index], hre.ethers.provider)) + ); + } + + return signerItem; + } + + /*** + * 트팬잭션을 중계할 때 사용될 서명자 + * @private + */ + public releaseSigner(signer: ISignerItem) { + signer.using = false; + } +} diff --git a/packages/relay/src/routers/DefaultRouter.ts b/packages/relay/src/routers/DefaultRouter.ts index e5d36ba7..24f8abd1 100644 --- a/packages/relay/src/routers/DefaultRouter.ts +++ b/packages/relay/src/routers/DefaultRouter.ts @@ -1,24 +1,17 @@ import { CurrencyRate, Ledger, PhoneLinkCollection, ShopCollection, Token } from "../../typechain-types"; import { Config } from "../common/Config"; import { logger } from "../common/Logger"; -import { GasPriceManager } from "../contract/GasPriceManager"; import { WebService } from "../service/WebService"; +import { LoyaltyType } from "../types"; import { ContractUtils } from "../utils/ContractUtils"; import { Validation } from "../validation"; -import { NonceManager } from "@ethersproject/experimental"; -import { BigNumber, Signer, Wallet } from "ethers"; +import { BigNumber } from "ethers"; import { body, query, validationResult } from "express-validator"; import * as hre from "hardhat"; import express from "express"; -import { LoyaltyType } from "../types"; - -interface ISignerItem { - index: number; - signer: Signer; - using: boolean; -} +import { ISignerItem, RelaySigners } from "../contract/Signers"; export class DefaultRouter { /** @@ -33,7 +26,7 @@ export class DefaultRouter { */ private readonly _config: Config; - private readonly _signers: ISignerItem[]; + private readonly _relaySigners: RelaySigners; /** * ERC20 토큰 컨트랙트 @@ -70,18 +63,11 @@ export class DefaultRouter { * @param service WebService * @param config Configuration */ - constructor(service: WebService, config: Config) { + constructor(service: WebService, config: Config, relaySigners: RelaySigners) { this._web_service = service; this._config = config; - let idx = 0; - this._signers = this._config.relay.managerKeys.map((m) => { - return { - index: idx++, - signer: new Wallet(m, hre.ethers.provider) as Signer, - using: false, - }; - }); + this._relaySigners = relaySigners; } private get app(): express.Application { @@ -93,36 +79,7 @@ export class DefaultRouter { * @private */ private async getRelaySigner(): Promise { - let signerItem: ISignerItem | undefined; - let done = false; - - const startTime = ContractUtils.getTimeStamp(); - while (done) { - for (signerItem of this._signers) { - if (!signerItem.using) { - signerItem.using = true; - done = true; - break; - } - } - if (ContractUtils.getTimeStamp() - startTime > 10) break; - await ContractUtils.delay(1000); - } - - if (signerItem !== undefined) { - signerItem.using = true; - signerItem.signer = new NonceManager( - new GasPriceManager(new Wallet(this._config.relay.managerKeys[signerItem.index], hre.ethers.provider)) - ); - } else { - signerItem = this._signers[0]; - signerItem.using = true; - signerItem.signer = new NonceManager( - new GasPriceManager(new Wallet(this._config.relay.managerKeys[signerItem.index], hre.ethers.provider)) - ); - } - - return signerItem; + return this._relaySigners.getSigner(); } /*** @@ -362,21 +319,6 @@ export class DefaultRouter { ], this.shop_closeWithdrawal.bind(this) ); - - this.app.get( - "/user/balance", - [query("account").exists().trim().isEthereumAddress()], - this.user_balance.bind(this) - ); - - this.app.post( - "/payment/info", - [body("accessKey").exists()], - [body("account").exists().trim().isEthereumAddress()], - [body("amount").exists().custom(Validation.isAmount)], - [body("currency").exists()], - this.payment_info.bind(this) - ); } private async getHealthStatus(req: express.Request, res: express.Response) { @@ -874,125 +816,4 @@ export class DefaultRouter { this.releaseRelaySigner(signerItem); } } - - /** - * 사용자 정보 / 로열티 종류와 잔고를 제공하는 엔드포인트 - * GET /user/balance - * @private - */ - private async user_balance(req: express.Request, res: express.Response) { - logger.http(`GET /user/balance`); - - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(200).json( - this.makeResponseData(501, undefined, { - message: "Failed to check the validity of parameters.", - validation: errors.array(), - }) - ); - } - - try { - const account: string = String(req.query.account).trim(); - const loyaltyType = await (await this.getLedgerContract()).loyaltyTypeOf(account); - const balance = - loyaltyType === LoyaltyType.POINT - ? await (await this.getLedgerContract()).pointBalanceOf(account) - : await (await this.getLedgerContract()).tokenBalanceOf(account); - return res - .status(200) - .json(this.makeResponseData(200, { account, loyaltyType, balance: balance.toString() })); - } catch (error: any) { - let message = ContractUtils.cacheEVMError(error as any); - if (message === "") message = "Failed /user/balance"; - logger.error(`GET /user/balance :`, message); - return res.status(200).json( - this.makeResponseData(500, undefined, { - message, - }) - ); - } - } - - /** - * 결제 / 결제정보를 제공한다 - * GET /payment/info - * @private - */ - private async payment_info(req: express.Request, res: express.Response) { - logger.http(`GET /payment/info`); - - const errors = validationResult(req); - if (!errors.isEmpty()) { - return res.status(200).json( - this.makeResponseData(501, undefined, { - message: "Failed to check the validity of parameters.", - validation: errors.array(), - }) - ); - } - - try { - const accessKey: string = String(req.body.accessKey).trim(); - if (accessKey !== this._config.relay.accessKey) { - return res.json( - this.makeResponseData(400, undefined, { - message: "The access key entered is not valid.", - }) - ); - } - - const account: string = String(req.body.account).trim(); - const amount: BigNumber = BigNumber.from(req.body.amount); - const currency: string = String(req.body.currency).trim(); - const loyaltyType = await (await this.getLedgerContract()).loyaltyTypeOf(account); - - const feeRate = await (await this.getLedgerContract()).fee(); - const rate = await (await this.getCurrencyRateContract()).get(currency.toLowerCase()); - const multiple = await (await this.getCurrencyRateContract()).MULTIPLE(); - - let balance: BigNumber; - let purchaseAmount: BigNumber; - let feeAmount: BigNumber; - let totalAmount: BigNumber; - - if (loyaltyType === LoyaltyType.POINT) { - balance = await (await this.getLedgerContract()).pointBalanceOf(account); - purchaseAmount = amount.mul(rate).div(multiple); - feeAmount = purchaseAmount.mul(feeRate).div(100); - totalAmount = purchaseAmount.add(feeAmount); - } else { - balance = await (await this.getLedgerContract()).tokenBalanceOf(account); - const symbol = await (await this.getTokenContract()).symbol(); - const tokenRate = await (await this.getCurrencyRateContract()).get(symbol); - purchaseAmount = amount.mul(rate).div(tokenRate); - feeAmount = purchaseAmount.mul(feeRate).div(100); - totalAmount = purchaseAmount.add(feeAmount); - } - - return res.status(200).json( - this.makeResponseData(200, { - account, - loyaltyType, - balance: balance.toString(), - purchaseAmount: purchaseAmount.toString(), - feeAmount: feeAmount.toString(), - totalAmount: totalAmount.toString(), - amount: amount.toString(), - currency, - feeRate: feeRate / 100, - }) - ); - } catch (error: any) { - let message = ContractUtils.cacheEVMError(error as any); - if (message === "") message = "Failed /payment/info"; - logger.error(`GET /payment/info :`, message); - return res.status(200).json( - this.makeResponseData(500, undefined, { - message, - }) - ); - } - } } diff --git a/packages/relay/src/routers/PaymentRouter.ts b/packages/relay/src/routers/PaymentRouter.ts new file mode 100644 index 00000000..838b8515 --- /dev/null +++ b/packages/relay/src/routers/PaymentRouter.ts @@ -0,0 +1,305 @@ +import { CurrencyRate, Ledger, PhoneLinkCollection, ShopCollection, Token } from "../../typechain-types"; +import { Config } from "../common/Config"; +import { logger } from "../common/Logger"; +import { WebService } from "../service/WebService"; +import { LoyaltyType } from "../types"; +import { ContractUtils } from "../utils/ContractUtils"; +import { Validation } from "../validation"; + +import { BigNumber } from "ethers"; +import { body, query, validationResult } from "express-validator"; +import * as hre from "hardhat"; + +import express from "express"; +import { ISignerItem, RelaySigners } from "../contract/Signers"; + +export class PaymentRouter { + /** + * + * @private + */ + private _web_service: WebService; + + /** + * The configuration of the database + * @private + */ + private readonly _config: Config; + + private readonly _relaySigners: RelaySigners; + + /** + * ERC20 토큰 컨트랙트 + * @private + */ + private _tokenContract: Token | undefined; + + /** + * 사용자의 원장 컨트랙트 + * @private + */ + private _ledgerContract: Ledger | undefined; + + /** + * 사용자의 원장 컨트랙트 + * @private + */ + private _shopContract: ShopCollection | undefined; + + /** + * 이메일 지갑주소 링크 컨트랙트 + * @private + */ + private _phoneLinkerContract: PhoneLinkCollection | undefined; + + /** + * 환률 컨트랙트 + * @private + */ + private _currencyRateContract: CurrencyRate | undefined; + + /** + * + * @param service WebService + * @param config Configuration + */ + constructor(service: WebService, config: Config, relaySigners: RelaySigners) { + this._web_service = service; + this._config = config; + + this._relaySigners = relaySigners; + } + + private get app(): express.Application { + return this._web_service.app; + } + + /*** + * 트팬잭션을 중계할 때 사용될 서명자 + * @private + */ + private async getRelaySigner(): Promise { + return this._relaySigners.getSigner(); + } + + /*** + * 트팬잭션을 중계할 때 사용될 서명자 + * @private + */ + private releaseRelaySigner(signer: ISignerItem) { + signer.using = false; + } + + /** + * ERC20 토큰 컨트랙트를 리턴한다. + * 컨트랙트의 객체가 생성되지 않았다면 컨트랙트 주소를 이용하여 컨트랙트 객체를 생성한 후 반환한다. + * @private + */ + private async getTokenContract(): Promise { + if (this._tokenContract === undefined) { + const tokenFactory = await hre.ethers.getContractFactory("Token"); + this._tokenContract = tokenFactory.attach(this._config.contracts.tokenAddress); + } + return this._tokenContract; + } + + /** + * 사용자의 원장 컨트랙트를 리턴한다. + * 컨트랙트의 객체가 생성되지 않았다면 컨트랙트 주소를 이용하여 컨트랙트 객체를 생성한 후 반환한다. + * @private + */ + private async getLedgerContract(): Promise { + if (this._ledgerContract === undefined) { + const ledgerFactory = await hre.ethers.getContractFactory("Ledger"); + this._ledgerContract = ledgerFactory.attach(this._config.contracts.ledgerAddress); + } + return this._ledgerContract; + } + + private async getShopContract(): Promise { + if (this._shopContract === undefined) { + const shopFactory = await hre.ethers.getContractFactory("ShopCollection"); + this._shopContract = shopFactory.attach(this._config.contracts.shopAddress); + } + return this._shopContract; + } + + /** + * 이메일 지갑주소 링크 컨트랙트를 리턴한다. + * 컨트랙트의 객체가 생성되지 않았다면 컨트랙트 주소를 이용하여 컨트랙트 객체를 생성한 후 반환한다. + * @private + */ + private async getPhoneLinkerContract(): Promise { + if (this._phoneLinkerContract === undefined) { + const linkCollectionFactory = await hre.ethers.getContractFactory("PhoneLinkCollection"); + this._phoneLinkerContract = linkCollectionFactory.attach(this._config.contracts.phoneLinkerAddress); + } + return this._phoneLinkerContract; + } + + /** + * 환률 컨트랙트를 리턴한다. + * 컨트랙트의 객체가 생성되지 않았다면 컨트랙트 주소를 이용하여 컨트랙트 객체를 생성한 후 반환한다. + * @private + */ + private async getCurrencyRateContract(): Promise { + if (this._currencyRateContract === undefined) { + const factory = await hre.ethers.getContractFactory("CurrencyRate"); + this._currencyRateContract = factory.attach(this._config.contracts.currencyRateAddress); + } + return this._currencyRateContract; + } + + /** + * Make the response data + * @param code The result code + * @param data The result data + * @param error The error + * @private + */ + private makeResponseData(code: number, data: any, error?: any): any { + return { + code, + data, + error, + }; + } + + public registerRoutes() { + this.app.get( + "/payment/balance", + [query("account").exists().trim().isEthereumAddress()], + this.user_balance.bind(this) + ); + + this.app.post( + "/payment/info", + [body("accessKey").exists()], + [body("account").exists().trim().isEthereumAddress()], + [body("amount").exists().custom(Validation.isAmount)], + [body("currency").exists()], + this.payment_info.bind(this) + ); + } + + /** + * 사용자 정보 / 로열티 종류와 잔고를 제공하는 엔드포인트 + * GET /payment/balance + * @private + */ + private async user_balance(req: express.Request, res: express.Response) { + logger.http(`GET /payment/balance`); + + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(200).json( + this.makeResponseData(501, undefined, { + message: "Failed to check the validity of parameters.", + validation: errors.array(), + }) + ); + } + + try { + const account: string = String(req.query.account).trim(); + const loyaltyType = await (await this.getLedgerContract()).loyaltyTypeOf(account); + const balance = + loyaltyType === LoyaltyType.POINT + ? await (await this.getLedgerContract()).pointBalanceOf(account) + : await (await this.getLedgerContract()).tokenBalanceOf(account); + return res + .status(200) + .json(this.makeResponseData(200, { account, loyaltyType, balance: balance.toString() })); + } catch (error: any) { + let message = ContractUtils.cacheEVMError(error as any); + if (message === "") message = "Failed /payment/balance"; + logger.error(`GET /payment/balance :`, message); + return res.status(200).json( + this.makeResponseData(500, undefined, { + message, + }) + ); + } + } + + /** + * 결제 / 결제정보를 제공한다 + * GET /payment/info + * @private + */ + private async payment_info(req: express.Request, res: express.Response) { + logger.http(`GET /payment/info`); + + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(200).json( + this.makeResponseData(501, undefined, { + message: "Failed to check the validity of parameters.", + validation: errors.array(), + }) + ); + } + + try { + const accessKey: string = String(req.body.accessKey).trim(); + if (accessKey !== this._config.relay.accessKey) { + return res.json( + this.makeResponseData(400, undefined, { + message: "The access key entered is not valid.", + }) + ); + } + + const account: string = String(req.body.account).trim(); + const amount: BigNumber = BigNumber.from(req.body.amount); + const currency: string = String(req.body.currency).trim(); + const loyaltyType = await (await this.getLedgerContract()).loyaltyTypeOf(account); + + const feeRate = await (await this.getLedgerContract()).fee(); + const rate = await (await this.getCurrencyRateContract()).get(currency.toLowerCase()); + const multiple = await (await this.getCurrencyRateContract()).MULTIPLE(); + + let balance: BigNumber; + let purchaseAmount: BigNumber; + let feeAmount: BigNumber; + let totalAmount: BigNumber; + + if (loyaltyType === LoyaltyType.POINT) { + balance = await (await this.getLedgerContract()).pointBalanceOf(account); + purchaseAmount = amount.mul(rate).div(multiple); + feeAmount = purchaseAmount.mul(feeRate).div(100); + totalAmount = purchaseAmount.add(feeAmount); + } else { + balance = await (await this.getLedgerContract()).tokenBalanceOf(account); + const symbol = await (await this.getTokenContract()).symbol(); + const tokenRate = await (await this.getCurrencyRateContract()).get(symbol); + purchaseAmount = amount.mul(rate).div(tokenRate); + feeAmount = purchaseAmount.mul(feeRate).div(100); + totalAmount = purchaseAmount.add(feeAmount); + } + + return res.status(200).json( + this.makeResponseData(200, { + account, + loyaltyType, + balance: balance.toString(), + purchaseAmount: purchaseAmount.toString(), + feeAmount: feeAmount.toString(), + totalAmount: totalAmount.toString(), + amount: amount.toString(), + currency, + feeRate: feeRate / 100, + }) + ); + } catch (error: any) { + let message = ContractUtils.cacheEVMError(error as any); + if (message === "") message = "Failed /payment/info"; + logger.error(`GET /payment/info :`, message); + return res.status(200).json( + this.makeResponseData(500, undefined, { + message, + }) + ); + } + } +} diff --git a/packages/relay/test/UserInfo.test.ts b/packages/relay/test/Payment.test.ts similarity index 99% rename from packages/relay/test/UserInfo.test.ts rename to packages/relay/test/Payment.test.ts index d6d3d057..3b31fc49 100644 --- a/packages/relay/test/UserInfo.test.ts +++ b/packages/relay/test/Payment.test.ts @@ -410,7 +410,7 @@ describe("Test of Server", function () { it("Get user's balance", async () => { const url = URI(serverURL) - .directory("user/balance") + .directory("payment/balance") .addQuery("account", users[purchase.userIndex].address) .toString(); const response = await client.get(url); @@ -464,7 +464,7 @@ describe("Test of Server", function () { it("Get user's balance", async () => { const url = URI(serverURL) - .directory("user/balance") + .directory("payment/balance") .addQuery("account", users[purchase.userIndex].address) .toString(); const response = await client.get(url); @@ -490,7 +490,7 @@ describe("Test of Server", function () { assert.deepStrictEqual(response.data.code, 200); assert.ok(response.data.data !== undefined); - console.log(response.data.data); + const tokenAmount = pointAmount.mul(multiple).div(price); const purchaseAmount2 = amount2.mul(1000).mul(multiple).div(price); const feeAmount2 = purchaseAmount2.mul(5).div(100);