diff --git a/src/controllers/alchemy.controller.ts b/src/controllers/alchemy.controller.ts index fb753a2..09a0b51 100644 --- a/src/controllers/alchemy.controller.ts +++ b/src/controllers/alchemy.controller.ts @@ -1,15 +1,16 @@ import logger from 'logger/logger' -import BaseController from 'common/base.controller' -import {ZError} from 'common/ZError' -import { router } from 'decorators/router' -import {createPageSign, refreshToken} from 'service/alchemy.svr' -import {generateKVStr} from 'utils/net.util' +import BaseController, { ROLE_ANON } from 'common/base.controller' +import { ZError } from 'common/ZError' +import { role, router } from 'decorators/router' +import { checkPayResultSign, createPageSign, refreshToken } from 'service/alchemy.svr' +import { generateKVStr } from 'utils/net.util' +import { PayRecord, PayStatus } from 'modules/PayRecord' class AlchemyController extends BaseController { @router('post /pay/alchemy/buy') async beginPay(req, res) { const user = req.user - const {chain, currency, address} = req.params + const { chain, currency, address } = req.params const tokenResult = await refreshToken(user.emailReal) console.log(tokenResult) if (!tokenResult.success) { @@ -19,26 +20,62 @@ class AlchemyController extends BaseController { const { id, email, accessToken } = tokenResult.data const redirectUrl = '' const callbackUrl = '' - const merchantOrderNo = '' - let dataOrign:any = { + let record = new PayRecord({ account: user.id, address, chain, currency }) + await record.save() + const merchantOrderNo = record.id + let dataOrign: any = { token: accessToken, email, id, showTable: 'buy', + merchantOrderNo, } - if (chain) dataOrign.network=chain - if (currency) dataOrign.crypto=currency + if (chain) dataOrign.network = chain + if (currency) dataOrign.crypto = currency let dataSign = { appId: process.env.ALCHEMY_APPID, - address + address, } - let signStr = generateKVStr({data: dataSign, sort: true}) + let signStr = generateKVStr({ data: dataSign, sort: true }) let sign = createPageSign(signStr) dataOrign.sign = sign Object.assign(dataOrign, dataSign) const urlBase = process.env.ALCHEMY_PAGE_BASE let url = `${urlBase}/` - url = generateKVStr({data: dataOrign, encode: true, uri: url}) - return {url} + url = generateKVStr({ data: dataOrign, encode: true, uri: url }) + 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') + } + 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 {} } } diff --git a/src/modules/PayRecord.ts b/src/modules/PayRecord.ts new file mode 100644 index 0000000..8795274 --- /dev/null +++ b/src/modules/PayRecord.ts @@ -0,0 +1,58 @@ +import { getModelForClass, index, modelOptions, mongoose, prop, Severity } from '@typegoose/typegoose' +import { dbconn } from 'decorators/dbconn' +import { BaseModule } from './Base' + +export enum PayPlatEnum { + ALCHEMY = 1, +} + +export enum PayType { + BUY = 1, + SELL = 2, +} + +export enum PayStatus { + PENDING = 0, + SUCCESS = 1, + FAIL = 2, +} + +@dbconn() +@modelOptions({ + schemaOptions: { collection: 'pay_record', timestamps: true }, + options: { allowMixed: Severity.ALLOW }, +}) +class PayRecordClass extends BaseModule { + @prop({ enum: PayPlatEnum, default: PayPlatEnum.ALCHEMY }) + public channel!: PayPlatEnum + + @prop({ required: true, default: PayType.BUY }) + public type: PayType + + @prop({ required: true }) + public account: string + + @prop() + public address: string + + @prop() + public chain: string + + @prop() + public currency?: string + + @prop({ required: true, default: PayStatus.PENDING }) + public status: PayStatus + // 渠道返回的原始资料 + @prop({ type: mongoose.Schema.Types.Mixed }) + public outData: any + + // 渠道返回的订单id + @prop() + public outOrderId: string + // 交易的txHash + @prop() + public txHash: string +} + +export const PayRecord = getModelForClass(PayRecordClass, { existingConnection: PayRecordClass.db }) diff --git a/src/service/alchemy.svr.ts b/src/service/alchemy.svr.ts index e159d15..3788c9b 100644 --- a/src/service/alchemy.svr.ts +++ b/src/service/alchemy.svr.ts @@ -1,52 +1,73 @@ import axios from 'axios' -import {sha1} from 'utils/security.util' +import { sha1 } from 'utils/security.util' import crypto from 'crypto' export function createSimpleSign() { let timestamp = Date.now() let appid = process.env.ALCHEMY_APPID let secret = process.env.ALCHEMY_APP_SECRET - let sign = sha1(appid+secret+timestamp) + let sign = sha1(appid + secret + timestamp) return { appid, timestamp, - sign + sign, } } +/** + * Check if the pay result sign is valid + * @param data - the data to be checked + * @returns true if the sign is valid, false otherwise + */ +export function checkPayResultSign(data: any) { + const { appId, orderNo, crypto, network, address, signature } = data + const sign = sha1(appId + process.env.ALCHEMY_APP_SECRET + appId + orderNo + crypto + network + address) + return sign === signature +} + +/** + * Create page sign + * @param plainText - plain text to be encrypted + * @returns encrypted text + */ export function createPageSign(plainText: string) { let secret = process.env.ALCHEMY_APP_SECRET try { - const plainTextData = Buffer.from(plainText, 'utf8'); - const secretKey = Buffer.from(secret, 'utf8'); - const iv = secret.substring(0, 16); - - const cipher = crypto.createCipheriv('aes-128-cbc', secretKey, iv); + const plainTextData = Buffer.from(plainText, 'utf8') + const secretKey = Buffer.from(secret, 'utf8') + const iv = secret.substring(0, 16) - let encrypted = cipher.update(plainTextData); - encrypted = Buffer.concat([encrypted, cipher.final()]); + const cipher = crypto.createCipheriv('aes-128-cbc', secretKey, iv) - return encrypted.toString('base64'); + let encrypted = cipher.update(plainTextData) + encrypted = Buffer.concat([encrypted, cipher.final()]) + + return encrypted.toString('base64') } catch (e) { - console.log(`AES encrypting exception, msg is ${e.toString()}`); + console.log(`AES encrypting exception, msg is ${e.toString()}`) } - return null; + return null } +/** + * Refresh token + * @param email - user email + * @returns token + */ export async function refreshToken(email: string) { - const data = JSON.stringify({email}) - const {appid, timestamp, sign} = createSimpleSign() + const data = JSON.stringify({ email }) + const { appid, timestamp, sign } = createSimpleSign() const host = process.env.ALCHEMY_API_BASE const config = { method: 'post', url: `${host}/merchant/getToken`, - headers: { - 'appId': appid, - 'timestamp': timestamp, - 'sign': sign, - 'Content-Type': 'application/json' + headers: { + appId: appid, + timestamp: timestamp, + sign: sign, + 'Content-Type': 'application/json', }, - data : data + data: data, } let response = await axios(config) return response.data