diff --git a/.env.development b/.env.development index 24126bd..25a2482 100644 --- a/.env.development +++ b/.env.development @@ -14,8 +14,11 @@ EMAIL_VERIFY_URL="https://wallet.cebggame.com" ALCHEMY_APPID="f83Is2y7L425rxl8" ALCHEMY_APP_SECRET="4Yn8RkxDXN71Q3p0" -ALCHEMY_API_BASE="https://openapi-test.alchemypay.org" -ALCHEMY_PAGE_BASE="https://ramptest.alchemypay.org" +# ALCHEMY_API_BASE="https://openapi-test.alchemypay.org" +# ALCHEMY_PAGE_BASE="https://ramptest.alchemypay.org" +ALCHEMY_API_BASE="http://127.0.0.1:3009" +ALCHEMY_PAGE_BASE="http://127.0.0.1:3009/pay_page" + ALCHEMY_PAY_CB_URL="https://wallet.cebggame.com" CHAIN_CLIENT_URL=http://127.0.0.1:3006 @@ -29,10 +32,12 @@ AVAILABLE_TOKENS=cec|ceg ARBITRUM_CHAIN_ID=421163 ARBITRUM_CEC_ADDRESS='0xaa34B79A0Ab433eaC900fB3CB9f191F5Cd27501D' ARBITRUM_CEG_ADDRESS='0xaa34B79A0Ab433eaC900fB3CB9f191F5Cd27501D' -ARBITRUM_WALLET='' +ARBITRUM_WALLET='0x50A8e60041A206AcaA5F844a1104896224be6F39' BSC_CHAIN_ID=97 BSC_CEC_ADDRESS='0xaa34B79A0Ab433eaC900fB3CB9f191F5Cd27501D' BSC_CEG_ADDRESS='0xaa34B79A0Ab433eaC900fB3CB9f191F5Cd27501D' -BSC_WALLET='' +BSC_WALLET='0x50A8e60041A206AcaA5F844a1104896224be6F39' # 链端转账回调地址 -PAY_TRANSFER_CB_URL='' \ No newline at end of file +PAY_TRANSFER_CB_URL='http://127.0.0.1:3007/api/internal/update_task' +# 链端回调hash的ket +HASH_SALT='iG4Rpsa)6U31$H#^T85$^^3' \ No newline at end of file diff --git a/src/config/config.ts b/src/config/config.ts index 18e54e0..63454ed 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -36,9 +36,9 @@ 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()}`] + data.wallet = process.env[`${network.toUpperCase()}_WALLET`] for (let sub of tokenList) { - data.tokens[sub] = process.env[`ADDRESS_${sub.toUpperCase()}_${network.toUpperCase()}`] + data.tokens[sub] = process.env[`${network.toUpperCase()}_${sub.toUpperCase()}_ADDRESS`] } chainCfgs[network] = data } diff --git a/src/controllers/alchemy.controller.ts b/src/controllers/alchemy.controller.ts index fe156d8..0ffe504 100644 --- a/src/controllers/alchemy.controller.ts +++ b/src/controllers/alchemy.controller.ts @@ -2,7 +2,7 @@ import logger from 'logger/logger' import BaseController from 'common/base.controller' import { ZError } from 'common/ZError' import { router } from 'decorators/router' -import { createPageSign, queryFiat, queryPrice, refreshToken } from 'service/alchemy.svr' +import { createOrder, createPageSign, queryFiat, queryPrice, refreshToken } from 'service/alchemy.svr' import { generateKVStr } from 'utils/net.util' import { PayRecord, PayStatus } from 'modules/PayRecord' import { PriceSvr } from 'service/price.svr' @@ -10,7 +10,59 @@ import { PriceSvr } from 'service/price.svr' const CALL_BACK_URL = `${process.env.ALCHEMY_PAY_CB_URL}/pay/out/alchemy/buycb` class AlchemyController extends BaseController { @router('post /pay/alchemy/buy') - async beginPay(req, res) { + async beginApiPay(req, res) { + const user = req.user + 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) + console.log(tokenResult) + if (!tokenResult.success || tokenResult.returnCode !== '0000') { + logger.info(`fetch pay token error::code: ${tokenResult.returnCode} msg: ${tokenResult.returnMsg}`) + throw new ZError(10, 'fetch pay token error') + } + const { id, email, accessToken } = tokenResult.data + 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() + let payData: any = { + side: 'BUY', + merchantOrderNo: record.id, + amount: record.fiatAmount, + fiatCurrency: record.fiat, + cryptoCurrency: record.crypto, + depositType: '2', + address: address, + network: record.network, + payWayCode: '10001', + alpha2: record.country, + callbackUrl: CALL_BACK_URL, + merchantName: 'CEBG', + } + logger.info(`create order data::${JSON.stringify(payData)}`) + let payRes = await createOrder(accessToken, payData) + logger.info(`create order result::${JSON.stringify(payRes)}`) + record.outData = payRes.data + if (payRes.success) { + record.outOrderId = payRes.data.orderNo + } else { + record.status = PayStatus.FAIL + } + record.save() + return { url: payRes.data.payUrl } + } + @router('post /pay/alchemy/buypage') + async beginPagePay(req, res) { const user = req.user const { network, crypto, address, fiat, fiatAmount, country } = req.params if (fiat || fiatAmount || country) { diff --git a/src/controllers/alchemyout.controller.ts b/src/controllers/alchemyout.controller.ts index 682eb92..648f5ee 100644 --- a/src/controllers/alchemyout.controller.ts +++ b/src/controllers/alchemyout.controller.ts @@ -5,6 +5,7 @@ import { role, router } from 'decorators/router' import { checkPayResultSign, checkSimpleSign } from 'service/alchemy.svr' import { PayRecord, PayStatus } from 'modules/PayRecord' import { TransferQueue } from 'queue/transfer.queue' +import { TransferRecord } from 'modules/TransferRecord' let errorRes = function (msg: string) { return { @@ -27,12 +28,17 @@ class AlchemyOutController extends BaseController { 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 && record.status !== PayStatus.TRANSFERING) { + if ( + record.status !== PayStatus.PENDING && + record.status !== PayStatus.TRANSFERING && + record.status !== PayStatus.TRANSFERED + ) { logger.info(`alchemy callback record status error`) throw new ZError(13, 'alchemy callback record status error') } @@ -42,13 +48,18 @@ class AlchemyOutController extends BaseController { await record.save() throw new ZError(14, 'alchemy callback sign error') } - + let transferRecord = await TransferRecord.findByRecordId(record.id) + if (transferRecord) { + transferRecord.status = 9 + await transferRecord.save() + } record.outOrderId = orderNo record.network = network record.crypto = crypto record.outData = req.params record.status = status == 'PAY_SUCCESS' ? PayStatus.SUCCESS : PayStatus.FAIL await record.save() + logger.info(`alchemy callback success, pay finished`) return {} } @@ -60,14 +71,15 @@ class AlchemyOutController extends BaseController { @router('get /pay/out/alchemy/queryprice') async queryToken(req, res) { const { crypto } = req.params - const { appId, timestamp, sign } = req.headers + let { appId, appid, timestamp, sign } = req.headers if (!crypto) { return errorRes('params mismatch') } + appId = appId || appid if (!appId || !timestamp || !sign) { return errorRes('headers mismatch') } - if (!checkSimpleSign(req.headers)) { + if (!checkSimpleSign(req.headers, { crypto })) { return errorRes('sign error') } let result = { @@ -90,20 +102,22 @@ class AlchemyOutController extends BaseController { /** * 通知商户打币 - * TODO:: + * TODO::test */ @role(ROLE_ANON) @router('post /pay/out/alchemy/distribute') async distributeToken(req, res) { const { orderNo, crypto, network, address, cryptoAmount, cryptoPrice, usdtAmount } = req.params - const { appId, timestamp, sign } = req.headers + let { appId, appid, timestamp, sign } = req.headers if (!orderNo || !crypto || !network || !address || !cryptoAmount || !cryptoPrice || !usdtAmount) { return errorRes('params mismatch') } + appId = appId || appid if (!appId || !timestamp || !sign) { return errorRes('headers mismatch') } - if (!checkSimpleSign(req.headers)) { + let signData = { orderNo, crypto, network, address, cryptoAmount, cryptoPrice, usdtAmount } + if (!checkSimpleSign(req.headers, signData)) { return errorRes('sign error') } @@ -117,6 +131,7 @@ class AlchemyOutController extends BaseController { record.cryptoAmount = cryptoAmount record.cryptoPrice = cryptoPrice record.usdtAdmount = usdtAmount + record.status = PayStatus.TRANSFERING await record.save() new TransferQueue().addTask(record) let result = { @@ -126,6 +141,6 @@ class AlchemyOutController extends BaseController { returnCode: '0000', // false: 9999 returnMsg: 'in amet', } - res.send(result) + return result } } diff --git a/src/controllers/internal.controller.ts b/src/controllers/internal.controller.ts index 9b70403..05f69a5 100644 --- a/src/controllers/internal.controller.ts +++ b/src/controllers/internal.controller.ts @@ -47,7 +47,7 @@ export default class InternalController extends BaseController { if (!sign) { throw new ZError(10, 'sign not found') } - let hash = calcHash({ id, result, successCount, errorCount, hashList }) + let hash = calcHash({ id, result, successCount, errorCount, gas, gasPrice, hashList }) console.log(hash, sign) if (sign !== hash) { throw new ZError(11, 'sign not match') @@ -71,7 +71,9 @@ export default class InternalController extends BaseController { record.hashList = hashList task.txHash = hashList[0] task.status = PayStatus.TRANSFERED - setImmediate(notify.apply(this, [task])) + setImmediate(async () => { + await notify(task, record) + }) } else { record.status = 10 task.status = PayStatus.TRANSFER_FAIL diff --git a/src/modules/PayRecord.ts b/src/modules/PayRecord.ts index e64f649..788f414 100644 --- a/src/modules/PayRecord.ts +++ b/src/modules/PayRecord.ts @@ -78,8 +78,8 @@ export class PayRecordClass extends BaseModule { @prop() public txHash?: string - public static async findByRecordId(this: ReturnModelType, outOrderid: string) { - return this.findOne({ outOrderid }).exec() + public static async findByRecordId(this: ReturnModelType, outOrderId: string) { + return this.findOne({ outOrderId }).exec() } } diff --git a/src/queue/transfer.queue.ts b/src/queue/transfer.queue.ts index 0cf6830..49aecb0 100644 --- a/src/queue/transfer.queue.ts +++ b/src/queue/transfer.queue.ts @@ -40,7 +40,7 @@ export class TransferQueue { assert(chainCfg, `chain config not found: ${task.network}`) let chainId = chainCfg.chainId let wallet = chainCfg.wallet - let address = chainCfg.tokens[task.crypto] + let address = chainCfg.tokens[task.crypto.toLowerCase()] assert(address, `token address not found: ${task.crypto}`) let record = await TransferRecord.insertOrUpdate( { recordId: task.id }, diff --git a/src/service/alchemy.svr.ts b/src/service/alchemy.svr.ts index e17f74d..def0822 100644 --- a/src/service/alchemy.svr.ts +++ b/src/service/alchemy.svr.ts @@ -1,13 +1,31 @@ import axios from 'axios' import { hmacsha256, sha1 } from 'utils/security.util' import crypto from 'crypto' +import { generateKVStr } from 'utils/net.util' -export function createSimpleSign() { +export function createSimpleSign(data: any) { let timestamp = Date.now() let appid = process.env.ALCHEMY_APPID let secret = process.env.ALCHEMY_APP_SECRET - // let sign = sha1(appid + secret + timestamp) - let sign = hmacsha256(appid + timestamp, secret) + let signData = { appid, timestamp } + signData = Object.assign(signData, data) + let signStr = Object.keys(signData) + .sort() + .map(key => `${key}=${signData[key]}`) + .join('&') + let sign = hmacsha256(signStr, secret) + return { + appid, + timestamp, + sign, + } +} + +export function createSha1Sign() { + let timestamp = Date.now() + let appid = process.env.ALCHEMY_APPID + let secret = process.env.ALCHEMY_APP_SECRET + let sign = sha1(appid + secret + timestamp) return { appid, timestamp, @@ -26,11 +44,18 @@ export function checkPayResultSign(data: any) { return sign === signature } -export function checkSimpleSign(data: any) { +export function checkSimpleSign(headers: any, data: any) { // alchemy 很不严谨, 有时候是 appid, 有时候是 appId - const { appid, appId, timestamp, sign } = data + const { appid, appId, timestamp, sign } = headers let appIdToCheck = appId || appid - const expectedSign = sha1(appIdToCheck + process.env.ALCHEMY_APP_SECRET + timestamp) + let signData = { appid: appIdToCheck, timestamp } + signData = Object.assign(signData, data) + let signStr = Object.keys(signData) + .sort() + .map(key => `${key}=${signData[key]}`) + .join('&') + const expectedSign = hmacsha256(signStr, process.env.ALCHEMY_APP_SECRET) + // const expectedSign = sha1(appIdToCheck + process.env.ALCHEMY_APP_SECRET + timestamp) return sign === expectedSign } @@ -66,11 +91,11 @@ export function createPageSign(plainText: string) { */ export async function refreshToken(email: string) { const data = JSON.stringify({ email }) - const { appid, timestamp, sign } = createSimpleSign() + const { appid, timestamp, sign } = createSimpleSign({ email }) const host = process.env.ALCHEMY_API_BASE const config = { method: 'post', - url: `${host}/merchant/getToken`, + url: `${host}/open/api/v3/merchant/getToken`, headers: { appId: appid, timestamp: timestamp, @@ -87,11 +112,11 @@ export async function refreshToken(email: string) { * https://alchemycn.readme.io/docs/创建订单 */ export async function createOrder(token: string, data: any) { - const { appid, timestamp, sign } = createSimpleSign() + const { appid, timestamp, sign } = createSimpleSign(data) const host = process.env.ALCHEMY_API_BASE const config = { method: 'post', - url: `${host}/merchant/trade/create`, + url: `${host}/open/api/v3/merchant/trade/create`, headers: { 'access-token': token, appId: appid, @@ -112,7 +137,7 @@ export async function createOrder(token: string, data: any) { * TODO:: test */ export async function updateOrderStatus(data: any) { - const { appid, timestamp, sign } = createSimpleSign() + const { appid, timestamp, sign } = createSha1Sign() const url = process.env.ALCHEMY_API_BASE + '/webhooks/treasure' const config = { method: 'post', @@ -133,7 +158,6 @@ export async function updateOrderStatus(data: any) { * https://alchemycn.readme.io/docs/查询数字货币币价 */ export async function queryPrice(data: any) { - const { appid, timestamp, sign } = createSimpleSign() let dataOrign = { crypto: 'ETH', network: 'ETH', @@ -143,9 +167,10 @@ export async function queryPrice(data: any) { side: 'BUY', } dataOrign = { ...dataOrign, ...data } + const { appid, timestamp, sign } = createSimpleSign(dataOrign) const config = { method: 'post', - url: process.env.ALCHEMY_API_BASE + '/merchant/order/quote', + url: process.env.ALCHEMY_API_BASE + '/open/api/v3/merchant/order/quote', headers: { 'Content-Type': 'application/json', appId: appid, @@ -164,20 +189,21 @@ export async function queryPrice(data: any) { * @returns */ export async function queryFiat() { - const { appid, timestamp, sign } = createSimpleSign() let dataOrign = { type: 'BUY', } + const { appid, timestamp, sign } = createSimpleSign(dataOrign) + let url = process.env.ALCHEMY_API_BASE + '/open/api/v3/merchant/fiat/list' + url = generateKVStr({ data: dataOrign, encode: true, uri: url }) const config = { method: 'GET', - url: process.env.ALCHEMY_API_BASE + '/merchant/fiat/list', + url, headers: { 'Content-Type': 'application/json', appId: appid, sign, timestamp, }, - data: dataOrign, } let response = await axios(config) return response.data diff --git a/src/utils/string.util.ts b/src/utils/string.util.ts index a4c288b..19bdd13 100644 --- a/src/utils/string.util.ts +++ b/src/utils/string.util.ts @@ -5,7 +5,7 @@ * @param splitChar 连接的字符, 默认是& * @param equalChar = */ -export function generateKeyValStr(data: {}, ignoreNull = true, splitChar: string = '&', equalChar = '=') { +export function generateKeyValStr(data: Record, ignoreNull = true, splitChar = '&', equalChar = '=') { const keys = Object.keys(data) keys.sort() let result = '' @@ -15,7 +15,7 @@ export function generateKeyValStr(data: {}, ignoreNull = true, splitChar: string return } if (i++ > 0) result += splitChar - result += `${key}${equalChar}${data[key]}` + result += `${key}${equalChar}${data[key]}` } return result } @@ -47,15 +47,15 @@ export function keyValToObject(str: string, splitChar: string = '&', equalChar = export function isTrue(obj) { return ( obj === 'true' || - obj === 'TRUE' || - obj === 'True' || - obj === 'on' || - obj === 'ON' || - obj === true || - obj === 1 || - obj === '1' || - obj === 'YES' || - obj === 'yes' + obj === 'TRUE' || + obj === 'True' || + obj === 'on' || + obj === 'ON' || + obj === true || + obj === 1 || + obj === '1' || + obj === 'YES' || + obj === 'yes' ) } @@ -84,7 +84,7 @@ export function string10to62(number: string | number) { qutient = (qutient - mod) / radix arr.unshift(chars[mod]) } while (qutient) - return arr.join('') + return arr.join('') } /** @@ -105,32 +105,33 @@ export function string62to10(numberCode: string) { return originNumber } -const reNormalUUID = /^[0-9a-fA-F-]{36}$/; -const reLongUUID = /^[0-9a-fA-F]{32}$/; -const reShortUUID = /^[0-9a-zA-Z+/]{22,23}$/; -const n = /-/g; +const reNormalUUID = /^[0-9a-fA-F-]{36}$/ +const reLongUUID = /^[0-9a-fA-F]{32}$/ +const reShortUUID = /^[0-9a-zA-Z+/]{22,23}$/ +const n = /-/g -export function compressUuid(e:string, t: boolean = false) { +export function compressUuid(e: string, t: boolean = false) { if (reNormalUUID.test(e)) { - e = e.replace(n, ''); + e = e.replace(n, '') } else if (!reLongUUID.test(e)) { - return e; + return e } - var r = !0 === t ? 2 : 5; + var r = !0 === t ? 2 : 5 return compressHex(e, r) -} +} -const CHARS_BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; +const CHARS_BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' export function compressHex(e: string, r: number) { - var i, n = e.length; - i = void 0 !== r ? r : n % 3; - for (var s = e.slice(0, i), o = []; i < n;) { + var i, + n = e.length + i = void 0 !== r ? r : n % 3 + for (var s = e.slice(0, i), o = []; i < n; ) { var u = parseInt(e[i], 16), a = parseInt(e[i + 1], 16), - c = parseInt(e[i + 2], 16); - o.push(CHARS_BASE64[u << 2 | a >> 2]); - o.push(CHARS_BASE64[(3 & a) << 4 | c]); - i += 3; + c = parseInt(e[i + 2], 16) + o.push(CHARS_BASE64[(u << 2) | (a >> 2)]) + o.push(CHARS_BASE64[((3 & a) << 4) | c]) + i += 3 } return s + o.join('') }