完善支付流程
This commit is contained in:
parent
5220dd2232
commit
476da08a8f
@ -18,6 +18,21 @@ ALCHEMY_API_BASE="https://openapi-test.alchemypay.org"
|
|||||||
ALCHEMY_PAGE_BASE="https://ramptest.alchemypay.org"
|
ALCHEMY_PAGE_BASE="https://ramptest.alchemypay.org"
|
||||||
ALCHEMY_PAY_CB_URL="https://wallet.cebggame.com"
|
ALCHEMY_PAY_CB_URL="https://wallet.cebggame.com"
|
||||||
|
|
||||||
|
CHAIN_CLIENT_URL=http://127.0.0.1:3006
|
||||||
|
|
||||||
#cryptocompare api key from metamask^_^
|
#cryptocompare api key from metamask^_^
|
||||||
CRYPTOCOMPARE_API_KEY=d1ec8cd68228095debc9db2dca45771b905ce1f27f522ebfef025c236f4aef3b
|
CRYPTOCOMPARE_API_KEY=d1ec8cd68228095debc9db2dca45771b905ce1f27f522ebfef025c236f4aef3b
|
||||||
|
|
||||||
|
# alchemy 相关配置
|
||||||
|
AVAILABLE_NETWORK=arbitrum|bsc
|
||||||
|
AVAILABLE_TOKENS=cec|ceg
|
||||||
|
ARBITRUM_CHAIN_ID=421163
|
||||||
|
ARBITRUM_CEC_ADDRESS='0xaa34B79A0Ab433eaC900fB3CB9f191F5Cd27501D'
|
||||||
|
ARBITRUM_CEG_ADDRESS='0xaa34B79A0Ab433eaC900fB3CB9f191F5Cd27501D'
|
||||||
|
ARBITRUM_WALLET=''
|
||||||
|
BSC_CHAIN_ID=97
|
||||||
|
BSC_CEC_ADDRESS='0xaa34B79A0Ab433eaC900fB3CB9f191F5Cd27501D'
|
||||||
|
BSC_CEG_ADDRESS='0xaa34B79A0Ab433eaC900fB3CB9f191F5Cd27501D'
|
||||||
|
BSC_WALLET=''
|
||||||
|
# 链端转账回调地址
|
||||||
|
PAY_TRANSFER_CB_URL=''
|
@ -30,7 +30,18 @@ const publicKey = `
|
|||||||
${process.env.API_TOKEN_SECRET_PUBLIC}
|
${process.env.API_TOKEN_SECRET_PUBLIC}
|
||||||
-----END PUBLIC KEY-----
|
-----END PUBLIC KEY-----
|
||||||
`
|
`
|
||||||
|
let networks = process.env.AVAILABLE_NETWORK.split('|')
|
||||||
|
let tokenList = process.env.AVAILABLE_TOKENS.split('|')
|
||||||
|
let chainCfgs = {}
|
||||||
|
for (let network of networks) {
|
||||||
|
let data = { chainId: 0, wallet: '', tokens: {} }
|
||||||
|
data.chainId = parseInt(process.env[`${network.toUpperCase()}_CHAIN_ID`])
|
||||||
|
data.wallet = process.env[`WALLET_${network.toUpperCase()}`]
|
||||||
|
for (let sub of tokenList) {
|
||||||
|
data.tokens[sub] = process.env[`ADDRESS_${sub.toUpperCase()}_${network.toUpperCase()}`]
|
||||||
|
}
|
||||||
|
chainCfgs[network] = data
|
||||||
|
}
|
||||||
let baseConfig = {
|
let baseConfig = {
|
||||||
api: {
|
api: {
|
||||||
port: parseInt(process.env.API_PORT),
|
port: parseInt(process.env.API_PORT),
|
||||||
@ -42,6 +53,7 @@ let baseConfig = {
|
|||||||
|
|
||||||
db_main: process.env.DB_MAIN,
|
db_main: process.env.DB_MAIN,
|
||||||
db_second: process.env.DB_SECOND,
|
db_second: process.env.DB_SECOND,
|
||||||
|
chainCfgs: chainCfgs,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default baseConfig
|
export default baseConfig
|
||||||
|
@ -2,7 +2,7 @@ import logger from 'logger/logger'
|
|||||||
import BaseController from 'common/base.controller'
|
import BaseController from 'common/base.controller'
|
||||||
import { ZError } from 'common/ZError'
|
import { ZError } from 'common/ZError'
|
||||||
import { router } from 'decorators/router'
|
import { router } from 'decorators/router'
|
||||||
import { createPageSign, queryPrice, refreshToken } from 'service/alchemy.svr'
|
import { createPageSign, queryFiat, queryPrice, refreshToken } from 'service/alchemy.svr'
|
||||||
import { generateKVStr } from 'utils/net.util'
|
import { generateKVStr } from 'utils/net.util'
|
||||||
import { PayRecord, PayStatus } from 'modules/PayRecord'
|
import { PayRecord, PayStatus } from 'modules/PayRecord'
|
||||||
import { PriceSvr } from 'service/price.svr'
|
import { PriceSvr } from 'service/price.svr'
|
||||||
@ -12,7 +12,17 @@ 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 { network, crypto, address, fiat, fiatAmount, country } = req.params
|
||||||
|
if (fiat || fiatAmount || country) {
|
||||||
|
if (!fiat || !fiatAmount || !country) {
|
||||||
|
throw new ZError(11, 'fiat, fiatAmount and country must be provided')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (network || crypto) {
|
||||||
|
if (!network || !crypto) {
|
||||||
|
throw new ZError(12, 'network and crypto must be provided')
|
||||||
|
}
|
||||||
|
}
|
||||||
const tokenResult = await refreshToken(user.emailReal || user.email)
|
const tokenResult = await refreshToken(user.emailReal || user.email)
|
||||||
console.log(tokenResult)
|
console.log(tokenResult)
|
||||||
if (!tokenResult.success || tokenResult.returnCode !== '0000') {
|
if (!tokenResult.success || tokenResult.returnCode !== '0000') {
|
||||||
@ -20,7 +30,10 @@ class AlchemyController extends BaseController {
|
|||||||
throw new ZError(10, 'fetch pay token error')
|
throw new ZError(10, 'fetch pay token error')
|
||||||
}
|
}
|
||||||
const { id, email, accessToken } = tokenResult.data
|
const { id, email, accessToken } = tokenResult.data
|
||||||
let record = new PayRecord({ account: user.id, address, chain, currency })
|
let record = new PayRecord({ account: user.id, address, network, crypto })
|
||||||
|
if (fiat) record.fiat = fiat
|
||||||
|
if (fiatAmount) record.fiatAmount = fiatAmount
|
||||||
|
if (country) record.country = country
|
||||||
await record.save()
|
await record.save()
|
||||||
const merchantOrderNo = record.id
|
const merchantOrderNo = record.id
|
||||||
let dataOrign: any = {
|
let dataOrign: any = {
|
||||||
@ -30,8 +43,11 @@ class AlchemyController extends BaseController {
|
|||||||
showTable: 'buy',
|
showTable: 'buy',
|
||||||
merchantOrderNo,
|
merchantOrderNo,
|
||||||
}
|
}
|
||||||
if (chain) dataOrign.network = chain
|
if (network) dataOrign.network = network
|
||||||
if (currency) dataOrign.crypto = currency
|
if (crypto) dataOrign.crypto = crypto
|
||||||
|
if (fiat) dataOrign.fiat = fiat
|
||||||
|
if (fiatAmount) dataOrign.fiatAmount = fiatAmount
|
||||||
|
if (country) dataOrign.country = country
|
||||||
let dataSign: any = {
|
let dataSign: any = {
|
||||||
appId: process.env.ALCHEMY_APPID,
|
appId: process.env.ALCHEMY_APPID,
|
||||||
address,
|
address,
|
||||||
@ -49,10 +65,16 @@ class AlchemyController extends BaseController {
|
|||||||
|
|
||||||
@router('post /pay/alchemy/crypto_price')
|
@router('post /pay/alchemy/crypto_price')
|
||||||
async queryCryptoPrice(req, res) {
|
async queryCryptoPrice(req, res) {
|
||||||
let { token, chain, currency } = req.params
|
let { token, chain, currency, env } = req.params
|
||||||
if (!token || !chain) {
|
if (!token || !chain) {
|
||||||
throw new ZError(11, 'token or network not found')
|
throw new ZError(11, 'token or network not found')
|
||||||
}
|
}
|
||||||
|
if (env.toLowerCase() === 'dev' && (token.toLowerCase() === 'ceg' || token.toLowerCase() === 'cec')) {
|
||||||
|
return { price: 1 }
|
||||||
|
}
|
||||||
|
if (token.toLowerCase() === 'ceg') {
|
||||||
|
return { price: 1 }
|
||||||
|
}
|
||||||
let data = {
|
let data = {
|
||||||
crypto: token,
|
crypto: token,
|
||||||
network: chain,
|
network: chain,
|
||||||
@ -61,4 +83,13 @@ class AlchemyController extends BaseController {
|
|||||||
let result = await new PriceSvr().fetchPrice(data)
|
let result = await new PriceSvr().fetchPrice(data)
|
||||||
return { price: result }
|
return { price: result }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@router('get /pay/alchemy/fait_list')
|
||||||
|
async cryptoList(req, res) {
|
||||||
|
let result = await queryFiat()
|
||||||
|
if (!result.success) {
|
||||||
|
throw new ZError(10, result.returnMsg || 'fetch fiat list error')
|
||||||
|
}
|
||||||
|
return result.data
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,20 @@
|
|||||||
|
|
||||||
import logger from 'logger/logger'
|
import logger from 'logger/logger'
|
||||||
import BaseController, { ROLE_ANON } from 'common/base.controller'
|
import BaseController, { ROLE_ANON } from 'common/base.controller'
|
||||||
import { ZError } from 'common/ZError'
|
import { ZError } from 'common/ZError'
|
||||||
import { role, router } from 'decorators/router'
|
import { role, router } from 'decorators/router'
|
||||||
import { checkPayResultSign } from 'service/alchemy.svr'
|
import { checkPayResultSign, checkSimpleSign } from 'service/alchemy.svr'
|
||||||
import { PayRecord, PayStatus } from 'modules/PayRecord'
|
import { PayRecord, PayStatus } from 'modules/PayRecord'
|
||||||
|
import { TransferQueue } from 'queue/transfer.queue'
|
||||||
|
|
||||||
|
let errorRes = function (msg: string) {
|
||||||
|
return {
|
||||||
|
direct: 1,
|
||||||
|
data: null,
|
||||||
|
success: false,
|
||||||
|
returnCode: '9999',
|
||||||
|
returnMsg: msg,
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* for Alchemy call
|
* for Alchemy call
|
||||||
*/
|
*/
|
||||||
@ -22,7 +32,7 @@ class AlchemyOutController extends BaseController {
|
|||||||
logger.info(`alchemy callback record not found`)
|
logger.info(`alchemy callback record not found`)
|
||||||
throw new ZError(12, 'alchemy callback record not found')
|
throw new ZError(12, 'alchemy callback record not found')
|
||||||
}
|
}
|
||||||
if (record.status !== PayStatus.PENDING) {
|
if (record.status !== PayStatus.PENDING && record.status !== PayStatus.TRANSFERING) {
|
||||||
logger.info(`alchemy callback record status error`)
|
logger.info(`alchemy callback record status error`)
|
||||||
throw new ZError(13, 'alchemy callback record status error')
|
throw new ZError(13, 'alchemy callback record status error')
|
||||||
}
|
}
|
||||||
@ -34,8 +44,8 @@ class AlchemyOutController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
record.outOrderId = orderNo
|
record.outOrderId = orderNo
|
||||||
record.chain = network
|
record.network = network
|
||||||
record.currency = crypto
|
record.crypto = crypto
|
||||||
record.outData = req.params
|
record.outData = req.params
|
||||||
record.status = status == 'PAY_SUCCESS' ? PayStatus.SUCCESS : PayStatus.FAIL
|
record.status = status == 'PAY_SUCCESS' ? PayStatus.SUCCESS : PayStatus.FAIL
|
||||||
await record.save()
|
await record.save()
|
||||||
@ -51,20 +61,29 @@ class AlchemyOutController extends BaseController {
|
|||||||
async queryToken(req, res) {
|
async queryToken(req, res) {
|
||||||
const { crypto } = req.params
|
const { crypto } = req.params
|
||||||
const { appId, timestamp, sign } = req.headers
|
const { appId, timestamp, sign } = req.headers
|
||||||
|
if (!crypto) {
|
||||||
|
return errorRes('params mismatch')
|
||||||
|
}
|
||||||
|
if (!appId || !timestamp || !sign) {
|
||||||
|
return errorRes('headers mismatch')
|
||||||
|
}
|
||||||
|
if (!checkSimpleSign(req.headers)) {
|
||||||
|
return errorRes('sign error')
|
||||||
|
}
|
||||||
let result = {
|
let result = {
|
||||||
direct: 1,
|
direct: 1,
|
||||||
data: {
|
data: {
|
||||||
price: "1.0",
|
price: '1.0',
|
||||||
networkList: [
|
networkList: [
|
||||||
{
|
{
|
||||||
network: "ETH",
|
network: 'ETH',
|
||||||
networkFee: "1.21"
|
networkFee: '1.21',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
success: true,
|
success: true,
|
||||||
returnCode: "0000", // false: 9999
|
returnCode: '0000', // false: 9999
|
||||||
returnMsg: "in amet",
|
returnMsg: 'in amet',
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@ -76,16 +95,37 @@ class AlchemyOutController extends BaseController {
|
|||||||
@role(ROLE_ANON)
|
@role(ROLE_ANON)
|
||||||
@router('post /pay/out/alchemy/distribute')
|
@router('post /pay/out/alchemy/distribute')
|
||||||
async distributeToken(req, res) {
|
async distributeToken(req, res) {
|
||||||
const { appId, timestamp, sign } = req.headers
|
|
||||||
const { orderNo, crypto, network, address, cryptoAmount, cryptoPrice, usdtAmount } = req.params
|
const { orderNo, crypto, network, address, cryptoAmount, cryptoPrice, usdtAmount } = req.params
|
||||||
|
const { appId, timestamp, sign } = req.headers
|
||||||
|
if (!orderNo || !crypto || !network || !address || !cryptoAmount || !cryptoPrice || !usdtAmount) {
|
||||||
|
return errorRes('params mismatch')
|
||||||
|
}
|
||||||
|
if (!appId || !timestamp || !sign) {
|
||||||
|
return errorRes('headers mismatch')
|
||||||
|
}
|
||||||
|
if (!checkSimpleSign(req.headers)) {
|
||||||
|
return errorRes('sign error')
|
||||||
|
}
|
||||||
|
|
||||||
|
let record = await PayRecord.findByRecordId(orderNo)
|
||||||
|
if (!record) {
|
||||||
|
return errorRes('orderNo not found')
|
||||||
|
}
|
||||||
|
if (record.crypto != crypto || record.network != network || record.address != address) {
|
||||||
|
return errorRes('params mismatch')
|
||||||
|
}
|
||||||
|
record.cryptoAmount = cryptoAmount
|
||||||
|
record.cryptoPrice = cryptoPrice
|
||||||
|
record.usdtAdmount = usdtAmount
|
||||||
|
await record.save()
|
||||||
|
new TransferQueue().addTask(record)
|
||||||
let result = {
|
let result = {
|
||||||
direct: 1,
|
direct: 1,
|
||||||
data: null,
|
data: null,
|
||||||
success: true,
|
success: true,
|
||||||
returnCode: "0000", // false: 9999
|
returnCode: '0000', // false: 9999
|
||||||
returnMsg: "in amet",
|
returnMsg: 'in amet',
|
||||||
}
|
}
|
||||||
res.send(result)
|
res.send(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
83
src/controllers/internal.controller.ts
Normal file
83
src/controllers/internal.controller.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { ZError } from 'common/ZError'
|
||||||
|
import BaseController, { ROLE_ANON } from 'common/base.controller'
|
||||||
|
import { role, router } from 'decorators/router'
|
||||||
|
import logger from 'logger/logger'
|
||||||
|
import { PayRecord, PayRecordClass, PayStatus } from 'modules/PayRecord'
|
||||||
|
import { TransferRecord, TransferRecordClass } from 'modules/TransferRecord'
|
||||||
|
import { hmacsha256 } from 'utils/security.util'
|
||||||
|
import { DocumentType } from '@typegoose/typegoose'
|
||||||
|
import { queryPrice, updateOrderStatus } from 'service/alchemy.svr'
|
||||||
|
|
||||||
|
const calcHash = function (data: any) {
|
||||||
|
let signStr = JSON.stringify(data)
|
||||||
|
return hmacsha256(signStr, process.env.HASH_SALT)
|
||||||
|
}
|
||||||
|
|
||||||
|
const notify = async function (record: DocumentType<PayRecordClass>, subTask: DocumentType<TransferRecordClass>) {
|
||||||
|
let data: any = {
|
||||||
|
orderNo: record.outOrderId, // AlchemyPay订单号
|
||||||
|
crypto: record.crypto, // 用户购买的数字货币
|
||||||
|
cryptoAmount: record.cryptoAmount, //用户的提币数量
|
||||||
|
cryptoPrice: record.cryptoPrice, // 实时的价格/USDT CEG锚定USDT
|
||||||
|
txHash: record.txHash, // 给用户转账的hash
|
||||||
|
network: record.network, // 用户购买的数字货币对应的网络
|
||||||
|
// networkFee: record.networkFee, // 网络费用/USDT
|
||||||
|
address: record.address, // 用户的提币地址
|
||||||
|
status: 'SUCCESS', // SUCCESS/FAIL
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
let priceData = await queryPrice({ gas: subTask.gas })
|
||||||
|
data.networkFee = priceData.leagel
|
||||||
|
let result = await updateOrderStatus(data)
|
||||||
|
logger.info('update transfer status success::', JSON.stringify(result))
|
||||||
|
if (result.success) {
|
||||||
|
subTask.status = 8
|
||||||
|
await subTask.save()
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`notify alchemy error:: ${err.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class InternalController extends BaseController {
|
||||||
|
@role(ROLE_ANON)
|
||||||
|
@router('post /api/internal/update_task')
|
||||||
|
async updateTaskInfo(req) {
|
||||||
|
let { sign, id, result, successCount, errorCount, gas, gasPrice, hashList } = req.params
|
||||||
|
if (!sign) {
|
||||||
|
throw new ZError(10, 'sign not found')
|
||||||
|
}
|
||||||
|
let hash = calcHash({ id, result, successCount, errorCount, hashList })
|
||||||
|
console.log(hash, sign)
|
||||||
|
if (sign !== hash) {
|
||||||
|
throw new ZError(11, 'sign not match')
|
||||||
|
}
|
||||||
|
logger.info(`task report:: ${id}|${result}|${successCount}|${errorCount}|${JSON.stringify(hashList)}}`)
|
||||||
|
if (!id) {
|
||||||
|
throw new ZError(11, 'taskId not found')
|
||||||
|
}
|
||||||
|
let record = await TransferRecord.findById(id)
|
||||||
|
if (!record) {
|
||||||
|
throw new ZError(12, 'TransferRecord not found')
|
||||||
|
}
|
||||||
|
let task = await PayRecord.findById(record.recordId)
|
||||||
|
if (!task) {
|
||||||
|
throw new ZError(13, 'PayRecord not found')
|
||||||
|
}
|
||||||
|
if (result === 2) {
|
||||||
|
record.status = 9
|
||||||
|
record.gas = gas
|
||||||
|
record.gasPrice = gasPrice
|
||||||
|
record.hashList = hashList
|
||||||
|
task.txHash = hashList[0]
|
||||||
|
task.status = PayStatus.TRANSFERED
|
||||||
|
setImmediate(notify.apply(this, [task]))
|
||||||
|
} else {
|
||||||
|
record.status = 10
|
||||||
|
task.status = PayStatus.TRANSFER_FAIL
|
||||||
|
}
|
||||||
|
await record.save()
|
||||||
|
await task.save()
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { getModelForClass, index, modelOptions, mongoose, prop, Severity } from '@typegoose/typegoose'
|
import { getModelForClass, index, modelOptions, mongoose, prop, ReturnModelType, Severity } from '@typegoose/typegoose'
|
||||||
import { dbconn } from 'decorators/dbconn'
|
import { dbconn } from 'decorators/dbconn'
|
||||||
import { BaseModule } from './Base'
|
import { BaseModule } from './Base'
|
||||||
|
|
||||||
@ -13,16 +13,20 @@ export enum PayType {
|
|||||||
|
|
||||||
export enum PayStatus {
|
export enum PayStatus {
|
||||||
PENDING = 0,
|
PENDING = 0,
|
||||||
SUCCESS = 1,
|
TRANSFERING = 1, //只有国库模式才会有该状态
|
||||||
FAIL = 2,
|
TRANSFERED = 2, //只有国库模式才会有该状态
|
||||||
|
SUCCESS = 9,
|
||||||
|
TRANSFER_FAIL = 98, // 转账错误
|
||||||
|
FAIL = 99,
|
||||||
}
|
}
|
||||||
|
|
||||||
@dbconn()
|
@dbconn()
|
||||||
|
@index({ outOrderId: 1 }, { unique: true, partialFilterExpression: { outOrderId: { $exists: true } } })
|
||||||
@modelOptions({
|
@modelOptions({
|
||||||
schemaOptions: { collection: 'pay_record', timestamps: true },
|
schemaOptions: { collection: 'pay_record', timestamps: true },
|
||||||
options: { allowMixed: Severity.ALLOW },
|
options: { allowMixed: Severity.ALLOW },
|
||||||
})
|
})
|
||||||
class PayRecordClass extends BaseModule {
|
export class PayRecordClass extends BaseModule {
|
||||||
@prop({ enum: PayPlatEnum, default: PayPlatEnum.ALCHEMY })
|
@prop({ enum: PayPlatEnum, default: PayPlatEnum.ALCHEMY })
|
||||||
public channel!: PayPlatEnum
|
public channel!: PayPlatEnum
|
||||||
|
|
||||||
@ -36,10 +40,30 @@ class PayRecordClass extends BaseModule {
|
|||||||
public address: string
|
public address: string
|
||||||
|
|
||||||
@prop()
|
@prop()
|
||||||
public chain: string
|
public network?: string
|
||||||
|
|
||||||
@prop()
|
@prop()
|
||||||
public currency?: string
|
public crypto?: string
|
||||||
|
|
||||||
|
// 法币
|
||||||
|
@prop()
|
||||||
|
public fiat?: string
|
||||||
|
// 法币数量
|
||||||
|
@prop()
|
||||||
|
public fiatAmount?: string
|
||||||
|
// 加密货币数量
|
||||||
|
@prop()
|
||||||
|
public cryptoAmount?: string
|
||||||
|
|
||||||
|
// 加密货币价格
|
||||||
|
@prop()
|
||||||
|
public cryptoPrice?: string
|
||||||
|
// 该笔交易渠道会给我们多少usdt
|
||||||
|
@prop()
|
||||||
|
public usdtAdmount?: string
|
||||||
|
// 国家
|
||||||
|
@prop()
|
||||||
|
public country?: string
|
||||||
|
|
||||||
@prop({ required: true, default: PayStatus.PENDING })
|
@prop({ required: true, default: PayStatus.PENDING })
|
||||||
public status: PayStatus
|
public status: PayStatus
|
||||||
@ -52,7 +76,11 @@ class PayRecordClass extends BaseModule {
|
|||||||
public outOrderId: string
|
public outOrderId: string
|
||||||
// 交易的txHash
|
// 交易的txHash
|
||||||
@prop()
|
@prop()
|
||||||
public txHash: string
|
public txHash?: string
|
||||||
|
|
||||||
|
public static async findByRecordId(this: ReturnModelType<typeof PayRecordClass>, outOrderid: string) {
|
||||||
|
return this.findOne({ outOrderid }).exec()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PayRecord = getModelForClass(PayRecordClass, { existingConnection: PayRecordClass.db })
|
export const PayRecord = getModelForClass(PayRecordClass, { existingConnection: PayRecordClass.db })
|
||||||
|
73
src/modules/TransferRecord.ts
Normal file
73
src/modules/TransferRecord.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { ReturnModelType, getModelForClass, index, modelOptions, prop } from '@typegoose/typegoose'
|
||||||
|
import { dbconn } from 'decorators/dbconn'
|
||||||
|
import { BaseModule } from './Base'
|
||||||
|
import { PayType } from './PayRecord'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付打款记录
|
||||||
|
*/
|
||||||
|
@dbconn()
|
||||||
|
@index({ recordId: 1 }, { unique: true })
|
||||||
|
@modelOptions({ schemaOptions: { collection: 'pay_transfer_record', timestamps: true } })
|
||||||
|
export class TransferRecordClass extends BaseModule {
|
||||||
|
@prop({ required: true })
|
||||||
|
public account: string
|
||||||
|
|
||||||
|
// pay_record 的 id
|
||||||
|
@prop()
|
||||||
|
public recordId: string
|
||||||
|
|
||||||
|
@prop({ required: true, default: PayType.BUY })
|
||||||
|
public type: PayType
|
||||||
|
|
||||||
|
@prop()
|
||||||
|
public chain: number
|
||||||
|
|
||||||
|
// token 的合约地址
|
||||||
|
@prop()
|
||||||
|
public contract: string
|
||||||
|
|
||||||
|
// 用户的钱包地址
|
||||||
|
@prop()
|
||||||
|
public to: string
|
||||||
|
|
||||||
|
// 转出钱包
|
||||||
|
@prop()
|
||||||
|
public from: string
|
||||||
|
|
||||||
|
@prop()
|
||||||
|
public amount: string
|
||||||
|
|
||||||
|
@prop()
|
||||||
|
public gas: string
|
||||||
|
|
||||||
|
@prop()
|
||||||
|
public gasPrice: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 0: 队列中, 等待上链
|
||||||
|
* 1: 已请求上链, 等待结果
|
||||||
|
* 2: 成功上链, 等待确认
|
||||||
|
* 9: 已确认成功(成功的最终状态)
|
||||||
|
* 8: 已上报状态
|
||||||
|
* 10: 失败
|
||||||
|
* 11: 部分失败
|
||||||
|
*/
|
||||||
|
@prop({ default: 0 })
|
||||||
|
public status: number
|
||||||
|
|
||||||
|
@prop({ default: 0 })
|
||||||
|
public errorCount: number
|
||||||
|
|
||||||
|
@prop({ type: () => [String] })
|
||||||
|
public hashList: string[]
|
||||||
|
|
||||||
|
@prop({ default: 0 })
|
||||||
|
public version: number
|
||||||
|
|
||||||
|
public static async findByRecordId(this: ReturnModelType<typeof TransferRecordClass>, recordId: string) {
|
||||||
|
return this.findOne({ recordId }).exec()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TransferRecord = getModelForClass(TransferRecordClass, { existingConnection: TransferRecordClass.db })
|
82
src/queue/transfer.queue.ts
Normal file
82
src/queue/transfer.queue.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import { singleton } from 'decorators/singleton'
|
||||||
|
import { AsyncQueue, createAsyncQueue } from 'common/AsyncQueue'
|
||||||
|
import { DocumentType } from '@typegoose/typegoose'
|
||||||
|
import { PayRecordClass } from 'modules/PayRecord'
|
||||||
|
import logger from 'logger/logger'
|
||||||
|
import { pushTaskToChain } from 'service/chain.svr'
|
||||||
|
import { TransferRecord } from 'modules/TransferRecord'
|
||||||
|
import config from 'config/config'
|
||||||
|
import assert from 'assert'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* let data = {
|
||||||
|
taskId: '1',
|
||||||
|
source: 'pay',
|
||||||
|
cb: 'status update callback url',
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
address: '0xb592244aa6477eBDDc14475aaeF921cdDcC0170f',
|
||||||
|
from: '',
|
||||||
|
to: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1',
|
||||||
|
amount: 1000,
|
||||||
|
type: 3
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
@singleton
|
||||||
|
export class TransferQueue {
|
||||||
|
private queue: AsyncQueue
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.queue = createAsyncQueue()
|
||||||
|
}
|
||||||
|
|
||||||
|
public addTask(task: DocumentType<PayRecordClass>) {
|
||||||
|
this.queue.push(async () => {
|
||||||
|
try {
|
||||||
|
let chainCfg = config.chainCfgs[task.network.toLowerCase()]
|
||||||
|
assert(chainCfg, `chain config not found: ${task.network}`)
|
||||||
|
let chainId = chainCfg.chainId
|
||||||
|
let wallet = chainCfg.wallet
|
||||||
|
let address = chainCfg.tokens[task.crypto]
|
||||||
|
assert(address, `token address not found: ${task.crypto}`)
|
||||||
|
let record = await TransferRecord.insertOrUpdate(
|
||||||
|
{ recordId: task.id },
|
||||||
|
{
|
||||||
|
account: task.account,
|
||||||
|
chain: chainId,
|
||||||
|
contract: address,
|
||||||
|
to: task.address,
|
||||||
|
from: wallet,
|
||||||
|
amount: task.cryptoAmount,
|
||||||
|
$inc: { version: 1 },
|
||||||
|
},
|
||||||
|
)
|
||||||
|
let datas: any = [
|
||||||
|
{
|
||||||
|
chain: record.chain,
|
||||||
|
address: record.contract,
|
||||||
|
from: record.from,
|
||||||
|
to: record.to,
|
||||||
|
amount: record.amount,
|
||||||
|
type: 3,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
let reqData = {
|
||||||
|
taskId: record.id,
|
||||||
|
source: 'pay',
|
||||||
|
data: datas,
|
||||||
|
cb: process.env.PAY_TRANSFER_CB_URL,
|
||||||
|
}
|
||||||
|
await pushTaskToChain(reqData)
|
||||||
|
|
||||||
|
await task.save()
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('error add chain task: ')
|
||||||
|
logger.error(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,13 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { sha1 } from 'utils/security.util'
|
import { hmacsha256, 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)
|
||||||
|
let sign = hmacsha256(appid + timestamp, secret)
|
||||||
return {
|
return {
|
||||||
appid,
|
appid,
|
||||||
timestamp,
|
timestamp,
|
||||||
@ -21,10 +22,18 @@ export function createSimpleSign() {
|
|||||||
*/
|
*/
|
||||||
export function checkPayResultSign(data: any) {
|
export function checkPayResultSign(data: any) {
|
||||||
const { appId, orderNo, crypto, network, address, signature } = data
|
const { appId, orderNo, crypto, network, address, signature } = data
|
||||||
const sign = sha1(appId + process.env.ALCHEMY_APP_SECRET + appId + orderNo + crypto + network + address)
|
const sign = hmacsha256(appId + orderNo + crypto + network + address, process.env.ALCHEMY_APP_SECRET)
|
||||||
return sign === signature
|
return sign === signature
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function checkSimpleSign(data: any) {
|
||||||
|
// alchemy 很不严谨, 有时候是 appid, 有时候是 appId
|
||||||
|
const { appid, appId, timestamp, sign } = data
|
||||||
|
let appIdToCheck = appId || appid
|
||||||
|
const expectedSign = sha1(appIdToCheck + process.env.ALCHEMY_APP_SECRET + timestamp)
|
||||||
|
return sign === expectedSign
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create page sign
|
* Create page sign
|
||||||
* @param plainText - plain text to be encrypted
|
* @param plainText - plain text to be encrypted
|
||||||
@ -51,6 +60,7 @@ export function createPageSign(plainText: string) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Refresh token
|
* Refresh token
|
||||||
|
* https://alchemycn.readme.io/docs/获取token
|
||||||
* @param email - user email
|
* @param email - user email
|
||||||
* @returns token
|
* @returns token
|
||||||
*/
|
*/
|
||||||
@ -101,7 +111,7 @@ export async function createOrder(token: string, data: any) {
|
|||||||
* https://alchemycn.readme.io/docs/更新订单状态
|
* https://alchemycn.readme.io/docs/更新订单状态
|
||||||
* TODO:: test
|
* TODO:: test
|
||||||
*/
|
*/
|
||||||
export async function updateOrderStatus(token: string, data: any) {
|
export async function updateOrderStatus(data: any) {
|
||||||
const { appid, timestamp, sign } = createSimpleSign()
|
const { appid, timestamp, sign } = createSimpleSign()
|
||||||
const url = process.env.ALCHEMY_API_BASE + '/webhooks/treasure'
|
const url = process.env.ALCHEMY_API_BASE + '/webhooks/treasure'
|
||||||
const config = {
|
const config = {
|
||||||
@ -109,7 +119,6 @@ export async function updateOrderStatus(token: string, data: any) {
|
|||||||
url,
|
url,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'access-token': token,
|
|
||||||
appId: appid,
|
appId: appid,
|
||||||
sign: sign,
|
sign: sign,
|
||||||
timestamp,
|
timestamp,
|
||||||
@ -121,6 +130,7 @@ export async function updateOrderStatus(token: string, data: any) {
|
|||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 查询数字货币币价
|
* 查询数字货币币价
|
||||||
|
* https://alchemycn.readme.io/docs/查询数字货币币价
|
||||||
*/
|
*/
|
||||||
export async function queryPrice(data: any) {
|
export async function queryPrice(data: any) {
|
||||||
const { appid, timestamp, sign } = createSimpleSign()
|
const { appid, timestamp, sign } = createSimpleSign()
|
||||||
@ -147,3 +157,28 @@ export async function queryPrice(data: any) {
|
|||||||
let response = await axios(config)
|
let response = await axios(config)
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询法币及支付方式
|
||||||
|
* https://alchemycn.readme.io/docs/查询法币及支付方式
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function queryFiat() {
|
||||||
|
const { appid, timestamp, sign } = createSimpleSign()
|
||||||
|
let dataOrign = {
|
||||||
|
type: 'BUY',
|
||||||
|
}
|
||||||
|
const config = {
|
||||||
|
method: 'GET',
|
||||||
|
url: process.env.ALCHEMY_API_BASE + '/merchant/fiat/list',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
appId: appid,
|
||||||
|
sign,
|
||||||
|
timestamp,
|
||||||
|
},
|
||||||
|
data: dataOrign,
|
||||||
|
}
|
||||||
|
let response = await axios(config)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
31
src/service/chain.svr.ts
Normal file
31
src/service/chain.svr.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const ADD_TASK_URI = '/chain/req'
|
||||||
|
|
||||||
|
const GAS_PRICE_URI = '/chain/estimate_gas'
|
||||||
|
|
||||||
|
export async function pushTaskToChain(data) {
|
||||||
|
let url = `${process.env.CHAIN_CLIENT_URL}${ADD_TASK_URI}`
|
||||||
|
let reqConfig: any = {
|
||||||
|
method: 'post',
|
||||||
|
url,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
data: JSON.stringify(data),
|
||||||
|
}
|
||||||
|
return axios(reqConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGasPrice(data) {
|
||||||
|
let url = `${process.env.CHAIN_CLIENT_URL}${GAS_PRICE_URI}`
|
||||||
|
let reqConfig: any = {
|
||||||
|
method: 'post',
|
||||||
|
url,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
data: JSON.stringify(data),
|
||||||
|
}
|
||||||
|
return axios(reqConfig)
|
||||||
|
}
|
@ -24,6 +24,13 @@ export function aesDecrypt(encryptedText: string, password: string, iv: string)
|
|||||||
return decrypted + decipher.final('utf8')
|
return decrypted + decipher.final('utf8')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hmacsha256(text: string, secret: string) {
|
||||||
|
const mac = crypto.createHmac('sha256', secret)
|
||||||
|
const data = mac.update(text).digest('hex').toLowerCase()
|
||||||
|
console.log(`HmacSHA256 rawContent is [${text}], key is [${secret}], hash result is [${data}]`)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
export function sha512(password: string, salt: string) {
|
export function sha512(password: string, salt: string) {
|
||||||
let hash = crypto.createHmac('sha512', salt)
|
let hash = crypto.createHmac('sha512', salt)
|
||||||
hash.update(password)
|
hash.update(password)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user