zkPix/bbpay.ts
2026-01-09 02:07:57 -03:00

283 lines
9.3 KiB
TypeScript

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<void> {
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<void> {
await this.setupOauth();
}
}
class Register extends BBPay {
public async post(req: Request, res: Response): Promise<void> {
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<void> {
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<void> {
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}`);
});
}