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,15 +1,16 @@
|
|||||||
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')
|
||||||
async beginPay(req, res) {
|
async beginPay(req, res) {
|
||||||
const user = req.user
|
const user = req.user
|
||||||
const {chain, currency, address} = req.params
|
const { chain, currency, address } = req.params
|
||||||
const tokenResult = await refreshToken(user.emailReal)
|
const tokenResult = await refreshToken(user.emailReal)
|
||||||
console.log(tokenResult)
|
console.log(tokenResult)
|
||||||
if (!tokenResult.success) {
|
if (!tokenResult.success) {
|
||||||
@ -19,26 +20,62 @@ 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 })
|
||||||
let dataOrign:any = {
|
await record.save()
|
||||||
|
const merchantOrderNo = record.id
|
||||||
|
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)
|
||||||
dataOrign.sign = sign
|
dataOrign.sign = sign
|
||||||
Object.assign(dataOrign, dataSign)
|
Object.assign(dataOrign, dataSign)
|
||||||
const urlBase = process.env.ALCHEMY_PAGE_BASE
|
const urlBase = process.env.ALCHEMY_PAGE_BASE
|
||||||
let url = `${urlBase}/`
|
let url = `${urlBase}/`
|
||||||
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
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 })
|
@ -1,52 +1,73 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import {sha1} from 'utils/security.util'
|
import { sha1 } from 'utils/security.util'
|
||||||
import crypto from 'crypto'
|
import crypto from 'crypto'
|
||||||
|
|
||||||
export function createSimpleSign() {
|
export function createSimpleSign() {
|
||||||
let timestamp = Date.now()
|
let timestamp = Date.now()
|
||||||
let appid = process.env.ALCHEMY_APPID
|
let appid = process.env.ALCHEMY_APPID
|
||||||
let secret = process.env.ALCHEMY_APP_SECRET
|
let secret = process.env.ALCHEMY_APP_SECRET
|
||||||
let sign = sha1(appid+secret+timestamp)
|
let sign = sha1(appid + secret + timestamp)
|
||||||
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()
|
||||||
const host = process.env.ALCHEMY_API_BASE
|
const host = process.env.ALCHEMY_API_BASE
|
||||||
const config = {
|
const config = {
|
||||||
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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user