import dotenv from 'dotenv'; import express, { Request, Response } from 'express'; import cors from 'cors'; import axios, { AxiosInstance } from 'axios'; import { ClientCredentials, Token } from 'simple-oauth2'; import { ethers } from 'ethers'; import bs58 from 'bs58'; import { Wallet } from 'ethers'; import { toWei } from 'web3-utils'; import https from 'https'; import http from 'http'; import debug from 'debug'; import axiosDebugLog from 'axios-debug-log'; // Load environment variables from .env file dotenv.config(); // Create a debug instance const log = debug('bbpay'); // Enable debug messages based on the DEBUG environment variable if (process.env.DEBUG) { debug.enable('bbpay,simple-oauth2,axios'); } // Enable debug messages for Axios requests axiosDebugLog({ request: function (debug, config) { debug('Request:', config); }, response: function (debug, response) { debug('Response:', response); }, error: function (debug, error) { debug('Error:', error); }, }); const app = express(); app.use(cors()); app.use(express.json()); class BBPay { protected oauth: AxiosInstance; protected cert: string; protected verifySsl: string; protected baseUrl: string; protected params: any; protected scope: string[]; protected async setupOauth(): Promise { log('Setting up OAuth...'); const client = new ClientCredentials({ client: { id: process.env.CLIENT_ID, secret: process.env.CLIENT_SECRET, }, auth: { tokenHost: process.env.ITP_OAUTH_URL, // Ensure this is set to the correct token endpoint URL }, }); this.cert = 'cert.pem'; this.key = 'key.pem' this.verifySsl = 'bb.pem'; this.baseUrl = process.env.ITP_API_URL; this.params = { numeroConvenio: 701, 'gw-dev-app-key': process.env.DEV_APP_KEY, }; this.scope = [ 'checkout.solicitacoes-requisicao', 'checkout.participantes-requisicao', 'checkout.solicitacoes-info', 'checkout.participantes-info', ]; const tokenParams = { scope: this.scope.join(' '), }; log('Fetching access token...'); const accessToken: Token = await client.getToken(tokenParams); log('Access token fetched successfully.'); this.oauth = axios.create({ baseURL: this.baseUrl, headers: { Authorization: `Bearer ${accessToken.token.access_token}`, }, httpsAgent: new https.Agent({ cert: this.cert, key: this.key, rejectUnauthorized: false, }), }); } public async init(): Promise { await this.setupOauth(); } } class Register extends BBPay { public async post(req: Request, res: Response): Promise { log('Registering participant...'); const data = req.body; const body = { numeroConvenio: 701, nomeParticipante: data.chainID, tipoDocumento: data.tipoDocumento, numeroDocumento: data.numeroDocumento, numeroConta: data.numeroConta, numeroAgencia: data.numeroAgencia, tipoConta: data.tipoConta, codigoIspb: data.codigoIspb, // Código identificador do Sistema de Pagamentos Brasileiro. Atualmente aceitamos apenas Banco do Brasil, codigoIspb igual a 0 }; try { log('Sending request to register participant...'); const response = await this.oauth.post('/participantes', body, { params: this.params, }); if (response.status !== 201) { log('Upstream error:', response.status); res.status(response.status).send('Upstream error'); return; } log('Participant registered successfully.'); res.json(response.data); } catch (error) { log('Internal server error:', error); res.status(500).send('Internal server error'); } } } class Request extends BBPay { public async post(req: Request, res: Response): Promise { log('Creating request...'); const data = req.body; const body = { geral: { numeroConvenio: 701, pagamentoUnico: true, descricaoSolicitacao: 'P2Pix', valorSolicitacao: data.amount, codigoConciliacaoSolicitacao: data.lockid, }, formasPagamento: [{ codigoTipoPagamento: 'PIX', quantidadeParcelas: 1 }], repasse: { tipoValorRepasse: 'Percentual', recebedores: [{ identificadorRecebedor: data.pixTarget, tipoRecebedor: 'Participante', valorRepasse: 100, }], }, }; try { log('Sending request to create request...'); const response = await this.oauth.post('/solicitacoes', body, { params: this.params, }); if (response.status !== 201) { log('Upstream error:', response.status); res.status(response.status).send('Upstream error'); return; } log('Request created successfully.'); res.json(response.data); } catch (error) { log('Internal server error:', error); res.status(500).send('Internal server error'); } } } class Release extends BBPay { public async get(req: Request, res: Response): Promise { const numeroSolicitacao = req.params.numeroSolicitacao; log(`Releasing request ${numeroSolicitacao}...`); try { log('Fetching request details...'); const response = await this.oauth.get(`/solicitacoes/${numeroSolicitacao}`, { params: this.params, }); if (response.status !== 200) { log('Upstream error:', response.status); res.status(response.status).send('Upstream error'); return; } const data = response.data; const numeroParticipante = data.repasse.recebedores[0].identificadorRecebedor; const pixTimestamp = ethers.utils.solidityPack(['bytes32'], [ethers.utils.hexZeroPad(bs58.decode(data.informacoesPix.txId),32)]); const valorSolicitacao = toWei(data.valorSolicitacao, 'ether'); const codigoEstadoSolicitacao = data.codigoEstadoSolicitacao; if (codigoEstadoSolicitacao !== 1) { log('Pix not paid.'); res.status(204).send('Pix not paid'); return; } log('Fetching participant details...'); const participantResponse = await this.oauth.get(`/participantes/${numeroParticipante}`, { params: this.params, }); if (participantResponse.status !== 200) { log('Upstream error:', participantResponse.status); res.status(participantResponse.status).send('Upstream error'); return; } const chainID = participantResponse.data.nomeParticipante; const packed = ethers.utils.solidityPack(['bytes32', 'uint80', 'bytes32'], [ ethers.utils.hexZeroPad(ethers.utils.toUtf8Bytes(`${chainID}-${numeroParticipante}`),32), ethers.BigNumber.from(valorSolicitacao), ethers.utils.hexZeroPad(pixTimestamp,32), ]); const signable = ethers.utils.keccak256(packed); const wallet = new Wallet(process.env.PRIVATE_KEY); const signature = await wallet.signMessage(signable); log('Request released successfully.'); res.json({ pixTarget: `${chainID}-${numeroParticipante}`, amount: valorSolicitacao.toString(), pixTimestamp: `0x${pixTimestamp.toString('hex')}`, signature: `0x${signature}`, }); } catch (error) { log('Internal server error:', error); res.status(500).send('Internal server error'); } } } // (CPF, nome, conta) -> participantID // should be called before deposit const register = new Register(); app.post('/register', async (req: Request, res: Response) => { await register.init(); await register.post(req, res); }); // (amount,pixtarget) -> requestID, QRcodeText // should be called after lock const request = new Request(); app.post('/request', async (req: Request, res: Response) => { await request.init(); await request.post(req, res); }); // (requestID) -> sig(pixTarget, amount, pixTimestamp) // should be called before release const release = new Release(); app.get('/release/:numeroSolicitacao', async (req: Request, res: Response) => { await release.init(); await release.get(req, res); }); if (process.env.DEBUG) { app.listen(process.env.PORT || 5000, () => { log(`Server running on port ${process.env.PORT || 5000}`); }); } else { const server = http.createServer(app); server.listen(process.env.PORT || 5000, () => { log(`Server running on port ${process.env.PORT || 5000}`); }); }