trubit.adapter.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. import { Provide, Inject, Config, ALL } from '@midwayjs/decorator';
  2. import {
  3. ChannelAdapter,
  4. CreateUserParams,
  5. CustomerInfo,
  6. } from '../interface/channel.adapter';
  7. import { Repository } from 'typeorm';
  8. import { InjectEntityModel } from '@midwayjs/typeorm';
  9. import { PaymentChannelEntity } from '../entity/channel';
  10. import axios from 'axios';
  11. import * as crypto from 'crypto';
  12. import { CustomerEntity } from '../entity/customer';
  13. @Provide()
  14. export class TrubitAdapter implements ChannelAdapter {
  15. @InjectEntityModel(PaymentChannelEntity)
  16. channelEntity: Repository<PaymentChannelEntity>;
  17. @InjectEntityModel(CustomerEntity)
  18. customerEntity: Repository<CustomerEntity>;
  19. @Config(ALL)
  20. globalConfig;
  21. @Inject()
  22. ctx;
  23. private config: {
  24. apiKey: string;
  25. apiSecret: string;
  26. apiUrl: string;
  27. chainApiKey: string;
  28. chainApiSecret: string;
  29. chainApiUrl: string;
  30. };
  31. /**
  32. * 初始化渠道配置
  33. */
  34. private async initConfig() {
  35. if (!this.config) {
  36. const channel = await this.channelEntity.findOne({
  37. where: { code: 'TRUBIT', isEnabled: true },
  38. });
  39. if (!channel) {
  40. throw new Error('TRUBIT channel not found or disabled');
  41. }
  42. this.config = {
  43. apiKey: channel.apiKey,
  44. apiSecret: channel.apiSecret,
  45. apiUrl: channel.apiUrl,
  46. chainApiKey: channel.chainApiKey,
  47. chainApiSecret: channel.chainApiSecret,
  48. chainApiUrl: channel.chainApiUrl,
  49. };
  50. }
  51. }
  52. /**
  53. * 生成随机字符串
  54. * 32位十六进制字符串 (0-9, a-f)
  55. */
  56. private generateNonce(): string {
  57. return Array.from({ length: 32 }, () =>
  58. Math.floor(Math.random() * 16).toString(16)
  59. ).join('');
  60. }
  61. /**
  62. * 生成签名
  63. * @see https://docs-merchant.sunpay.pro/guide
  64. */
  65. generateSignature(
  66. /*
  67. *
  68. * (HMAC SHA256)
  69. [linux]$ curl -H "X-BH-APIKEY: tAQfOrPIZAhym0qHISRt8EFvxPemdBm5j5WMlkm3Ke9aFp0EGWC2CGM8GHV4kCYW" -X POST 'https://$HOST?1538323200000&signature=5fafd5098eca1e135037f335fe6a9ec6884271cb6933bd7f5ed6a3582f1f0eea' -d '{"id":0}'
  70. *
  71. * (HMAC SHA256)
  72. [linux]$ curl -H "X-BH-APIKEY: tAQfOrPIZAhym0qHISRt8EFvxPemdBm5j5WMlkm3Ke9aFp0EGWC2CGM8GHV4kCYW" -X POST 'https://$HOST?1538323200000&signature=5fafd5098eca1e135037f335fe6a9ec6884271cb6933bd7f5ed6a3582f1f0eea' -d '{"id":0}'
  73. * */
  74. timestamp: string,
  75. body = '',
  76. secret = ''
  77. ): string {
  78. // const payload = `timestamp=${timestamp}${body}`;
  79. const payload = `timestamp=1538323200000{"id":0}`;
  80. console.log(83, payload)
  81. console.log(83, secret)
  82. // 创建 HMAC 对象
  83. const hmac = crypto.createHmac('sha256', secret);
  84. // 更新数据
  85. hmac.update(payload);
  86. // 生成哈希值
  87. const signature = hmac.digest('hex');
  88. // const signature = crypto
  89. // .createHmac('sha256', secret)
  90. // .update(payload)
  91. // .digest('hex')
  92. // .toUpperCase();
  93. return signature;
  94. }
  95. // 生成签名
  96. async testSigin(params) {
  97. await this.initConfig();
  98. // 1. 准备请求参数
  99. // const timestamp = Date.now().toString();
  100. const timestamp = '1538323200000';
  101. const secret = "lH3ELTNiFxCQTmi9pPcWWikhsjO04Yoqw3euoHUuOLC3GYBW64ZqzQsiOEHXQS76"
  102. console.log(99, params)
  103. // 2. 生成签名
  104. const sign = this.generateSignature(timestamp, JSON.stringify(params), secret);
  105. return sign;
  106. }
  107. /**
  108. * 发送请求到 SunPay API
  109. */
  110. private async request(method: string, endpoint: string, data?: any) {
  111. await this.initConfig();
  112. let url, secret, key;
  113. if (endpoint.includes('/Crypto/')) {
  114. url = this.config.chainApiUrl;
  115. secret = this.config.chainApiSecret;
  116. key = this.config.chainApiKey;
  117. } else {
  118. url = this.config.apiUrl;
  119. secret = this.config.apiSecret;
  120. key = this.config.apiKey;
  121. }
  122. // 1. 准备请求参数
  123. const timestamp = Date.now().toString();
  124. const nonce = this.generateNonce();
  125. const body = method === 'GET' ? '' : JSON.stringify(data);
  126. // 2. 生成签名
  127. const sign = this.generateSignature(timestamp, body, secret);
  128. try {
  129. url = `${url}${endpoint}`;
  130. const response = await axios({
  131. method,
  132. url,
  133. data: method !== 'GET' ? data : undefined,
  134. params: method === 'GET' ? data : undefined,
  135. headers: {
  136. 'Content-Type': 'application/json',
  137. 'SunPay-Key': key,
  138. 'SunPay-Timestamp': timestamp,
  139. 'SunPay-Nonce': nonce,
  140. 'SunPay-Sign': sign,
  141. },
  142. });
  143. // 检查响应
  144. if (response.data.code !== 200) {
  145. console.log(response.data);
  146. throw new Error(`FusionPay API ${response.data.msg}`);
  147. }
  148. return response.data;
  149. } catch (error) {
  150. // console.log(error.response.data);
  151. if (axios.isAxiosError(error) && error.response) {
  152. console.log(error.response.data);
  153. throw new Error(`FusionPay API Network ${error.response.data.msg}`);
  154. }
  155. throw error;
  156. }
  157. }
  158. /**
  159. * 验证用户信息
  160. */
  161. async validateCustomerInfo(params: CreateUserParams): Promise<void> {
  162. const { data } = await this.request('POST', '/Customer/Validate', {
  163. type: params.userType,
  164. out_user_id: params.userId, // 外部用户ID
  165. webhook_url: this.globalConfig.callback.sunpay,
  166. ...params.userInfo,
  167. });
  168. return data;
  169. }
  170. /**
  171. * 创建渠道用户
  172. */
  173. async createUser(params: CreateUserParams): Promise<string> {
  174. const customerInfo = {
  175. type: params.userType,
  176. out_user_id: params.userId, // 外部用户ID
  177. webhook_url: this.globalConfig.callback.sunpay,
  178. ...params.userInfo,
  179. };
  180. // 先验证用户信息
  181. await this.validateCustomerInfo(params);
  182. // 创建用户
  183. const { data } = await this.request('POST', '/Customer', customerInfo);
  184. if (!data.customerId) {
  185. throw new Error(
  186. 'Failed to create customer: customerId is missing in response'
  187. );
  188. }
  189. return data.customerId;
  190. }
  191. /**
  192. * 创建钱包
  193. */
  194. async createWallet(params: any): Promise<string> {
  195. console.log('createWallet-=-=-',params)
  196. const { data } = await this.request('POST', '/Fiat/Wallet', params);
  197. return data;
  198. }
  199. /**
  200. * 获取关联银行账户必填字段
  201. * @param params
  202. * @returns
  203. */
  204. async associateBankAccountRequiredFieldsAsync(params: any): Promise<any> {
  205. console.log('associateBankAccountRequiredFieldsAsync-==-=-',params)
  206. return await this.request(
  207. 'GET',
  208. '/Fiat/AssociateBankAccountRequiredFieldsAsync',
  209. params
  210. );
  211. }
  212. /**
  213. * 获取受益人地址必填字段
  214. * /api/v3/Fiat/BeneficiaryAddressRequiredFields
  215. * @param params
  216. * @returns
  217. */
  218. async getBeneficiaryAddressRequiredFields(params: any): Promise<any> {
  219. return await this.request(
  220. 'GET',
  221. '/Fiat/BeneficiaryAddressRequiredFields',
  222. params
  223. );
  224. }
  225. /**
  226. * 验证受益人地址必填字段
  227. * /api/v3/Fiat/BeneficiaryAddress/Validate
  228. * @param params
  229. * @returns
  230. */
  231. async validateBeneficiaryAddress(params: any): Promise<any> {
  232. return await this.request(
  233. 'POST',
  234. '/Fiat/BeneficiaryAddress/Validate',
  235. params
  236. );
  237. }
  238. /**
  239. * 关联银行账户
  240. * @param params
  241. * @returns
  242. */
  243. async associateBankAccount(params: any): Promise<any> {
  244. console.log('associateBankAccountRequiredFieldsAsync-==-=-adapter',params)
  245. const { data } = await this.request(
  246. 'POST',
  247. '/Fiat/AssociateBankAccount',
  248. params
  249. );
  250. return data;
  251. }
  252. /**
  253. * 获取钱包信息
  254. * @param params
  255. * @returns
  256. */
  257. async getWallet(params: any): Promise<any> {
  258. console.log('GetWalletAccounts-=-=-=',params)
  259. const res = await this.request('GET', '/Fiat/GetWalletAccounts', params);
  260. return res;
  261. }
  262. /**
  263. * 获取创建客户必填字段
  264. * /api/v3/Fiat/CustomerRequiredFields
  265. * @param params
  266. * @returns
  267. */
  268. async getCustomerRequiredFields(params: any): Promise<any> {
  269. return await this.request('GET', '/Fiat/CustomerRequiredFields', params);
  270. }
  271. /**
  272. * 验证用户信息
  273. */
  274. async checkCustomerInfo(params: CreateUserParams): Promise<void> {
  275. return await this.request('POST', '/Fiat/Customer/Validate', params);
  276. }
  277. /**
  278. * 获取受益人
  279. */
  280. async getBeneficiaryForId(id: string): Promise<void> {
  281. return await this.request('GET', '/Fiat/Beneficiary/' + id);
  282. }
  283. /**
  284. * 获取受益人
  285. */
  286. async getCustomerForId(id: string): Promise<void> {
  287. return await this.request('GET', '/Fiat/Customer/' + id);
  288. }
  289. /**
  290. * 获取受益人列表
  291. */
  292. async getBeneficiaryList(data: any): Promise<void> {
  293. return await this.request('GET', '/Fiat/Beneficiary', data);
  294. }
  295. /**
  296. * 获取账户钱包币种余额信息
  297. * @param params
  298. * @returns
  299. */
  300. async getWalletBalance(params: any): Promise<any> {
  301. console.log('getWalletBalance-=-=-=',params)
  302. const res = await this.request('GET', '/Fiat/AccountBalance', params);
  303. console.log(res);
  304. return res;
  305. }
  306. /**
  307. * 充值
  308. */
  309. async deposit(
  310. walletId: string,
  311. amount: number,
  312. currency: string
  313. ): Promise<string> {
  314. const { data } = await this.request('POST', '/transactions/deposit', {
  315. wallet_id: walletId,
  316. amount: amount.toString(),
  317. currency,
  318. });
  319. return data.transaction_id;
  320. }
  321. /**
  322. * 提现
  323. */
  324. async withdraw(
  325. walletId: string,
  326. amount: number,
  327. currency: string
  328. ): Promise<string> {
  329. const { data } = await this.request('POST', '/transactions/withdraw', {
  330. wallet_id: walletId,
  331. amount: amount.toString(),
  332. currency,
  333. });
  334. return data.transaction_id;
  335. }
  336. /**
  337. * 转账
  338. */
  339. async transfer(
  340. fromWalletId: string,
  341. toWalletId: string,
  342. amount: number,
  343. currency: string
  344. ): Promise<string> {
  345. const { data } = await this.request('POST', '/transactions/transfer', {
  346. from_wallet_id: fromWalletId,
  347. to_wallet_id: toWalletId,
  348. amount: amount.toString(),
  349. currency,
  350. });
  351. return data.transaction_id;
  352. }
  353. /**
  354. * 兑换
  355. */
  356. async exchange(
  357. fromWalletId: string,
  358. fromCurrency: string,
  359. toWalletId: string,
  360. toCurrency: string,
  361. amount: number
  362. ): Promise<string> {
  363. const { data } = await this.request('POST', '/transactions/exchange', {
  364. from_wallet_id: fromWalletId,
  365. from_currency: fromCurrency,
  366. to_wallet_id: toWalletId,
  367. to_currency: toCurrency,
  368. amount: amount.toString(),
  369. });
  370. return data.transaction_id;
  371. }
  372. /**
  373. * 获取余额
  374. */
  375. async getBalance(walletId: string): Promise<number> {
  376. const { data } = await this.request('GET', `/wallets/${walletId}/balance`);
  377. return parseFloat(data.balance);
  378. }
  379. async getBanks(params: any = {}) {
  380. return await this.request('GET', '/Fiat/Banks', params);
  381. }
  382. async getBank(params: any = {}) {
  383. console.log('getBank-=--=',params)
  384. const res = await this.request(
  385. 'GET',
  386. '/Fiat/GetAssociateBankAccounts',
  387. params
  388. );
  389. return res;
  390. }
  391. /**
  392. * 创建或者更新 客户信息 根据 out_user_id
  393. * @param params
  394. * @returns
  395. */
  396. async setCustomerInfo(params: any = {}) {
  397. const { merchant } = this.ctx.admin;
  398. const merchantId = merchant.mchId;
  399. // 兼容 白标 和 va平台的身份认证, 根据 openApi 判断
  400. // 保存客户信息
  401. const customer = await this.customerEntity.findOne({
  402. where: {
  403. // 通过判断是否为开放api, 确认白标渠道
  404. [params.openApi ? 'out_user_id' : 'merchantId']: params.out_user_id, // 商户ID
  405. channel: 'SUNPAY',
  406. },
  407. });
  408. let res = null;
  409. if (customer) {
  410. params.customer_id = customer.customer_id;
  411. res = await this.request('PUT', '/Fiat/Customer', {
  412. ...params,
  413. });
  414. } else {
  415. res = await this.request('POST', '/Fiat/Customer', {
  416. ...params,
  417. webhook_url: this.globalConfig.callback.sunpay // 回调用fusionPay配置的的回调生成用户信息
  418. });
  419. }
  420. await this.customerEntity.save({
  421. type: params.customer_type,
  422. merchantId: res.data.out_user_id,
  423. channel: 'SUNPAY',
  424. customer_id: res.data.customer_id,
  425. status: res.data.status,
  426. webhook_url: params.webhook_url,
  427. beneficiary_id: res.data.beneficiary_id,
  428. out_user_id: params.openApi ? merchantId : null // 白标用户是有 out_user_id , va商户没有
  429. });
  430. return res;
  431. }
  432. async createBeneficiary(params: any = {}) {
  433. return this.request('POST', '/Fiat/Beneficiary', params);
  434. }
  435. async updateBeneficiary(params: any = {}) {
  436. return this.request('PUT', '/Fiat/Beneficiary', params);
  437. }
  438. async deleteBeneficiary(params: any = {}) {
  439. return this.request('DELETE', `/Fiat/Beneficiary/${params.id}`, params);
  440. }
  441. async createBeneficiaryAddress(params: any = {}) {
  442. return this.request('POST', '/Fiat/BeneficiaryAddress', params);
  443. }
  444. async updateBeneficiaryAddress(params: any = {}) {
  445. return this.request('PUT', '/Fiat/BeneficiaryAddress', params);
  446. }
  447. async deleteBeneficiaryAddress(params: any = {}) {
  448. return this.request(
  449. 'DELETE',
  450. `/Fiat/BeneficiaryAddress/${params.id}`,
  451. params
  452. );
  453. }
  454. async payIn(params: any = {}) {
  455. console.log('createPayInOrder-=-=payIn-=-=333',params);
  456. return this.request('POST', '/Fiat/PayIn', params);
  457. }
  458. async payInOrder(params:any) {
  459. console.log('getPayInForOrderNo-=-=payInOrder-=-=333',params);
  460. return this.request('GET', `/Fiat/PayIn/${params.orderNo}`);
  461. }
  462. async payInOrderCancel(params:any) {
  463. console.log('cancelPayInOrderForOrderNo-=-=444',params);
  464. return this.request('POST', `/Fiat/PayIn/${params.orderNo}/Cancel`);
  465. }
  466. async payOut(params: any = {}) {
  467. return this.request('POST', '/Fiat/PayOut', params);
  468. }
  469. async payOutOrder(orderNo: string) {
  470. return this.request('GET', `/Fiat/PayOut/${orderNo}`);
  471. }
  472. async cancelPayOut(orderNo: string) {
  473. return this.request('POST', `/Fiat/PayOut/${orderNo}/Cancel`);
  474. }
  475. async confirmPayOut(orderNo: string) {
  476. return this.request('POST', `/Fiat/PayOut/${orderNo}/Confirm`);
  477. }
  478. async payInByChain(params: any = {}) {
  479. return this.request('POST', '/Crypto/PayIn', params);
  480. }
  481. async payOutByChain(params: any = {}) {
  482. return this.request('POST', '/Crypto/PayOut', params);
  483. }
  484. }