zkPix v0
This commit is contained in:
parent
8043e2b4ca
commit
4695773736
13
.env.example
13
.env.example
@ -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
142
README.md
@ -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
283
bbpay.ts
Normal 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}`);
|
||||
});
|
||||
}
|
||||
82
index.js
82
index.js
@ -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
2223
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user