From 5c92bdeb84d9e53ba1497ec314bd77d3b1667dc8 Mon Sep 17 00:00:00 2001 From: Michael Kim Date: Tue, 7 Nov 2023 17:51:31 +0900 Subject: [PATCH] [Relay] Implement a process that responds to KIOSK --- packages/relay/config/config.yaml | 1 + packages/relay/config/config_test.yaml | 1 + packages/relay/env/.env.sample | 1 + packages/relay/src/common/Config.ts | 5 + packages/relay/src/routers/PaymentRouter.ts | 831 ++++++++++++++---- packages/relay/src/storage/RelayStorage.ts | 67 +- packages/relay/src/storage/mapper/payment.xml | 53 +- packages/relay/src/storage/mapper/table.xml | 16 +- packages/relay/src/types/index.ts | 54 +- packages/relay/src/utils/ContractUtils.ts | 9 + packages/relay/src/utils/Utils.ts | 57 ++ packages/relay/test/Payment.test.ts | 87 +- .../relay/test/helper/FakerCallbackServer.ts | 74 ++ packages/relay/test/helper/Utility.ts | 1 - 14 files changed, 1019 insertions(+), 238 deletions(-) create mode 100644 packages/relay/test/helper/FakerCallbackServer.ts diff --git a/packages/relay/config/config.yaml b/packages/relay/config/config.yaml index 5904f6f0..c5dba9ac 100644 --- a/packages/relay/config/config.yaml +++ b/packages/relay/config/config.yaml @@ -39,6 +39,7 @@ relay: - "${MANAGER_KEY5}" accessKey: "${ACCESS_KEY}" certifier: "${CERTIFIER_KEY}" + callbackEndpoint: "${CALLBACK_ENDPOINT}" contracts: tokenAddress : "${TOKEN_CONTRACT_ADDRESS}" diff --git a/packages/relay/config/config_test.yaml b/packages/relay/config/config_test.yaml index 5904f6f0..c5dba9ac 100644 --- a/packages/relay/config/config_test.yaml +++ b/packages/relay/config/config_test.yaml @@ -39,6 +39,7 @@ relay: - "${MANAGER_KEY5}" accessKey: "${ACCESS_KEY}" certifier: "${CERTIFIER_KEY}" + callbackEndpoint: "${CALLBACK_ENDPOINT}" contracts: tokenAddress : "${TOKEN_CONTRACT_ADDRESS}" diff --git a/packages/relay/env/.env.sample b/packages/relay/env/.env.sample index 0efcaf4e..d268e5d0 100644 --- a/packages/relay/env/.env.sample +++ b/packages/relay/env/.env.sample @@ -64,6 +64,7 @@ MANAGER_KEY5=0xc645ef34a5428f3b00f2d4b8cc647380f0b939d4ae5ccb7a04ead77ab0a8628d CERTIFIER_KEY=0x8a840e693d52e57ca028621a8692c42d2d9c368c6fe4d0c1027408b87827e6ef ACCESS_KEY=0x2c93e943c0d7f6f1a42f53e116c52c40fe5c1b428506dc04b290f2a77580a342 +CALLBACK_ENDPOINT=http://127.0.0.1:3400/callback TOKEN_CONTRACT_ADDRESS=0xFDa3d1ff3C570c2f76c2157Ef7A8640A75794eD9 LEDGER_CONTRACT_ADDRESS=0x12c316e0358d914A211A3d477db912A503cFCc21 diff --git a/packages/relay/src/common/Config.ts b/packages/relay/src/common/Config.ts index fc628415..c1ef2440 100644 --- a/packages/relay/src/common/Config.ts +++ b/packages/relay/src/common/Config.ts @@ -281,6 +281,7 @@ export class RelayConfig implements IRelayConfig { public managerKeys: string[]; public accessKey: string; public certifierKey: string; + public callbackEndpoint: string; /** * Constructor @@ -291,6 +292,7 @@ export class RelayConfig implements IRelayConfig { this.managerKeys = defaults.managerKeys; this.accessKey = defaults.accessKey; this.certifierKey = defaults.certifierKey; + this.callbackEndpoint = defaults.callbackEndpoint; } /** @@ -307,6 +309,7 @@ export class RelayConfig implements IRelayConfig { ], accessKey: process.env.ACCESS_SECRET || "", certifierKey: process.env.CERTIFIER_KEY || "", + callbackEndpoint: process.env.CALLBACK_ENDPOINT || "", }; } @@ -318,6 +321,7 @@ export class RelayConfig implements IRelayConfig { if (config.managerKeys !== undefined) this.managerKeys = config.managerKeys; if (config.accessKey !== undefined) this.accessKey = config.accessKey; if (config.certifierKey !== undefined) this.certifierKey = config.certifierKey; + if (config.callbackEndpoint !== undefined) this.callbackEndpoint = config.callbackEndpoint; } } @@ -501,6 +505,7 @@ export interface IRelayConfig { managerKeys: string[]; accessKey: string; certifierKey: string; + callbackEndpoint: string; } export interface IContractsConfig { diff --git a/packages/relay/src/routers/PaymentRouter.ts b/packages/relay/src/routers/PaymentRouter.ts index 38763026..76dffec2 100644 --- a/packages/relay/src/routers/PaymentRouter.ts +++ b/packages/relay/src/routers/PaymentRouter.ts @@ -2,7 +2,15 @@ import { CurrencyRate, Ledger, PhoneLinkCollection, ShopCollection, Token } from import { Config } from "../common/Config"; import { logger } from "../common/Logger"; import { WebService } from "../service/WebService"; -import { LoyaltyPaymentInputDataStatus, LoyaltyType, WithdrawStatus } from "../types"; +import { + LoyaltyPaymentInputDataStatus, + LoyaltyPaymentInternalData, + LoyaltyType, + PaymentResultCode, + PaymentResultData, + PaymentResultType, + WithdrawStatus, +} from "../types"; import { ContractUtils } from "../utils/ContractUtils"; import { Validation } from "../validation"; @@ -13,6 +21,7 @@ import * as hre from "hardhat"; import express from "express"; import { ISignerItem, RelaySigners } from "../contract/Signers"; import { RelayStorage } from "../storage/RelayStorage"; +import { HTTPClient } from "../utils/Utils"; export class PaymentRouter { /** @@ -65,6 +74,8 @@ export class PaymentRouter { * * @param service WebService * @param config Configuration + * @param storage + * @param relaySigners */ constructor(service: WebService, config: Config, storage: RelayStorage, relaySigners: RelaySigners) { this._web_service = service; @@ -78,97 +89,6 @@ export class PaymentRouter { 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( "/v1/payment/user/balance", @@ -278,6 +198,97 @@ export class PaymentRouter { ); } + /*** + * 트팬잭션을 중계할 때 사용될 서명자 + * @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, + }; + } + private async getPaymentId(account: string): Promise { const nonce = await (await this.getLedgerContract()).nonceOf(account); // 내부에 랜덤으로 32 Bytes 를 생성하여 ID를 생성하므로 무한반복될 가능성이 극히 낮음 @@ -453,23 +464,38 @@ export class PaymentRouter { const multiple = await (await this.getCurrencyRateContract()).MULTIPLE(); let balance: BigNumber; - let purchaseAmount: BigNumber; - let feeAmount: BigNumber; - let totalAmount: BigNumber; + let paidPoint: BigNumber; + let paidToken: BigNumber; + let paidValue: BigNumber; + let feePoint: BigNumber; + let feeToken: BigNumber; + let feeValue: BigNumber; + let totalPoint: BigNumber; + let totalToken: BigNumber; + let totalValue: 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); + paidPoint = amount.mul(rate).div(multiple); + feePoint = paidPoint.mul(feeRate).div(100); + totalPoint = paidPoint.add(feePoint); + paidToken = BigNumber.from(0); + feeToken = BigNumber.from(0); + totalToken = BigNumber.from(0); } 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); + paidToken = amount.mul(rate).div(tokenRate); + feeToken = paidToken.mul(feeRate).div(100); + totalToken = paidToken.add(feeToken); + paidPoint = BigNumber.from(0); + feePoint = BigNumber.from(0); + totalPoint = BigNumber.from(0); } + paidValue = BigNumber.from(amount); + feeValue = paidValue.mul(feeRate).div(100); + totalValue = paidValue.add(feeValue); return res.status(200).json( this.makeResponseData(200, { @@ -478,9 +504,15 @@ export class PaymentRouter { amount: amount.toString(), currency, balance: balance.toString(), - purchaseAmount: purchaseAmount.toString(), - feeAmount: feeAmount.toString(), - totalAmount: totalAmount.toString(), + paidPoint: paidPoint.toString(), + paidToken: paidToken.toString(), + paidValue: paidValue.toString(), + feePoint: feePoint.toString(), + feeToken: feeToken.toString(), + feeValue: feeValue.toString(), + totalPoint: totalPoint.toString(), + totalToken: totalToken.toString(), + totalValue: totalValue.toString(), feeRate: feeRate / 100, }) ); @@ -535,31 +567,48 @@ export class PaymentRouter { const multiple = await (await this.getCurrencyRateContract()).MULTIPLE(); let balance: BigNumber; - let purchaseAmount: BigNumber; - let feeAmount: BigNumber; - let totalAmount: BigNumber; + let paidPoint: BigNumber; + let paidToken: BigNumber; + let paidValue: BigNumber; + let feePoint: BigNumber; + let feeToken: BigNumber; + let feeValue: BigNumber; + let totalPoint: BigNumber; + let totalToken: BigNumber; + let totalValue: BigNumber; const loyaltyType = await (await this.getLedgerContract()).loyaltyTypeOf(account); 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); + paidPoint = amount.mul(rate).div(multiple); + feePoint = paidPoint.mul(feeRate).div(100); + totalPoint = paidPoint.add(feePoint); + if (totalPoint.gt(balance)) { + return res.status(200).json(this.makeResponseData(401, null, { message: "Insufficient balance" })); + } + paidToken = BigNumber.from(0); + feeToken = BigNumber.from(0); + totalToken = BigNumber.from(0); } 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); - } - - if (totalAmount.gt(balance)) { - return res.status(200).json(this.makeResponseData(401, null, { message: "Insufficient balance" })); + paidToken = amount.mul(rate).div(tokenRate); + feeToken = paidToken.mul(feeRate).div(100); + totalToken = paidToken.add(feeToken); + if (totalToken.gt(balance)) { + return res.status(200).json(this.makeResponseData(401, null, { message: "Insufficient balance" })); + } + paidPoint = BigNumber.from(0); + feePoint = BigNumber.from(0); + totalPoint = BigNumber.from(0); } + paidValue = BigNumber.from(amount); + feeValue = paidValue.mul(feeRate).div(100); + totalValue = paidValue.add(feeValue); const paymentId = await this.getPaymentId(account); - const item = { + const item: LoyaltyPaymentInternalData = { paymentId, purchaseId, amount, @@ -567,10 +616,18 @@ export class PaymentRouter { shopId, account, loyaltyType, - purchaseAmount, - feeAmount, - totalAmount, + paidPoint, + paidToken, + paidValue, + feePoint, + feeToken, + feeValue, + totalPoint, + totalToken, + totalValue, paymentStatus: LoyaltyPaymentInputDataStatus.CREATED, + createTimestamp: ContractUtils.getTimeStamp(), + cancelTimestamp: 0, }; await this._storage.postPayment( item.paymentId, @@ -580,10 +637,17 @@ export class PaymentRouter { item.shopId, item.account, item.loyaltyType, - item.purchaseAmount, - item.feeAmount, - item.totalAmount, - item.paymentStatus + item.paidPoint, + item.paidToken, + item.paidValue, + item.feePoint, + item.feeToken, + item.feeValue, + item.totalPoint, + item.totalToken, + item.totalValue, + item.paymentStatus, + item.createTimestamp ); /// 사용자에게 푸쉬 메세지 발송 후 서명을 확인함 @@ -597,10 +661,17 @@ export class PaymentRouter { shopId: item.shopId, account: item.account, loyaltyType: item.loyaltyType, - purchaseAmount: item.purchaseAmount.toString(), - feeAmount: item.feeAmount.toString(), - totalAmount: item.totalAmount.toString(), + paidPoint: item.paidPoint.toString(), + paidToken: item.paidToken.toString(), + paidValue: item.paidValue.toString(), + feePoint: item.feePoint.toString(), + feeToken: item.feeToken.toString(), + feeValue: item.feeValue.toString(), + totalPoint: item.totalPoint.toString(), + totalToken: item.totalToken.toString(), + totalValue: item.totalValue.toString(), paymentStatus: item.paymentStatus, + createTimestamp: item.createTimestamp, }) ); } catch (error: any) { @@ -651,10 +722,17 @@ export class PaymentRouter { shopId: item.shopId, account: item.account, loyaltyType: item.loyaltyType, - purchaseAmount: item.purchaseAmount.toString(), - feeAmount: item.feeAmount.toString(), - totalAmount: item.totalAmount.toString(), + paidPoint: item.paidPoint.toString(), + paidToken: item.paidToken.toString(), + paidValue: item.paidValue.toString(), + feePoint: item.feePoint.toString(), + feeToken: item.feeToken.toString(), + feeValue: item.feeValue.toString(), + totalPoint: item.totalPoint.toString(), + totalToken: item.totalToken.toString(), + totalValue: item.totalValue.toString(), paymentStatus: item.paymentStatus, + createTimestamp: item.createTimestamp, }) ); } catch (error: any) { @@ -728,34 +806,169 @@ export class PaymentRouter { return; } - const tx = await (await this.getLedgerContract()).connect(signerItem.signer).createLoyaltyPayment({ + const defaultResult: PaymentResultData = { paymentId: item.paymentId, purchaseId: item.purchaseId, - amount: item.amount, - currency: item.currency.toLowerCase(), + amount: item.amount.toString(), + currency: item.currency, shopId: item.shopId, account: item.account, - signature, - }); + loyaltyType: item.loyaltyType, + paidPoint: item.paidPoint.toString(), + paidToken: item.paidToken.toString(), + paidValue: item.paidValue.toString(), + feePoint: item.feePoint.toString(), + feeToken: item.feeToken.toString(), + feeValue: item.feeValue.toString(), + totalPoint: item.totalPoint.toString(), + totalToken: item.totalToken.toString(), + totalValue: item.totalValue.toString(), + }; + + const now = ContractUtils.getTimeStamp(); + if (now - item.createTimestamp > 60) { + const message = "Timeout period expired"; + res.status(200).json( + this.makeResponseData(404, undefined, { + message, + }) + ); - item.paymentStatus = LoyaltyPaymentInputDataStatus.CREATE_CONFIRMED; - await this._storage.updatePaymentStatus(item.paymentId, item.paymentStatus); - return res.status(200).json( - this.makeResponseData(200, { + item.paymentStatus = LoyaltyPaymentInputDataStatus.TIMEOUT; + await this._storage.updatePaymentStatus(item.paymentId, item.paymentStatus); + + await this.sendPaymentResult( + PaymentResultType.CREATE, + PaymentResultCode.TIMEOUT, + message, + defaultResult + ); + return; + } + + let success = true; + const contract = await this.getLedgerContract(); + let tx: any; + try { + tx = await contract.connect(signerItem.signer).createLoyaltyPayment({ paymentId: item.paymentId, purchaseId: item.purchaseId, - amount: item.amount.toString(), - currency: item.currency, + amount: item.amount, + currency: item.currency.toLowerCase(), shopId: item.shopId, account: item.account, - loyaltyType: item.loyaltyType, - purchaseAmount: item.purchaseAmount.toString(), - feeAmount: item.feeAmount.toString(), - totalAmount: item.totalAmount.toString(), - paymentStatus: item.paymentStatus, - txHash: tx.hash, - }) - ); + signature, + }); + + item.paymentStatus = LoyaltyPaymentInputDataStatus.CREATE_CONFIRMED; + await this._storage.updatePaymentStatus(item.paymentId, item.paymentStatus); + res.status(200).json( + this.makeResponseData(200, { + paymentId: item.paymentId, + purchaseId: item.purchaseId, + amount: item.amount.toString(), + currency: item.currency, + shopId: item.shopId, + account: item.account, + loyaltyType: item.loyaltyType, + paidPoint: item.paidPoint.toString(), + paidToken: item.paidToken.toString(), + paidValue: item.paidValue.toString(), + feePoint: item.feePoint.toString(), + feeToken: item.feeToken.toString(), + feeValue: item.feeValue.toString(), + totalPoint: item.totalPoint.toString(), + totalToken: item.totalToken.toString(), + totalValue: item.totalValue.toString(), + paymentStatus: item.paymentStatus, + createTimestamp: item.createTimestamp, + txHash: tx.hash, + }) + ); + } catch (error) { + success = false; + const message = ContractUtils.cacheEVMError(error as any); + await this.sendPaymentResult( + PaymentResultType.CREATE, + PaymentResultCode.CONTRACT_ERROR, + `An error occurred while executing the contract. (${message})`, + defaultResult + ); + } + + if (success && tx !== undefined) { + const contractReceipt = await tx.wait(); + const log = ContractUtils.findLog(contractReceipt, contract.interface, "CreatedLoyaltyPayment"); + if (log !== undefined) { + const parsedLog = contract.interface.parseLog(log); + if (item.paymentId === parsedLog.args.paymentId) { + item.paidPoint = + parsedLog.args.loyaltyType === LoyaltyType.POINT + ? BigNumber.from(parsedLog.args.paidPoint) + : BigNumber.from(0); + item.paidToken = + parsedLog.args.loyaltyType === LoyaltyType.TOKEN + ? BigNumber.from(parsedLog.args.paidToken) + : BigNumber.from(0); + item.paidValue = BigNumber.from(parsedLog.args.paidValue); + + item.feePoint = + parsedLog.args.loyaltyType === LoyaltyType.POINT + ? BigNumber.from(parsedLog.args.feePoint) + : BigNumber.from(0); + item.feeToken = + parsedLog.args.loyaltyType === LoyaltyType.TOKEN + ? BigNumber.from(parsedLog.args.feeToken) + : BigNumber.from(0); + item.feeValue = BigNumber.from(parsedLog.args.feeValue); + + item.totalPoint = item.paidPoint.add(item.feePoint); + item.totalToken = item.paidToken.add(item.feeToken); + item.totalValue = item.paidValue.add(item.feeValue); + + await this.sendPaymentResult( + PaymentResultType.CREATE, + PaymentResultCode.SUCCESS, + "The payment has been successfully completed.", + { + paymentId: parsedLog.args.paymentId, + purchaseId: parsedLog.args.purchaseId, + amount: parsedLog.args.paidValue.toString(), + currency: parsedLog.args.currency, + account: parsedLog.args.account, + shopId: parsedLog.args.shopId, + loyaltyType: parsedLog.args.loyaltyType, + paidPoint: item.paidPoint.toString(), + paidToken: item.paidToken.toString(), + paidValue: item.paidValue.toString(), + feePoint: item.feePoint.toString(), + feeToken: item.feeToken.toString(), + feeValue: item.feeValue.toString(), + totalPoint: item.totalPoint.toString(), + totalToken: item.totalToken.toString(), + totalValue: item.totalValue.toString(), + balance: parsedLog.args.balance.toString(), + } + ); + item.paymentStatus = LoyaltyPaymentInputDataStatus.CREATE_COMPLETE; + await this._storage.updatePaymentStatus(item.paymentId, item.paymentStatus); + } else { + await this.sendPaymentResult( + PaymentResultType.CREATE, + PaymentResultCode.INTERNAL_ERROR, + `An error occurred while executing the contract.`, + defaultResult + ); + } + } else { + await this.sendPaymentResult( + PaymentResultType.CREATE, + PaymentResultCode.INTERNAL_ERROR, + `An error occurred while executing the contract.`, + defaultResult + ); + } + } } } catch (error: any) { let message = ContractUtils.cacheEVMError(error as any); @@ -828,9 +1041,53 @@ export class PaymentRouter { return; } + const defaultResult: PaymentResultData = { + paymentId: item.paymentId, + purchaseId: item.purchaseId, + amount: item.amount.toString(), + currency: item.currency, + shopId: item.shopId, + account: item.account, + loyaltyType: item.loyaltyType, + paidPoint: item.paidPoint.toString(), + paidToken: item.paidToken.toString(), + paidValue: item.paidValue.toString(), + feePoint: item.feePoint.toString(), + feeToken: item.feeToken.toString(), + feeValue: item.feeValue.toString(), + totalPoint: item.totalPoint.toString(), + totalToken: item.totalToken.toString(), + totalValue: item.totalValue.toString(), + }; + + const now = ContractUtils.getTimeStamp(); + if (now - item.createTimestamp > 60) { + const message = "Timeout period expired"; + res.status(200).json( + this.makeResponseData(404, undefined, { + message, + }) + ); + + item.paymentStatus = LoyaltyPaymentInputDataStatus.TIMEOUT; + await this._storage.updatePaymentStatus(item.paymentId, item.paymentStatus); + + await this.sendPaymentResult( + PaymentResultType.CREATE, + PaymentResultCode.TIMEOUT, + message, + defaultResult + ); + return; + } + item.paymentStatus = LoyaltyPaymentInputDataStatus.CREATE_DENIED; await this._storage.updatePaymentStatus(item.paymentId, item.paymentStatus); - return res.status(200).json( + + item.cancelTimestamp = ContractUtils.getTimeStamp(); + await this._storage.updateCancelTimestamp(item.paymentId, item.cancelTimestamp); + + res.status(200).json( this.makeResponseData(200, { paymentId: item.paymentId, purchaseId: item.purchaseId, @@ -839,12 +1096,26 @@ export class PaymentRouter { shopId: item.shopId, account: item.account, loyaltyType: item.loyaltyType, - purchaseAmount: item.purchaseAmount.toString(), - feeAmount: item.feeAmount.toString(), - totalAmount: item.totalAmount.toString(), + paidPoint: item.paidPoint.toString(), + paidToken: item.paidToken.toString(), + paidValue: item.paidValue.toString(), + feePoint: item.feePoint.toString(), + feeToken: item.feeToken.toString(), + feeValue: item.feeValue.toString(), + totalPoint: item.totalPoint.toString(), + totalToken: item.totalToken.toString(), + totalValue: item.totalValue.toString(), paymentStatus: item.paymentStatus, + createTimestamp: item.createTimestamp, }) ); + + await this.sendPaymentResult( + PaymentResultType.CREATE, + PaymentResultCode.DENIED, + "The payment denied by user.", + defaultResult + ); } } catch (error: any) { let message = ContractUtils.cacheEVMError(error as any); @@ -895,15 +1166,16 @@ export class PaymentRouter { }) ); } else { - if (item.paymentStatus !== LoyaltyPaymentInputDataStatus.CREATE_CONFIRMED) { + if (item.paymentStatus !== LoyaltyPaymentInputDataStatus.CREATE_COMPLETE) { res.status(200).json( this.makeResponseData(402, undefined, { - message: "This payment has not been confirmed.", + message: "This payment has not been completed.", }) ); return; } + item.cancelTimestamp = LoyaltyPaymentInputDataStatus.CANCELED; item.paymentStatus = LoyaltyPaymentInputDataStatus.CANCELED; await this._storage.updatePaymentStatus(item.paymentId, item.paymentStatus); return res.status(200).json( @@ -915,10 +1187,17 @@ export class PaymentRouter { shopId: item.shopId, account: item.account, loyaltyType: item.loyaltyType, - purchaseAmount: item.purchaseAmount.toString(), - feeAmount: item.feeAmount.toString(), - totalAmount: item.totalAmount.toString(), + paidPoint: item.paidPoint.toString(), + paidToken: item.paidToken.toString(), + paidValue: item.paidValue.toString(), + feePoint: item.feePoint.toString(), + feeToken: item.feeToken.toString(), + feeValue: item.feeValue.toString(), + totalPoint: item.totalPoint.toString(), + totalToken: item.totalToken.toString(), + totalValue: item.totalValue.toString(), paymentStatus: item.paymentStatus, + createTimestamp: item.createTimestamp, }) ); } @@ -990,6 +1269,46 @@ export class PaymentRouter { return; } + const defaultResult: PaymentResultData = { + paymentId: item.paymentId, + purchaseId: item.purchaseId, + amount: item.amount.toString(), + currency: item.currency, + shopId: item.shopId, + account: item.account, + loyaltyType: item.loyaltyType, + paidPoint: item.paidPoint.toString(), + paidToken: item.paidToken.toString(), + paidValue: item.paidValue.toString(), + feePoint: item.feePoint.toString(), + feeToken: item.feeToken.toString(), + feeValue: item.feeValue.toString(), + totalPoint: item.totalPoint.toString(), + totalToken: item.totalToken.toString(), + totalValue: item.totalValue.toString(), + }; + + const now = ContractUtils.getTimeStamp(); + if (now - item.cancelTimestamp > 60) { + const message = "Timeout period expired"; + res.status(200).json( + this.makeResponseData(404, undefined, { + message, + }) + ); + + item.paymentStatus = LoyaltyPaymentInputDataStatus.TIMEOUT; + await this._storage.updatePaymentStatus(item.paymentId, item.paymentStatus); + + await this.sendPaymentResult( + PaymentResultType.CANCEL, + PaymentResultCode.TIMEOUT, + message, + defaultResult + ); + return; + } + const wallet = new hre.ethers.Wallet(this._config.relay.certifierKey); const certifierSignature = await ContractUtils.signLoyaltyPaymentCancel( wallet, @@ -998,32 +1317,103 @@ export class PaymentRouter { await (await this.getLedgerContract()).nonceOf(wallet.address) ); - const tx = await (await this.getLedgerContract()).connect(signerItem.signer).cancelLoyaltyPayment({ - paymentId: item.paymentId, - purchaseId: item.purchaseId, - account: item.account, - signature, - certifierSignature, - }); - - item.paymentStatus = LoyaltyPaymentInputDataStatus.CANCEL_CONFIRMED; - await this._storage.updatePaymentStatus(item.paymentId, item.paymentStatus); - return res.status(200).json( - this.makeResponseData(200, { + let success = true; + const contract = await this.getLedgerContract(); + let tx: any; + try { + tx = await (await this.getLedgerContract()).connect(signerItem.signer).cancelLoyaltyPayment({ paymentId: item.paymentId, purchaseId: item.purchaseId, - amount: item.amount.toString(), - currency: item.currency, - shopId: item.shopId, account: item.account, - loyaltyType: item.loyaltyType, - purchaseAmount: item.purchaseAmount.toString(), - feeAmount: item.feeAmount.toString(), - totalAmount: item.totalAmount.toString(), - paymentStatus: item.paymentStatus, - txHash: tx.hash, - }) - ); + signature, + certifierSignature, + }); + + item.paymentStatus = LoyaltyPaymentInputDataStatus.CANCEL_CONFIRMED; + await this._storage.updatePaymentStatus(item.paymentId, item.paymentStatus); + res.status(200).json( + this.makeResponseData(200, { + paymentId: item.paymentId, + purchaseId: item.purchaseId, + amount: item.amount.toString(), + currency: item.currency, + shopId: item.shopId, + account: item.account, + loyaltyType: item.loyaltyType, + paidPoint: item.paidPoint.toString(), + paidToken: item.paidToken.toString(), + paidValue: item.paidValue.toString(), + feePoint: item.feePoint.toString(), + feeToken: item.feeToken.toString(), + feeValue: item.feeValue.toString(), + totalPoint: item.totalPoint.toString(), + totalToken: item.totalToken.toString(), + totalValue: item.totalValue.toString(), + paymentStatus: item.paymentStatus, + createTimestamp: item.createTimestamp, + txHash: tx.hash, + }) + ); + } catch (error) { + success = false; + const message = ContractUtils.cacheEVMError(error as any); + await this.sendPaymentResult( + PaymentResultType.CANCEL, + PaymentResultCode.CONTRACT_ERROR, + `An error occurred while executing the contract. (${message})`, + defaultResult + ); + } + + if (success && tx !== undefined) { + const contractReceipt = await tx.wait(); + const log = ContractUtils.findLog(contractReceipt, contract.interface, "CancelledLoyaltyPayment"); + if (log !== undefined) { + const parsedLog = contract.interface.parseLog(log); + if (item.amount.eq(parsedLog.args.paidValue)) { + await this.sendPaymentResult( + PaymentResultType.CANCEL, + PaymentResultCode.SUCCESS, + "The cancellation has been successfully completed.", + { + paymentId: parsedLog.args.paymentId, + purchaseId: parsedLog.args.purchaseId, + amount: parsedLog.args.paidValue.toString(), + currency: parsedLog.args.currency, + account: parsedLog.args.account, + shopId: parsedLog.args.shopId, + loyaltyType: parsedLog.args.loyaltyType, + paidPoint: item.paidPoint.toString(), + paidToken: item.paidToken.toString(), + paidValue: item.paidValue.toString(), + feePoint: item.feePoint.toString(), + feeToken: item.feeToken.toString(), + feeValue: item.feeValue.toString(), + totalPoint: item.totalPoint.toString(), + totalToken: item.totalToken.toString(), + totalValue: item.totalValue.toString(), + balance: parsedLog.args.balance.toString(), + } + ); + item.paymentStatus = LoyaltyPaymentInputDataStatus.CANCEL_COMPLETE; + await this._storage.updatePaymentStatus(item.paymentId, item.paymentStatus); + } else { + await this.sendPaymentResult( + PaymentResultType.CANCEL, + PaymentResultCode.INTERNAL_ERROR, + `An error occurred while executing the contract.`, + defaultResult + ); + } + } else { + await this.sendPaymentResult( + PaymentResultType.CANCEL, + PaymentResultCode.INTERNAL_ERROR, + `An error occurred while executing the contract.`, + defaultResult + ); + } + } } } catch (error: any) { let message = ContractUtils.cacheEVMError(error as any); @@ -1093,9 +1483,49 @@ export class PaymentRouter { return; } + const defaultResult: PaymentResultData = { + paymentId: item.paymentId, + purchaseId: item.purchaseId, + amount: item.amount.toString(), + currency: item.currency, + shopId: item.shopId, + account: item.account, + loyaltyType: item.loyaltyType, + paidPoint: item.paidPoint.toString(), + paidToken: item.paidToken.toString(), + paidValue: item.paidValue.toString(), + feePoint: item.feePoint.toString(), + feeToken: item.feeToken.toString(), + feeValue: item.feeValue.toString(), + totalPoint: item.totalPoint.toString(), + totalToken: item.totalToken.toString(), + totalValue: item.totalValue.toString(), + }; + + const now = ContractUtils.getTimeStamp(); + if (now - item.cancelTimestamp > 60) { + const message = "Timeout period expired"; + res.status(200).json( + this.makeResponseData(404, undefined, { + message, + }) + ); + + item.paymentStatus = LoyaltyPaymentInputDataStatus.TIMEOUT; + await this._storage.updatePaymentStatus(item.paymentId, item.paymentStatus); + + await this.sendPaymentResult( + PaymentResultType.CANCEL, + PaymentResultCode.TIMEOUT, + message, + defaultResult + ); + return; + } + item.paymentStatus = LoyaltyPaymentInputDataStatus.CANCEL_DENIED; await this._storage.updatePaymentStatus(item.paymentId, item.paymentStatus); - return res.status(200).json( + res.status(200).json( this.makeResponseData(200, { paymentId: item.paymentId, purchaseId: item.purchaseId, @@ -1104,12 +1534,26 @@ export class PaymentRouter { shopId: item.shopId, account: item.account, loyaltyType: item.loyaltyType, - purchaseAmount: item.purchaseAmount.toString(), - feeAmount: item.feeAmount.toString(), - totalAmount: item.totalAmount.toString(), + paidPoint: item.paidPoint.toString(), + paidToken: item.paidToken.toString(), + paidValue: item.paidValue.toString(), + feePoint: item.feePoint.toString(), + feeToken: item.feeToken.toString(), + feeValue: item.feeValue.toString(), + totalPoint: item.totalPoint.toString(), + totalToken: item.totalToken.toString(), + totalValue: item.totalValue.toString(), paymentStatus: item.paymentStatus, + createTimestamp: item.createTimestamp, }) ); + + await this.sendPaymentResult( + PaymentResultType.CANCEL, + PaymentResultCode.DENIED, + "The cancellation denied by user.", + defaultResult + ); } } catch (error: any) { let message = ContractUtils.cacheEVMError(error as any); @@ -1122,4 +1566,19 @@ export class PaymentRouter { ); } } + + private async sendPaymentResult( + type: PaymentResultType, + code: PaymentResultCode, + message: string, + data: PaymentResultData + ) { + const client = new HTTPClient(); + await client.post(this._config.relay.callbackEndpoint, { + type, + code, + message, + data, + }); + } } diff --git a/packages/relay/src/storage/RelayStorage.ts b/packages/relay/src/storage/RelayStorage.ts index 2914a543..aed74566 100644 --- a/packages/relay/src/storage/RelayStorage.ts +++ b/packages/relay/src/storage/RelayStorage.ts @@ -6,7 +6,7 @@ import { BigNumber } from "ethers"; import MybatisMapper from "mybatis-mapper"; import path from "path"; -import { LoyaltyPaymentInputData, LoyaltyPaymentInputDataStatus, LoyaltyType } from "../types"; +import { LoyaltyPaymentInputDataStatus, LoyaltyPaymentInternalData, LoyaltyType } from "../types"; /** * The class that inserts and reads the ledger into the database. @@ -40,9 +40,6 @@ export class RelayStorage extends Storage { } public async dropTestDB(database: any): Promise { - await this.exec( - `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = '${database}'` - ); await this.queryForMapper("table", "drop_table", { database }); } @@ -54,10 +51,17 @@ export class RelayStorage extends Storage { shopId: string, account: string, loyaltyType: LoyaltyType, - purchaseAmount: BigNumber, - feeAmount: BigNumber, - totalAmount: BigNumber, - paymentStatus: LoyaltyPaymentInputDataStatus + paidPoint: BigNumber, + paidToken: BigNumber, + paidValue: BigNumber, + feePoint: BigNumber, + feeToken: BigNumber, + feeValue: BigNumber, + totalPoint: BigNumber, + totalToken: BigNumber, + totalValue: BigNumber, + paymentStatus: LoyaltyPaymentInputDataStatus, + createTimestamp: number ): Promise { return new Promise(async (resolve, reject) => { this.queryForMapper("payment", "postPayment", { @@ -68,10 +72,17 @@ export class RelayStorage extends Storage { shopId, account, loyaltyType, - purchaseAmount: purchaseAmount.toString(), - feeAmount: feeAmount.toString(), - totalAmount: totalAmount.toString(), + paidPoint: paidPoint.toString(), + paidToken: paidToken.toString(), + paidValue: paidValue.toString(), + feePoint: feePoint.toString(), + feeToken: feeToken.toString(), + feeValue: feeValue.toString(), + totalPoint: totalPoint.toString(), + totalToken: totalToken.toString(), + totalValue: totalValue.toString(), paymentStatus, + createTimestamp, }) .then(() => { return resolve(); @@ -83,8 +94,8 @@ export class RelayStorage extends Storage { }); } - public getPayment(paymentId: string): Promise { - return new Promise(async (resolve, reject) => { + public getPayment(paymentId: string): Promise { + return new Promise(async (resolve, reject) => { this.queryForMapper("payment", "getPayment", { paymentId }) .then((result) => { if (result.rows.length > 0) { @@ -97,10 +108,18 @@ export class RelayStorage extends Storage { shopId: m.shopId, account: m.account, loyaltyType: m.loyaltyType, - purchaseAmount: BigNumber.from(m.purchaseAmount), - feeAmount: BigNumber.from(m.feeAmount), - totalAmount: BigNumber.from(m.totalAmount), + paidPoint: BigNumber.from(m.paidPoint), + paidToken: BigNumber.from(m.paidToken), + paidValue: BigNumber.from(m.paidValue), + feePoint: BigNumber.from(m.feePoint), + feeToken: BigNumber.from(m.feeToken), + feeValue: BigNumber.from(m.feeValue), + totalPoint: BigNumber.from(m.totalPoint), + totalToken: BigNumber.from(m.totalToken), + totalValue: BigNumber.from(m.totalValue), paymentStatus: m.paymentStatus, + createTimestamp: m.createTimestamp, + cancelTimestamp: m.cancelTimestamp, }); } else { return resolve(undefined); @@ -128,4 +147,20 @@ export class RelayStorage extends Storage { }); }); } + + public updateCancelTimestamp(paymentId: string, cancelTimestamp: number): Promise { + return new Promise(async (resolve, reject) => { + this.queryForMapper("payment", "updateCancelTimestamp", { + paymentId, + cancelTimestamp, + }) + .then(() => { + return resolve(); + }) + .catch((reason) => { + if (reason instanceof Error) return reject(reason); + return reject(new Error(reason)); + }); + }); + } } diff --git a/packages/relay/src/storage/mapper/payment.xml b/packages/relay/src/storage/mapper/payment.xml index 9709dad3..3476c372 100644 --- a/packages/relay/src/storage/mapper/payment.xml +++ b/packages/relay/src/storage/mapper/payment.xml @@ -5,17 +5,25 @@ INSERT INTO payments ( - "paymentId" , - "purchaseId" , - "amount" , - "currency" , - "shopId" , - "account" , - "loyaltyType" , - "purchaseAmount" , - "feeAmount" , - "totalAmount" , - "paymentStatus" + "paymentId" , + "purchaseId" , + "amount" , + "currency" , + "shopId" , + "account" , + "loyaltyType" , + "paidPoint" , + "paidToken" , + "paidValue" , + "feePoint" , + "feeToken" , + "feeValue" , + "totalPoint" , + "totalToken" , + "totalValue" , + "paymentStatus" , + "createTimestamp" , + "cencelTimestamp" ) VALUES ( @@ -26,10 +34,18 @@ #{shopId}, #{account}, ${loyaltyType}, - #{purchaseAmount}, - #{feeAmount}, - #{totalAmount}, - ${paymentStatus} + #{paidPoint}, + #{paidToken}, + #{paidValue}, + #{feePoint}, + #{feeToken}, + #{feeValue}, + #{totalPoint}, + #{totalToken}, + #{totalValue}, + ${paymentStatus}, + ${createTimestamp}, + 0 ) ON CONFLICT ("paymentId") DO NOTHING; @@ -44,4 +60,11 @@ "paymentStatus" = #{paymentStatus} WHERE "paymentId" = #{paymentId} + + + UPDATE payments + SET + "cencelTimestamp" = #{cancelTimestamp} + WHERE "paymentId" = #{paymentId} + diff --git a/packages/relay/src/storage/mapper/table.xml b/packages/relay/src/storage/mapper/table.xml index 0c6dd658..7d60d036 100644 --- a/packages/relay/src/storage/mapper/table.xml +++ b/packages/relay/src/storage/mapper/table.xml @@ -11,10 +11,18 @@ "shopId" VARCHAR(66) NOT NULL, "account" VARCHAR(42) NOT NULL, "loyaltyType" INTEGER, - "purchaseAmount" VARCHAR(64) NOT NULL, - "feeAmount" VARCHAR(64) NOT NULL, - "totalAmount" VARCHAR(64) NOT NULL, - "paymentStatus" INTEGER, + "paidPoint" VARCHAR(64) NOT NULL, + "paidToken" VARCHAR(64) NOT NULL, + "paidValue" VARCHAR(64) NOT NULL, + "feePoint" VARCHAR(64) NOT NULL, + "feeToken" VARCHAR(64) NOT NULL, + "feeValue" VARCHAR(64) NOT NULL, + "totalPoint" VARCHAR(64) NOT NULL, + "totalToken" VARCHAR(64) NOT NULL, + "totalValue" VARCHAR(64) NOT NULL, + "paymentStatus" INTEGER DEFAULT 0, + "createTimestamp" INTEGER DEFAULT 0, + "cencelTimestamp" INTEGER DEFAULT 0, PRIMARY KEY ("paymentId") ) diff --git a/packages/relay/src/types/index.ts b/packages/relay/src/types/index.ts index e6fef502..4c05ca77 100644 --- a/packages/relay/src/types/index.ts +++ b/packages/relay/src/types/index.ts @@ -14,12 +14,15 @@ export enum LoyaltyPaymentInputDataStatus { CREATED, CREATE_CONFIRMED, CREATE_DENIED, + CREATE_COMPLETE, CANCELED, CANCEL_CONFIRMED, CANCEL_DENIED, + CANCEL_COMPLETE, + TIMEOUT, } -export interface LoyaltyPaymentInputData { +export interface LoyaltyPaymentInternalData { paymentId: string; purchaseId: string; amount: BigNumber; @@ -27,8 +30,51 @@ export interface LoyaltyPaymentInputData { shopId: string; account: string; loyaltyType: LoyaltyType; - purchaseAmount: BigNumber; - feeAmount: BigNumber; - totalAmount: BigNumber; + + paidPoint: BigNumber; + paidToken: BigNumber; + paidValue: BigNumber; + feePoint: BigNumber; + feeToken: BigNumber; + feeValue: BigNumber; + totalPoint: BigNumber; + totalToken: BigNumber; + totalValue: BigNumber; + paymentStatus: LoyaltyPaymentInputDataStatus; + createTimestamp: number; + cancelTimestamp: number; +} + +export enum PaymentResultType { + CREATE = "create", + CANCEL = "cancel", +} + +export enum PaymentResultCode { + SUCCESS = 0, + DENIED = 1001, + CONTRACT_ERROR = 1002, + INTERNAL_ERROR = 1003, + TIMEOUT = 2000, +} + +export interface PaymentResultData { + paymentId: string; + purchaseId: string; + amount: string; + currency: string; + account: string; + shopId: string; + loyaltyType: number; + paidPoint: string; + paidToken: string; + paidValue: string; + feePoint: string; + feeToken: string; + feeValue: string; + totalPoint: string; + totalToken: string; + totalValue: string; + balance?: string; } diff --git a/packages/relay/src/utils/ContractUtils.ts b/packages/relay/src/utils/ContractUtils.ts index d0914b62..916be25c 100644 --- a/packages/relay/src/utils/ContractUtils.ts +++ b/packages/relay/src/utils/ContractUtils.ts @@ -4,7 +4,16 @@ import { BigNumberish, BytesLike, ethers, Signer } from "ethers"; import { arrayify } from "ethers/lib/utils"; import * as hre from "hardhat"; +import { Log } from "@ethersproject/providers"; +import { id } from "@ethersproject/hash"; +import { ContractReceipt } from "@ethersproject/contracts"; +import { Interface } from "@ethersproject/abi"; + export class ContractUtils { + public static findLog(receipt: ContractReceipt, iface: Interface, eventName: string): Log | undefined { + return receipt.logs.find((log) => log.topics[0] === id(iface.getEvent(eventName).format("sighash"))); + } + /** * It generates hash values. * @param data The source data diff --git a/packages/relay/src/utils/Utils.ts b/packages/relay/src/utils/Utils.ts index e7bbb1c8..42f6a631 100644 --- a/packages/relay/src/utils/Utils.ts +++ b/packages/relay/src/utils/Utils.ts @@ -1,4 +1,6 @@ +import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios"; import process from "process"; +import { handleNetworkError } from "../network/ErrorTypes"; export class Utils { /** @@ -221,3 +223,58 @@ export class ArrayRange { export function iota(begin: number, end?: number, step?: number): ArrayRange { return new ArrayRange(begin, end, step); } + +export class HTTPClient { + private client: AxiosInstance; + constructor() { + this.client = axios.create(); + } + public get(url: string, config?: AxiosRequestConfig): Promise { + return new Promise((resolve, reject) => { + this.client + .get(url, config) + .then((response: AxiosResponse) => { + resolve(response); + }) + .catch((reason: any) => { + reject(handleNetworkError(reason)); + }); + }); + } + public delete(url: string, config?: AxiosRequestConfig): Promise { + return new Promise((resolve, reject) => { + this.client + .delete(url, config) + .then((response: AxiosResponse) => { + resolve(response); + }) + .catch((reason: any) => { + reject(handleNetworkError(reason)); + }); + }); + } + public post(url: string, data?: any, config?: AxiosRequestConfig): Promise { + return new Promise((resolve, reject) => { + this.client + .post(url, data, config) + .then((response: AxiosResponse) => { + resolve(response); + }) + .catch((reason: any) => { + reject(handleNetworkError(reason)); + }); + }); + } + public put(url: string, data?: any, config?: AxiosRequestConfig): Promise { + return new Promise((resolve, reject) => { + this.client + .put(url, data, config) + .then((response: AxiosResponse) => { + resolve(response); + }) + .catch((reason: any) => { + reject(handleNetworkError(reason)); + }); + }); + } +} diff --git a/packages/relay/test/Payment.test.ts b/packages/relay/test/Payment.test.ts index 6238c6b1..14bd3fe9 100644 --- a/packages/relay/test/Payment.test.ts +++ b/packages/relay/test/Payment.test.ts @@ -25,6 +25,7 @@ import { BigNumber, Wallet } from "ethers"; // tslint:disable-next-line:no-implicit-dependencies import { AddressZero } from "@ethersproject/constants"; import { LoyaltyPaymentInputDataStatus, LoyaltyType } from "../src/types"; +import { FakerCallbackServer } from "./helper/FakerCallbackServer"; // tslint:disable-next-line:no-var-requires const URI = require("urijs"); @@ -174,6 +175,8 @@ describe("Test of Server", function () { let serverURL: URL; let config: Config; + let fakerCallbackServer: FakerCallbackServer; + interface IShopData { shopId: string; name: string; @@ -309,6 +312,15 @@ describe("Test of Server", function () { await storage.close(); }); + before("Start CallbackServer", async () => { + fakerCallbackServer = new FakerCallbackServer(3400); + await fakerCallbackServer.start(); + }); + + after("Stop CallbackServer", async () => { + await fakerCallbackServer.stop(); + }); + context("Prepare shop data", () => { it("Add Shop Data", async () => { for (const elem of shopData) { @@ -457,9 +469,9 @@ describe("Test of Server", function () { assert.deepStrictEqual(response.data.data.account, users[purchase.userIndex].address); assert.deepStrictEqual(response.data.data.loyaltyType, LoyaltyType.POINT); assert.deepStrictEqual(response.data.data.balance, pointAmount.toString()); - assert.deepStrictEqual(response.data.data.purchaseAmount, Amount.make(1000).toString()); - assert.deepStrictEqual(response.data.data.feeAmount, Amount.make(50).toString()); - assert.deepStrictEqual(response.data.data.totalAmount, Amount.make(1050).toString()); + assert.deepStrictEqual(response.data.data.paidPoint, Amount.make(1000).toString()); + assert.deepStrictEqual(response.data.data.feePoint, Amount.make(50).toString()); + assert.deepStrictEqual(response.data.data.totalPoint, Amount.make(1050).toString()); assert.deepStrictEqual(response.data.data.amount, Amount.make(1).toString()); assert.deepStrictEqual(response.data.data.currency, "USD"); assert.deepStrictEqual(response.data.data.feeRate, 0.05); @@ -502,7 +514,7 @@ describe("Test of Server", function () { assert.deepStrictEqual(response.data.data.paymentId, paymentId); assert.deepStrictEqual(response.data.data.account, users[purchase.userIndex].address); assert.deepStrictEqual(response.data.data.loyaltyType, LoyaltyType.POINT); - assert.deepStrictEqual(response.data.data.purchaseAmount, amountOfLoyalty.toString()); + assert.deepStrictEqual(response.data.data.amount, amountOfLoyalty.toString()); }); it("Endpoint POST /v1/payment/create/deny", async () => { @@ -537,6 +549,12 @@ describe("Test of Server", function () { assert.ok(response.data.data.paymentStatus === LoyaltyPaymentInputDataStatus.CREATE_DENIED); }); + it("Waiting", async () => { + await ContractUtils.delay(2000); + assert.deepStrictEqual(fakerCallbackServer.responseData.length, 1); + assert.deepStrictEqual(fakerCallbackServer.responseData[0].code, 1001); + }); + it("Endpoint POST /v1/payment/create/confirm", async () => { const responseItem = await client.post( URI(serverURL).directory("/v1/payment/create").filename("item").toString(), @@ -688,6 +706,12 @@ describe("Test of Server", function () { assert.ok(response.data.data.paymentStatus === LoyaltyPaymentInputDataStatus.CREATE_CONFIRMED); }); + it("Waiting", async () => { + await ContractUtils.delay(2000); + assert.deepStrictEqual(fakerCallbackServer.responseData.length, 2); + assert.deepStrictEqual(fakerCallbackServer.responseData[1].code, 0); + }); + it("Endpoint POST /v1/payment/create/deny", async () => { const responseItem = await client.post( URI(serverURL).directory("/v1/payment/create").filename("item").toString(), @@ -772,6 +796,15 @@ describe("Test of Server", function () { await storage.close(); }); + before("Start CallbackServer", async () => { + fakerCallbackServer = new FakerCallbackServer(3400); + await fakerCallbackServer.start(); + }); + + after("Stop CallbackServer", async () => { + await fakerCallbackServer.stop(); + }); + context("Prepare shop data", () => { it("Add Shop Data", async () => { for (const elem of shopData) { @@ -920,9 +953,9 @@ describe("Test of Server", function () { assert.deepStrictEqual(response.data.data.account, users[purchase.userIndex].address); assert.deepStrictEqual(response.data.data.loyaltyType, LoyaltyType.POINT); assert.deepStrictEqual(response.data.data.balance, pointAmount.toString()); - assert.deepStrictEqual(response.data.data.purchaseAmount, Amount.make(1000).toString()); - assert.deepStrictEqual(response.data.data.feeAmount, Amount.make(50).toString()); - assert.deepStrictEqual(response.data.data.totalAmount, Amount.make(1050).toString()); + assert.deepStrictEqual(response.data.data.paidPoint, Amount.make(1000).toString()); + assert.deepStrictEqual(response.data.data.feePoint, Amount.make(50).toString()); + assert.deepStrictEqual(response.data.data.totalPoint, Amount.make(1050).toString()); assert.deepStrictEqual(response.data.data.amount, Amount.make(1).toString()); assert.deepStrictEqual(response.data.data.currency, "USD"); assert.deepStrictEqual(response.data.data.feeRate, 0.05); @@ -983,6 +1016,10 @@ describe("Test of Server", function () { assert.ok(response.data.data.paymentStatus === LoyaltyPaymentInputDataStatus.CREATE_CONFIRMED); }); + it("Waiting", async () => { + await ContractUtils.delay(2000); + }); + it("Endpoint POST /v1/payment/cancel", async () => { const url = URI(serverURL).directory("/v1/payment").filename("cancel").toString(); @@ -1041,12 +1078,12 @@ describe("Test of Server", function () { LoyaltyPaymentInputDataStatus.CANCEL_CONFIRMED ); const newBalance = await ledgerContract.pointBalanceOf(users[purchaseOfLoyalty.userIndex].address); - assert.deepStrictEqual(newBalance, oldBalance.add(BigNumber.from(responseItem.data.data.totalAmount))); + assert.deepStrictEqual(newBalance, oldBalance.add(BigNumber.from(responseItem.data.data.totalPoint))); const newShopInfo = await shopCollection.shopOf(responseItem.data.data.shopId); assert.deepStrictEqual( newShopInfo.usedPoint, - oldShopInfo.usedPoint.sub(BigNumber.from(responseItem.data.data.purchaseAmount)) + oldShopInfo.usedPoint.sub(BigNumber.from(responseItem.data.data.paidPoint)) ); }); }); @@ -1103,6 +1140,15 @@ describe("Test of Server", function () { await storage.close(); }); + before("Start CallbackServer", async () => { + fakerCallbackServer = new FakerCallbackServer(3400); + await fakerCallbackServer.start(); + }); + + after("Stop CallbackServer", async () => { + await fakerCallbackServer.stop(); + }); + context("Prepare shop data", () => { it("Add Shop Data", async () => { for (const elem of shopData) { @@ -1251,9 +1297,9 @@ describe("Test of Server", function () { assert.deepStrictEqual(response.data.data.account, users[purchase.userIndex].address); assert.deepStrictEqual(response.data.data.loyaltyType, LoyaltyType.POINT); assert.deepStrictEqual(response.data.data.balance, pointAmount.toString()); - assert.deepStrictEqual(response.data.data.purchaseAmount, Amount.make(1000).toString()); - assert.deepStrictEqual(response.data.data.feeAmount, Amount.make(50).toString()); - assert.deepStrictEqual(response.data.data.totalAmount, Amount.make(1050).toString()); + assert.deepStrictEqual(response.data.data.paidPoint, Amount.make(1000).toString()); + assert.deepStrictEqual(response.data.data.feePoint, Amount.make(50).toString()); + assert.deepStrictEqual(response.data.data.totalPoint, Amount.make(1050).toString()); assert.deepStrictEqual(response.data.data.amount, Amount.make(1).toString()); assert.deepStrictEqual(response.data.data.currency, "USD"); assert.deepStrictEqual(response.data.data.feeRate, 0.05); @@ -1314,6 +1360,10 @@ describe("Test of Server", function () { assert.ok(response.data.data.paymentStatus === LoyaltyPaymentInputDataStatus.CREATE_CONFIRMED); }); + it("Waiting", async () => { + await ContractUtils.delay(2000); + }); + it("Endpoint POST /v1/payment/cancel", async () => { const url = URI(serverURL).directory("/v1/payment").filename("cancel").toString(); @@ -1417,6 +1467,15 @@ describe("Test of Server", function () { await storage.close(); }); + before("Start CallbackServer", async () => { + fakerCallbackServer = new FakerCallbackServer(3400); + await fakerCallbackServer.start(); + }); + + after("Stop CallbackServer", async () => { + await fakerCallbackServer.stop(); + }); + context("Prepare shop data", () => { it("Add Shop Data", async () => { for (const elem of shopData) { @@ -1632,6 +1691,10 @@ describe("Test of Server", function () { assert.ok(response.data.data !== undefined); assert.ok(response.data.data.txHash !== undefined); }); + + it("Waiting", async () => { + await ContractUtils.delay(2000); + }); }); }); }); diff --git a/packages/relay/test/helper/FakerCallbackServer.ts b/packages/relay/test/helper/FakerCallbackServer.ts new file mode 100644 index 00000000..c9fb17e3 --- /dev/null +++ b/packages/relay/test/helper/FakerCallbackServer.ts @@ -0,0 +1,74 @@ +import bodyParser from "body-parser"; +import cors from "cors"; +import express from "express"; +import http from "http"; + +export class FakerCallbackServer { + protected app: express.Application; + protected server: http.Server | null = null; + private readonly port: number; + public responseData: any[] = []; + + constructor(port: number | string) { + if (typeof port === "string") this.port = parseInt(port, 10); + else this.port = port; + + this.app = express(); + } + + 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("/callback", [], this.callback.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, () => { + resolve(); + }); + }); + } + + public stop(): Promise { + return new Promise((resolve, reject) => { + if (this.server != null) + this.server.close((err?) => { + err === undefined ? resolve() : reject(err); + }); + else resolve(); + }); + } + + private makeResponseData(code: number, data: any, error?: any): any { + return { + code, + data, + error, + }; + } + + private async getHealthStatus(req: express.Request, res: express.Response) { + return res.status(200).json("OK"); + } + + private async callback(req: express.Request, res: express.Response) { + console.log(JSON.stringify(req.body)); + this.responseData.push(req.body); + res.status(200).json(this.makeResponseData(200, { message: "OK" }, undefined)); + } +} diff --git a/packages/relay/test/helper/Utility.ts b/packages/relay/test/helper/Utility.ts index 718cbf01..99847594 100644 --- a/packages/relay/test/helper/Utility.ts +++ b/packages/relay/test/helper/Utility.ts @@ -1,7 +1,6 @@ import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios"; import { DefaultServer } from "../../src/DefaultServer"; import { handleNetworkError } from "../../src/network/ErrorTypes"; - export class TestServer extends DefaultServer {} /**