Add Alchemy server callback endpoint and PayRecord class

Add functionality to check pay result sign, create page sign, and refresh token
 Implement a new `POST /pay/alchemy/cb` endpoint in `alchemy.controller.ts`
 `alchemyCallback` method now checks `PayRecord` status and pay sign before changing status and returning empty body
- `ROLE_ANON` added to `alchemyCallback` method and import of `role` changed in `alchemy.controller.ts`
- `PayRecord.ts` added to modules with `PayRecord` class and payment property enums
This commit is contained in:
zhl 2023-03-27 14:09:18 +08:00
parent 26a27a837c
commit 6ede0fd01a
3 changed files with 151 additions and 35 deletions

View File

@ -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 {}
}
}

58
src/modules/PayRecord.ts Normal file
View File

@ -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 })

View File

@ -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