增加查询crypto兑换美元价格的接口
This commit is contained in:
parent
5fbd59b738
commit
5220dd2232
@ -17,3 +17,7 @@ ALCHEMY_APP_SECRET="4Yn8RkxDXN71Q3p0"
|
|||||||
ALCHEMY_API_BASE="https://openapi-test.alchemypay.org"
|
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"
|
||||||
|
|
||||||
|
|
||||||
|
#cryptocompare api key from metamask^_^
|
||||||
|
CRYPTOCOMPARE_API_KEY=d1ec8cd68228095debc9db2dca45771b905ce1f27f522ebfef025c236f4aef3b
|
@ -7,6 +7,7 @@ import logger from 'logger/logger'
|
|||||||
import config from 'config/config'
|
import config from 'config/config'
|
||||||
import { ConnectOptions } from 'mongoose'
|
import { ConnectOptions } from 'mongoose'
|
||||||
import CodeTaskSchedule from 'schedule/codetask.schedule'
|
import CodeTaskSchedule from 'schedule/codetask.schedule'
|
||||||
|
import { PriceSvr } from 'service/price.svr'
|
||||||
|
|
||||||
const zReqParserPlugin = require('plugins/zReqParser')
|
const zReqParserPlugin = require('plugins/zReqParser')
|
||||||
|
|
||||||
@ -87,6 +88,7 @@ export class ApiServer {
|
|||||||
|
|
||||||
initSchedules() {
|
initSchedules() {
|
||||||
new CodeTaskSchedule().scheduleAll()
|
new CodeTaskSchedule().scheduleAll()
|
||||||
|
new PriceSvr().scheduleAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
async connectDB() {
|
async connectDB() {
|
||||||
@ -131,8 +133,8 @@ export class ApiServer {
|
|||||||
/**
|
/**
|
||||||
* 格式化接口返回数据, 统一封装成如下格式
|
* 格式化接口返回数据, 统一封装成如下格式
|
||||||
* {
|
* {
|
||||||
* code: 0,
|
* errcode: 0,
|
||||||
* msg?: '',
|
* errmsg?: '',
|
||||||
* data: any
|
* data: any
|
||||||
* }
|
* }
|
||||||
* @private
|
* @private
|
||||||
@ -142,6 +144,12 @@ export class ApiServer {
|
|||||||
reply.header('X-Powered-By', 'PHP/5.4.16')
|
reply.header('X-Powered-By', 'PHP/5.4.16')
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (!payload.errcode) {
|
if (!payload.errcode) {
|
||||||
|
// @ts-ignore
|
||||||
|
if (payload.direct) {
|
||||||
|
// @ts-ignore
|
||||||
|
delete payload.direct
|
||||||
|
return payload
|
||||||
|
}
|
||||||
payload = {
|
payload = {
|
||||||
errcode: 0,
|
errcode: 0,
|
||||||
data: payload,
|
data: payload,
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import logger from 'logger/logger'
|
import logger from 'logger/logger'
|
||||||
import BaseController, { ROLE_ANON } from 'common/base.controller'
|
import BaseController from 'common/base.controller'
|
||||||
import { ZError } from 'common/ZError'
|
import { ZError } from 'common/ZError'
|
||||||
import { role, router } from 'decorators/router'
|
import { router } from 'decorators/router'
|
||||||
import { checkPayResultSign, createPageSign, refreshToken } from 'service/alchemy.svr'
|
import { createPageSign, 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'
|
||||||
|
|
||||||
const CALL_BACK_URL = `${process.env.ALCHEMY_PAY_CB_URL}/pay/alchemy/cb`
|
const CALL_BACK_URL = `${process.env.ALCHEMY_PAY_CB_URL}/pay/out/alchemy/buycb`
|
||||||
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) {
|
||||||
@ -19,7 +20,6 @@ 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
|
||||||
const redirectUrl = ''
|
|
||||||
let record = new PayRecord({ account: user.id, address, chain, currency })
|
let record = new PayRecord({ account: user.id, address, chain, currency })
|
||||||
await record.save()
|
await record.save()
|
||||||
const merchantOrderNo = record.id
|
const merchantOrderNo = record.id
|
||||||
@ -47,36 +47,18 @@ class AlchemyController extends BaseController {
|
|||||||
return { url }
|
return { url }
|
||||||
}
|
}
|
||||||
|
|
||||||
@role(ROLE_ANON)
|
@router('post /pay/alchemy/crypto_price')
|
||||||
@router('post /pay/alchemy/cb')
|
async queryCryptoPrice(req, res) {
|
||||||
async alchemyCallback(req, res) {
|
let { token, chain, currency } = req.params
|
||||||
let { orderNo, status, crypto, network, merchantOrderNo } = req.params
|
if (!token || !chain) {
|
||||||
if (!merchantOrderNo) {
|
throw new ZError(11, 'token or network not found')
|
||||||
logger.info(`alchemy callback merchantOrderNo not found`)
|
|
||||||
throw new ZError(11, 'alchemy callback merchantOrderNo not found')
|
|
||||||
}
|
}
|
||||||
let record = await PayRecord.findById(merchantOrderNo)
|
let data = {
|
||||||
if (!record) {
|
crypto: token,
|
||||||
logger.info(`alchemy callback record not found`)
|
network: chain,
|
||||||
throw new ZError(12, 'alchemy callback record not found')
|
fiat: currency || 'USD',
|
||||||
}
|
}
|
||||||
if (record.status !== PayStatus.PENDING) {
|
let result = await new PriceSvr().fetchPrice(data)
|
||||||
logger.info(`alchemy callback record status error`)
|
return { price: result }
|
||||||
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 {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
91
src/controllers/alchemyout.controller.ts
Normal file
91
src/controllers/alchemyout.controller.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
|
||||||
|
import logger from 'logger/logger'
|
||||||
|
import BaseController, { ROLE_ANON } from 'common/base.controller'
|
||||||
|
import { ZError } from 'common/ZError'
|
||||||
|
import { role, router } from 'decorators/router'
|
||||||
|
import { checkPayResultSign } from 'service/alchemy.svr'
|
||||||
|
import { PayRecord, PayStatus } from 'modules/PayRecord'
|
||||||
|
/**
|
||||||
|
* for Alchemy call
|
||||||
|
*/
|
||||||
|
class AlchemyOutController extends BaseController {
|
||||||
|
@role(ROLE_ANON)
|
||||||
|
@router('post /pay/out/alchemy/buycb')
|
||||||
|
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 {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向商户查询币价
|
||||||
|
* TODO::
|
||||||
|
*/
|
||||||
|
@role(ROLE_ANON)
|
||||||
|
@router('get /pay/out/alchemy/queryprice')
|
||||||
|
async queryToken(req, res) {
|
||||||
|
const { crypto } = req.params
|
||||||
|
const { appId, timestamp, sign } = req.headers
|
||||||
|
let result = {
|
||||||
|
direct: 1,
|
||||||
|
data: {
|
||||||
|
price: "1.0",
|
||||||
|
networkList: [
|
||||||
|
{
|
||||||
|
network: "ETH",
|
||||||
|
networkFee: "1.21"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
success: true,
|
||||||
|
returnCode: "0000", // false: 9999
|
||||||
|
returnMsg: "in amet",
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知商户打币
|
||||||
|
* TODO::
|
||||||
|
*/
|
||||||
|
@role(ROLE_ANON)
|
||||||
|
@router('post /pay/out/alchemy/distribute')
|
||||||
|
async distributeToken(req, res) {
|
||||||
|
const { appId, timestamp, sign } = req.headers
|
||||||
|
const { orderNo, crypto, network, address, cryptoAmount, cryptoPrice, usdtAmount } = req.params
|
||||||
|
let result = {
|
||||||
|
direct: 1,
|
||||||
|
data: null,
|
||||||
|
success: true,
|
||||||
|
returnCode: "0000", // false: 9999
|
||||||
|
returnMsg: "in amet",
|
||||||
|
}
|
||||||
|
res.send(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -72,7 +72,10 @@ export async function refreshToken(email: string) {
|
|||||||
let response = await axios(config)
|
let response = await axios(config)
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 创建订单
|
||||||
|
* https://alchemycn.readme.io/docs/创建订单
|
||||||
|
*/
|
||||||
export async function createOrder(token: string, data: any) {
|
export async function createOrder(token: string, data: any) {
|
||||||
const { appid, timestamp, sign } = createSimpleSign()
|
const { appid, timestamp, sign } = createSimpleSign()
|
||||||
const host = process.env.ALCHEMY_API_BASE
|
const host = process.env.ALCHEMY_API_BASE
|
||||||
@ -91,3 +94,56 @@ export async function createOrder(token: string, data: any) {
|
|||||||
let response = await axios(config)
|
let response = await axios(config)
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新订单状态
|
||||||
|
* 当您给用户转移token后,您需要通过此接口通知Alchemy打币的信息
|
||||||
|
* https://alchemycn.readme.io/docs/更新订单状态
|
||||||
|
* TODO:: test
|
||||||
|
*/
|
||||||
|
export async function updateOrderStatus(token: string, data: any) {
|
||||||
|
const { appid, timestamp, sign } = createSimpleSign()
|
||||||
|
const url = process.env.ALCHEMY_API_BASE + '/webhooks/treasure'
|
||||||
|
const config = {
|
||||||
|
method: 'post',
|
||||||
|
url,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'access-token': token,
|
||||||
|
appId: appid,
|
||||||
|
sign: sign,
|
||||||
|
timestamp,
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
let response = await axios(config)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 查询数字货币币价
|
||||||
|
*/
|
||||||
|
export async function queryPrice(data: any) {
|
||||||
|
const { appid, timestamp, sign } = createSimpleSign()
|
||||||
|
let dataOrign = {
|
||||||
|
crypto: 'ETH',
|
||||||
|
network: 'ETH',
|
||||||
|
fiat: 'USD',
|
||||||
|
amount: '100',
|
||||||
|
payWayCode: '10001',
|
||||||
|
side: 'BUY',
|
||||||
|
}
|
||||||
|
dataOrign = { ...dataOrign, ...data }
|
||||||
|
const config = {
|
||||||
|
method: 'post',
|
||||||
|
url: process.env.ALCHEMY_API_BASE + '/merchant/order/quote',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
appId: appid,
|
||||||
|
sign,
|
||||||
|
timestamp,
|
||||||
|
},
|
||||||
|
data: dataOrign,
|
||||||
|
}
|
||||||
|
let response = await axios(config)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
65
src/service/price.svr.ts
Normal file
65
src/service/price.svr.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import { singleton } from 'decorators/singleton'
|
||||||
|
import { queryPrice } from './alchemy.svr'
|
||||||
|
import * as schedule from 'node-schedule'
|
||||||
|
import logger from 'logger/logger'
|
||||||
|
|
||||||
|
@singleton
|
||||||
|
export class PriceSvr {
|
||||||
|
private priceMap: Map<string, string> = new Map()
|
||||||
|
|
||||||
|
public async queryEthPrice(eth: string) {
|
||||||
|
const usd = 'USD'
|
||||||
|
const apiKey = process.env.CRYPTOCOMPARE_API_KEY
|
||||||
|
const url = `https://min-api.cryptocompare.com/data/price?fsym=${eth}&tsyms=${usd}&api_key=${apiKey}`
|
||||||
|
let priceData = await axios.get(url).then(res => res.data)
|
||||||
|
let price = priceData[usd] + ''
|
||||||
|
return price
|
||||||
|
}
|
||||||
|
|
||||||
|
public async fetchPrice(data: any) {
|
||||||
|
let { crypto, network, fiat } = data
|
||||||
|
let key = `${crypto}_${network}_${fiat}`
|
||||||
|
let price = this.priceMap.get(key)
|
||||||
|
if (!price) {
|
||||||
|
await this.refreshOne(data)
|
||||||
|
price = this.priceMap.get(key)
|
||||||
|
}
|
||||||
|
return price
|
||||||
|
}
|
||||||
|
|
||||||
|
private async refreshOne(data) {
|
||||||
|
let { crypto, network, fiat } = data
|
||||||
|
let key = `${crypto}_${network}_${fiat}`
|
||||||
|
try {
|
||||||
|
let priceData = await queryPrice(data)
|
||||||
|
this.priceMap.set(key, priceData.data.cryptoPrice + '')
|
||||||
|
} catch (e) {
|
||||||
|
logger.debug('update eth price with alchemy error: ' + e.message || e)
|
||||||
|
try {
|
||||||
|
let price = await this.queryEthPrice(crypto)
|
||||||
|
this.priceMap.set(key, price + '')
|
||||||
|
} catch (e) {
|
||||||
|
logger.debug('update eth price with cryptocompare error: ' + e.message || e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async refreshAll() {
|
||||||
|
for (let key of this.priceMap.keys()) {
|
||||||
|
let [crypto, chain, fiat] = key.split('_')
|
||||||
|
let data = {
|
||||||
|
crypto,
|
||||||
|
network: chain,
|
||||||
|
fiat,
|
||||||
|
}
|
||||||
|
await this.refreshOne(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleAll() {
|
||||||
|
const job = schedule.scheduleJob('*/30 * * * * *', async () => {
|
||||||
|
this.refreshAll()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user