This commit is contained in:
hueso 2026-01-09 02:07:57 -03:00
parent 8043e2b4ca
commit 4695773736
6 changed files with 2300 additions and 456 deletions

View File

@ -1,5 +1,12 @@
PRIVATE_KEY=0xYOUR_PRIVATE_KEY
# mTLS
CLIENT_KEY=/path/to/your/client/key
CLIENT_CRT=/path/to/your/client/cert
# BBpay
CLIENT_ID=
CLIENT_SECRET=
DEV_APP_KEY=
ITP_OAUTH_URL=https://oauth.hm.bb.com.br/oauth/token
ITP_API_URL=https://checkout.mtls.api.bb.com.br
ITP_API_URL=https://checkout.mtls.api.hm.bb.com.br
DEBUG=false

142
README.md
View File

@ -1,63 +1,103 @@
# network-core-sdk-mtls-example
# P2Pix zkTLS ITP prover
## About Primus Network-Core-SDK
## Variáveis de ambiente necessárias no arquivo `.env`:
When integrating data verification solutions into your **backend** server, you can utilize the [**Primus Network Core SDK**](https://docs.primuslabs.xyz/primus-network/build-with-primus/for-backend/simpleexample).
- `CLIENT_ID`, `CLIENT_SECRET`, e `DEV_APP_KEY`: Fornecidas pelo Banco do Brasil
- `ITP_OAUTH_URL`, `ITP_API_URL`: Endpoints BBPay
- `PRIVATE_KEY`:
Chave privada Ethereum (em formato hexadecimal com prefixo '0x')
que vai assinar as liberações de pagamento)
The Network-Core-SDK allows you to verify data through API endpoint responses. An authorized token or other credential is required to request private data if the data source server requires permissioned access. Note that in the backend integration situation, the developer usually proves their off-chain data in their built application, and the Primus extension is **not** required. Typical scenarios include proof of reserves, in which a configured web page periodically proves that the stablecoin issuer holds sufficient collateral across off-chain platforms.
# Como rodar
- `npm install`
- `npm start`
For more details about Primus zkTLS, please refer to:
1. zkTLS technology link: https://docs.primuslabs.xyz/data-verification/tech-intro
2. Primus Network: https://docs.primuslabs.xyz/primus-network/understand-primus-network
# Endpoints
## Install
```bash
npm install
## POST /register
### Registra um participante.
Chamado pelo vendedor antes de fazer `deposit` no smart contract.
#### Parametros requeridos:
- `chainID`
- `tipoDocumento`
- `numeroDocumento`
- `numeroConta`
- `tipoConta`
- `codigoIspb`
``` exemplo
curl --request POST \
--url http://localhost:5000/register \
--header 'content-type: application/json' \
--data '{
"chainID": "1337",
"tipoDocumento": 1,
"numeroDocumento": 12345678900,
"numeroConta": 1234567890123456,
"numeroAgencia": 123,
"tipoConta": 1,
"codigoIspb": 0
}'
```
#### Retorna em formato JSON:
- `numeroParticipante` (usado no `deposit` do SC como `pixTarget`)
## POST /request
### Solicitação de Pagamento
Chamado pelo comprador após o `lock` no smart contract.
#### Parametros requeridos:
- `amount`
- `pixTarget`
```
curl -X POST http://localhost:5000/request \
-H "Content-Type: application/json" \
-d '{
"amount": 100.00,
"pixTarget": 123
}'
```
#### Retorna em formato JSON:
- `numeroSolicitacao`: usado para chamar `/release` depois
- `textoQrCode`: usado para gerar o QR PIX
## GET /release/{numeroSolicitacao}
### Liberação de Pagamento
Chamado pelo comprador após pagar o Pix
``` exemplo
curl http://localhost:5000/release/123
```
#### Retorna em formato JSON:
- `chainid`-`pixTarget`
- `amount`: valor em wei
- `pixTimestamp`
- `signature`: assinatura ethereum compatível
# mTLS
##### `key.pem`: chave privada e certificado da empresa
Descriptografar o e-CNPJ em formato PKCS#12 e converter em formato PEM (☢️ contém chave privada):
```
umask 077; # tirar permissão de leitura global
openssl pkcs12 -in <arquivo>.p12 -legacy -clcerts -noenc -out key.pem
```
## Configure
Copy `.env.example` to `.env` in the project root:
```bash
cp .env.example .env
##### `bb.pem`: certificado do BB
Descarregar o certificado de https://apoio.developers.bb.com.br/referency/post/646799afa2e2b90012c5ede8 e converter em formato PEM:
```
openssl x509 -in raiz_v3.der -inform DER -outform PEM -out bb.pem
```
Then set your `PRIVATE_KEY`:
```sh
PRIVATE_KEY=0xYOUR_PRIVATE_KEY
### Envio de certificado
Para criar a cadeia de certificados pra enviar pro BB usar:
```
and
`CLIENT_KEY` and `CLIENT_CRT` for mTLS:
```sh
CLIENT_KEY=/path/to/your/client/key
CLIENT_CRT=/path/to/your/client/cert
openssl pkcs12 -in <arquivo>.p12 -nokeys -legacy -out cert.pem
```
enviar o `cert.pem` no formulário do portal developers como "cadeia completa".
* `CLIENT_KEY`: the client private key file, in PEM format (e.g. `-----BEGIN PRIVATE KEY-----...`).
* `CLIENT_CRT`: the client certificate file, in PEM format (e.g. `-----BEGIN CERTIFICATE-----...`).
<br/>
**Security note**: keep the client key private and stored securely; never commit it or share it publicly.
## Customize
Edit these sections in `index.js`:
- `address`: your wallet address
- `requests`: request params for your mTLS endpoint
- `responseResolves`: JSON parse paths for the response fields you want to attest
- `chainId` and `baseSepoliaRpcUrl`: switch to Base mainnet if needed
## Run
```bash
node index.js
```
You should see logs for:
- submit task result
- attest result
- task result
## Notes
- Keep your `.env` out of version control.
- The example uses a public RPC; for reliability, use your own provider endpoint.
# Observações
- Para ambiente de desenvolvimento use `DEBUG=true`
- Em produção, o servidor usa Waitress como servidor WSGI
- Para mais informações, consulte a documentação oficial: https://developers.bb.com.br/

283
bbpay.ts Normal file
View File

@ -0,0 +1,283 @@
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}`);
});
}

View File

@ -1,82 +0,0 @@
const { PrimusNetwork } = require("@primuslabs/network-core-sdk");
const { ethers } = require("ethers");
require("dotenv/config");
const fs = require("fs");
async function primusProofTest() {
const PRIVATE_KEY = process.env.PRIVATE_KEY?.trim() ?? (() => { throw new Error("Missing PRIVATE_KEY in .env"); })();
const CLIENT_CRT = process.env.CLIENT_CRT?.trim() ?? (() => { throw new Error("Missing CLIENT_CRT in .env"); })();
const CLIENT_KEY = process.env.CLIENT_KEY?.trim() ?? (() => { throw new Error("Missing CLIENT_KEY in .env"); })();
const address = "0x8F0D4188307496926d785fB00E08Ed772f3be890"; // change to your address
const chainId = 84532; // Base Sepolia (or 8453 for Base mainnet)
const baseSepoliaRpcUrl = "https://sepolia.base.org"; // (or 'https://mainnet.base.org' for Base mainnet)
// The request and response can be customized as needed.
// change to your request params
const requests = [
{
url: "https://checkout.mtls.api.hm.bb.com.br/",
method: "GET",
header: {},
body: "",
}
];
const responseResolves = [
[
{
keyName: "detail", // change to your response key name
parseType: "json",
parsePath: "$.detail", // change to your response json path
},
],
];
const provider = new ethers.providers.JsonRpcProvider(baseSepoliaRpcUrl);
const wallet = new ethers.Wallet(PRIVATE_KEY, provider);
try {
const primusNetwork = new PrimusNetwork();
// Initialize the network with wallet
const initResult = await primusNetwork.init(wallet, chainId);
// Submit task
const submitTaskParams = {
address,
};
let submitTaskResult = await primusNetwork.submitTask(submitTaskParams);
console.log("Submit task result:", submitTaskResult);
// Compose params for attest
const mTLS = {
clientKey: fs.readFileSync(CLIENT_KEY).toString(),
clientCrt: fs.readFileSync(CLIENT_CRT).toString(),
}
const attestParams = {
...submitTaskParams,
...submitTaskResult,
requests,
responseResolves,
mTLS
};
let attestResult = await primusNetwork.attest(attestParams);
console.log("Attest result:", attestResult);
// Verify and poll task result
const verifyParams = {
taskId: attestResult[0].taskId,
reportTxHash: attestResult[0].reportTxHash,
};
const taskResult = await primusNetwork.verifyAndPollTaskResult(
verifyParams
);
console.log("Task result:", taskResult);
} catch (error) {
console.error("Unexpected error:", error);
throw error;
}
}
primusProofTest();

2223
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,13 +4,22 @@
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "echo \"Error: no test specified\" && exit 1",
"start": "tsx bbpay.ts"
},
"author": "",
"license": "ISC",
"dependencies": {
"@primuslabs/network-core-sdk": "^0.1.5",
"axios": "^1.13.2",
"axios-debug-log": "^1.0.0",
"bs58": "^6.0.0",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"ethers": "^5.7.2"
"ethers": "^5.8.0",
"express": "^5.2.1",
"simple-oauth2": "^5.1.0",
"typescript": "^5.9.3",
"web3-utils": "^4.3.3"
}
}