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:
parent
26a27a837c
commit
6ede0fd01a
@ -1,9 +1,10 @@
|
||||
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 { router } from 'decorators/router'
|
||||
import {createPageSign, refreshToken} from 'service/alchemy.svr'
|
||||
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')
|
||||
@ -19,18 +20,21 @@ class AlchemyController extends BaseController {
|
||||
const { id, email, accessToken } = tokenResult.data
|
||||
const redirectUrl = ''
|
||||
const callbackUrl = ''
|
||||
const merchantOrderNo = ''
|
||||
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
|
||||
let dataSign = {
|
||||
appId: process.env.ALCHEMY_APPID,
|
||||
address
|
||||
address,
|
||||
}
|
||||
let signStr = generateKVStr({ data: dataSign, sort: true })
|
||||
let sign = createPageSign(signStr)
|
||||
@ -41,4 +45,37 @@ class AlchemyController extends BaseController {
|
||||
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
58
src/modules/PayRecord.ts
Normal 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 })
|
@ -10,29 +10,50 @@ export function createSimpleSign() {
|
||||
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 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 cipher = crypto.createCipheriv('aes-128-cbc', secretKey, iv)
|
||||
|
||||
let encrypted = cipher.update(plainTextData);
|
||||
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
||||
let encrypted = cipher.update(plainTextData)
|
||||
encrypted = Buffer.concat([encrypted, cipher.final()])
|
||||
|
||||
return encrypted.toString('base64');
|
||||
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()
|
||||
@ -41,12 +62,12 @@ export async function refreshToken(email: string) {
|
||||
method: 'post',
|
||||
url: `${host}/merchant/getToken`,
|
||||
headers: {
|
||||
'appId': appid,
|
||||
'timestamp': timestamp,
|
||||
'sign': sign,
|
||||
'Content-Type': 'application/json'
|
||||
appId: appid,
|
||||
timestamp: timestamp,
|
||||
sign: sign,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data : data
|
||||
data: data,
|
||||
}
|
||||
let response = await axios(config)
|
||||
return response.data
|
||||
|
Loading…
x
Reference in New Issue
Block a user