From 5220dd223296bc3be89b1d82d3cd8927056c61d4 Mon Sep 17 00:00:00 2001 From: zhl Date: Fri, 26 May 2023 14:04:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=9F=A5=E8=AF=A2crypto?= =?UTF-8?q?=E5=85=91=E6=8D=A2=E7=BE=8E=E5=85=83=E4=BB=B7=E6=A0=BC=E7=9A=84?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.development | 4 ++ src/api.server.ts | 12 +++- src/controllers/alchemy.controller.ts | 50 +++++-------- src/controllers/alchemyout.controller.ts | 91 ++++++++++++++++++++++++ src/service/alchemy.svr.ts | 58 ++++++++++++++- src/service/price.svr.ts | 65 +++++++++++++++++ 6 files changed, 243 insertions(+), 37 deletions(-) create mode 100644 src/controllers/alchemyout.controller.ts create mode 100644 src/service/price.svr.ts diff --git a/.env.development b/.env.development index 3eaba9b..d40b18a 100644 --- a/.env.development +++ b/.env.development @@ -17,3 +17,7 @@ ALCHEMY_APP_SECRET="4Yn8RkxDXN71Q3p0" ALCHEMY_API_BASE="https://openapi-test.alchemypay.org" ALCHEMY_PAGE_BASE="https://ramptest.alchemypay.org" ALCHEMY_PAY_CB_URL="https://wallet.cebggame.com" + + +#cryptocompare api key from metamask^_^ +CRYPTOCOMPARE_API_KEY=d1ec8cd68228095debc9db2dca45771b905ce1f27f522ebfef025c236f4aef3b \ No newline at end of file diff --git a/src/api.server.ts b/src/api.server.ts index b18a937..9a90725 100644 --- a/src/api.server.ts +++ b/src/api.server.ts @@ -7,6 +7,7 @@ import logger from 'logger/logger' import config from 'config/config' import { ConnectOptions } from 'mongoose' import CodeTaskSchedule from 'schedule/codetask.schedule' +import { PriceSvr } from 'service/price.svr' const zReqParserPlugin = require('plugins/zReqParser') @@ -87,6 +88,7 @@ export class ApiServer { initSchedules() { new CodeTaskSchedule().scheduleAll() + new PriceSvr().scheduleAll() } async connectDB() { @@ -131,8 +133,8 @@ export class ApiServer { /** * 格式化接口返回数据, 统一封装成如下格式 * { - * code: 0, - * msg?: '', + * errcode: 0, + * errmsg?: '', * data: any * } * @private @@ -142,6 +144,12 @@ export class ApiServer { reply.header('X-Powered-By', 'PHP/5.4.16') // @ts-ignore if (!payload.errcode) { + // @ts-ignore + if (payload.direct) { + // @ts-ignore + delete payload.direct + return payload + } payload = { errcode: 0, data: payload, diff --git a/src/controllers/alchemy.controller.ts b/src/controllers/alchemy.controller.ts index b40f10b..bfe52b7 100644 --- a/src/controllers/alchemy.controller.ts +++ b/src/controllers/alchemy.controller.ts @@ -1,12 +1,13 @@ import logger from 'logger/logger' -import BaseController, { ROLE_ANON } from 'common/base.controller' +import BaseController from 'common/base.controller' import { ZError } from 'common/ZError' -import { role, router } from 'decorators/router' -import { checkPayResultSign, createPageSign, refreshToken } from 'service/alchemy.svr' +import { router } from 'decorators/router' +import { createPageSign, queryPrice, refreshToken } from 'service/alchemy.svr' import { generateKVStr } from 'utils/net.util' import { PayRecord, PayStatus } from 'modules/PayRecord' +import { PriceSvr } from 'service/price.svr' -const CALL_BACK_URL = `${process.env.ALCHEMY_PAY_CB_URL}/pay/alchemy/cb` +const CALL_BACK_URL = `${process.env.ALCHEMY_PAY_CB_URL}/pay/out/alchemy/buycb` class AlchemyController extends BaseController { @router('post /pay/alchemy/buy') async beginPay(req, res) { @@ -19,7 +20,6 @@ class AlchemyController extends BaseController { throw new ZError(10, 'fetch pay token error') } const { id, email, accessToken } = tokenResult.data - const redirectUrl = '' let record = new PayRecord({ account: user.id, address, chain, currency }) await record.save() const merchantOrderNo = record.id @@ -47,36 +47,18 @@ class AlchemyController extends BaseController { return { url } } - @role(ROLE_ANON) - @router('post /pay/alchemy/cb') - async alchemyCallback(req, res) { - let { orderNo, status, crypto, network, merchantOrderNo } = req.params - if (!merchantOrderNo) { - logger.info(`alchemy callback merchantOrderNo not found`) - throw new ZError(11, 'alchemy callback merchantOrderNo not found') + @router('post /pay/alchemy/crypto_price') + async queryCryptoPrice(req, res) { + let { token, chain, currency } = req.params + if (!token || !chain) { + throw new ZError(11, 'token or network not found') } - let record = await PayRecord.findById(merchantOrderNo) - if (!record) { - logger.info(`alchemy callback record not found`) - throw new ZError(12, 'alchemy callback record not found') + let data = { + crypto: token, + network: chain, + fiat: currency || 'USD', } - if (record.status !== PayStatus.PENDING) { - logger.info(`alchemy callback record status error`) - throw new ZError(13, 'alchemy callback record status error') - } - if (!checkPayResultSign(req.params)) { - logger.info(`alchemy callback sign error`) - record.status = PayStatus.FAIL - await record.save() - throw new ZError(14, 'alchemy callback sign error') - } - - record.outOrderId = orderNo - record.chain = network - record.currency = crypto - record.outData = req.params - record.status = status == 'PAY_SUCCESS' ? PayStatus.SUCCESS : PayStatus.FAIL - await record.save() - return {} + let result = await new PriceSvr().fetchPrice(data) + return { price: result } } } diff --git a/src/controllers/alchemyout.controller.ts b/src/controllers/alchemyout.controller.ts new file mode 100644 index 0000000..bef4738 --- /dev/null +++ b/src/controllers/alchemyout.controller.ts @@ -0,0 +1,91 @@ + +import logger from 'logger/logger' +import BaseController, { ROLE_ANON } from 'common/base.controller' +import { ZError } from 'common/ZError' +import { role, router } from 'decorators/router' +import { checkPayResultSign } from 'service/alchemy.svr' +import { PayRecord, PayStatus } from 'modules/PayRecord' +/** + * for Alchemy call + */ +class AlchemyOutController extends BaseController { + @role(ROLE_ANON) + @router('post /pay/out/alchemy/buycb') + async alchemyCallback(req, res) { + let { orderNo, status, crypto, network, merchantOrderNo } = req.params + if (!merchantOrderNo) { + logger.info(`alchemy callback merchantOrderNo not found`) + throw new ZError(11, 'alchemy callback merchantOrderNo not found') + } + let record = await PayRecord.findById(merchantOrderNo) + if (!record) { + logger.info(`alchemy callback record not found`) + throw new ZError(12, 'alchemy callback record not found') + } + if (record.status !== PayStatus.PENDING) { + logger.info(`alchemy callback record status error`) + throw new ZError(13, 'alchemy callback record status error') + } + if (!checkPayResultSign(req.params)) { + logger.info(`alchemy callback sign error`) + record.status = PayStatus.FAIL + await record.save() + throw new ZError(14, 'alchemy callback sign error') + } + + record.outOrderId = orderNo + record.chain = network + record.currency = crypto + record.outData = req.params + record.status = status == 'PAY_SUCCESS' ? PayStatus.SUCCESS : PayStatus.FAIL + await record.save() + return {} + } + + /** + * 向商户查询币价 + * TODO:: + */ + @role(ROLE_ANON) + @router('get /pay/out/alchemy/queryprice') + async queryToken(req, res) { + const { crypto } = req.params + const { appId, timestamp, sign } = req.headers + let result = { + direct: 1, + data: { + price: "1.0", + networkList: [ + { + network: "ETH", + networkFee: "1.21" + } + ] + }, + success: true, + returnCode: "0000", // false: 9999 + returnMsg: "in amet", + } + return result + } + + /** + * 通知商户打币 + * TODO:: + */ + @role(ROLE_ANON) + @router('post /pay/out/alchemy/distribute') + async distributeToken(req, res) { + const { appId, timestamp, sign } = req.headers + const { orderNo, crypto, network, address, cryptoAmount, cryptoPrice, usdtAmount } = req.params + let result = { + direct: 1, + data: null, + success: true, + returnCode: "0000", // false: 9999 + returnMsg: "in amet", + } + res.send(result) + } + +} diff --git a/src/service/alchemy.svr.ts b/src/service/alchemy.svr.ts index b3f46fa..589bf4e 100644 --- a/src/service/alchemy.svr.ts +++ b/src/service/alchemy.svr.ts @@ -72,7 +72,10 @@ export async function refreshToken(email: string) { let response = await axios(config) return response.data } - +/** + * 创建订单 + * https://alchemycn.readme.io/docs/创建订单 + */ export async function createOrder(token: string, data: any) { const { appid, timestamp, sign } = createSimpleSign() const host = process.env.ALCHEMY_API_BASE @@ -91,3 +94,56 @@ export async function createOrder(token: string, data: any) { let response = await axios(config) return response.data } + +/** + * 更新订单状态 + * 当您给用户转移token后,您需要通过此接口通知Alchemy打币的信息 + * https://alchemycn.readme.io/docs/更新订单状态 + * TODO:: test + */ +export async function updateOrderStatus(token: string, data: any) { + const { appid, timestamp, sign } = createSimpleSign() + const url = process.env.ALCHEMY_API_BASE + '/webhooks/treasure' + const config = { + method: 'post', + url, + headers: { + 'Content-Type': 'application/json', + 'access-token': token, + appId: appid, + sign: sign, + timestamp, + }, + data, + } + let response = await axios(config) + return response.data +} +/** + * 查询数字货币币价 + */ +export async function queryPrice(data: any) { + const { appid, timestamp, sign } = createSimpleSign() + let dataOrign = { + crypto: 'ETH', + network: 'ETH', + fiat: 'USD', + amount: '100', + payWayCode: '10001', + side: 'BUY', + } + dataOrign = { ...dataOrign, ...data } + const config = { + method: 'post', + url: process.env.ALCHEMY_API_BASE + '/merchant/order/quote', + headers: { + 'Content-Type': 'application/json', + appId: appid, + sign, + timestamp, + }, + data: dataOrign, + } + let response = await axios(config) + return response.data +} diff --git a/src/service/price.svr.ts b/src/service/price.svr.ts new file mode 100644 index 0000000..306b2c8 --- /dev/null +++ b/src/service/price.svr.ts @@ -0,0 +1,65 @@ +import axios from 'axios' +import { singleton } from 'decorators/singleton' +import { queryPrice } from './alchemy.svr' +import * as schedule from 'node-schedule' +import logger from 'logger/logger' + +@singleton +export class PriceSvr { + private priceMap: Map = new Map() + + public async queryEthPrice(eth: string) { + const usd = 'USD' + const apiKey = process.env.CRYPTOCOMPARE_API_KEY + const url = `https://min-api.cryptocompare.com/data/price?fsym=${eth}&tsyms=${usd}&api_key=${apiKey}` + let priceData = await axios.get(url).then(res => res.data) + let price = priceData[usd] + '' + return price + } + + public async fetchPrice(data: any) { + let { crypto, network, fiat } = data + let key = `${crypto}_${network}_${fiat}` + let price = this.priceMap.get(key) + if (!price) { + await this.refreshOne(data) + price = this.priceMap.get(key) + } + return price + } + + private async refreshOne(data) { + let { crypto, network, fiat } = data + let key = `${crypto}_${network}_${fiat}` + try { + let priceData = await queryPrice(data) + this.priceMap.set(key, priceData.data.cryptoPrice + '') + } catch (e) { + logger.debug('update eth price with alchemy error: ' + e.message || e) + try { + let price = await this.queryEthPrice(crypto) + this.priceMap.set(key, price + '') + } catch (e) { + logger.debug('update eth price with cryptocompare error: ' + e.message || e) + } + } + } + + private async refreshAll() { + for (let key of this.priceMap.keys()) { + let [crypto, chain, fiat] = key.split('_') + let data = { + crypto, + network: chain, + fiat, + } + await this.refreshOne(data) + } + } + + scheduleAll() { + const job = schedule.scheduleJob('*/30 * * * * *', async () => { + this.refreshAll() + }) + } +}