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,9 +1,10 @@
import logger from 'logger/logger' import logger from 'logger/logger'
import BaseController from 'common/base.controller' import BaseController, { ROLE_ANON } from 'common/base.controller'
import { ZError } from 'common/ZError' import { ZError } from 'common/ZError'
import { router } from 'decorators/router' import { role, router } from 'decorators/router'
import {createPageSign, refreshToken} from 'service/alchemy.svr' import { checkPayResultSign, createPageSign, refreshToken } from 'service/alchemy.svr'
import { generateKVStr } from 'utils/net.util' import { generateKVStr } from 'utils/net.util'
import { PayRecord, PayStatus } from 'modules/PayRecord'
class AlchemyController extends BaseController { class AlchemyController extends BaseController {
@router('post /pay/alchemy/buy') @router('post /pay/alchemy/buy')
@ -19,18 +20,21 @@ class AlchemyController extends BaseController {
const { id, email, accessToken } = tokenResult.data const { id, email, accessToken } = tokenResult.data
const redirectUrl = '' const redirectUrl = ''
const callbackUrl = '' const callbackUrl = ''
const merchantOrderNo = '' let record = new PayRecord({ account: user.id, address, chain, currency })
await record.save()
const merchantOrderNo = record.id
let dataOrign: any = { let dataOrign: any = {
token: accessToken, token: accessToken,
email, email,
id, id,
showTable: 'buy', showTable: 'buy',
merchantOrderNo,
} }
if (chain) dataOrign.network = chain if (chain) dataOrign.network = chain
if (currency) dataOrign.crypto = currency if (currency) dataOrign.crypto = currency
let dataSign = { let dataSign = {
appId: process.env.ALCHEMY_APPID, 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) let sign = createPageSign(signStr)
@ -41,4 +45,37 @@ class AlchemyController extends BaseController {
url = generateKVStr({ data: dataOrign, encode: true, uri: url }) url = generateKVStr({ data: dataOrign, encode: true, uri: url })
return { 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

@ -10,29 +10,50 @@ export function createSimpleSign() {
return { return {
appid, appid,
timestamp, 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) { export function createPageSign(plainText: string) {
let secret = process.env.ALCHEMY_APP_SECRET let secret = process.env.ALCHEMY_APP_SECRET
try { try {
const plainTextData = Buffer.from(plainText, 'utf8'); const plainTextData = Buffer.from(plainText, 'utf8')
const secretKey = Buffer.from(secret, 'utf8'); const secretKey = Buffer.from(secret, 'utf8')
const iv = secret.substring(0, 16); const iv = secret.substring(0, 16)
const cipher = crypto.createCipheriv('aes-128-cbc', secretKey, iv); const cipher = crypto.createCipheriv('aes-128-cbc', secretKey, iv)
let encrypted = cipher.update(plainTextData); let encrypted = cipher.update(plainTextData)
encrypted = Buffer.concat([encrypted, cipher.final()]); encrypted = Buffer.concat([encrypted, cipher.final()])
return encrypted.toString('base64'); return encrypted.toString('base64')
} catch (e) { } 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) { export async function refreshToken(email: string) {
const data = JSON.stringify({ email }) const data = JSON.stringify({ email })
const { appid, timestamp, sign } = createSimpleSign() const { appid, timestamp, sign } = createSimpleSign()
@ -41,12 +62,12 @@ export async function refreshToken(email: string) {
method: 'post', method: 'post',
url: `${host}/merchant/getToken`, url: `${host}/merchant/getToken`,
headers: { headers: {
'appId': appid, appId: appid,
'timestamp': timestamp, timestamp: timestamp,
'sign': sign, sign: sign,
'Content-Type': 'application/json' 'Content-Type': 'application/json',
}, },
data : data data: data,
} }
let response = await axios(config) let response = await axios(config)
return response.data return response.data