Compare commits

...

17 Commits

Author SHA1 Message Date
hueso
60a22e1b0b dynamic deploy environments 2025-10-31 14:28:35 -03:00
hueso
68f33a3249 improve error handling 2025-10-31 14:07:07 -03:00
hueso
2729be8d2c added .gitignore for sensitive files 2025-07-11 14:16:08 -03:00
hueso
65db1edc06 0x prefixed and padded return values 2025-07-04 21:15:06 -03:00
hueso
3edac0ecc9 Added mTLS documentation 2025-06-11 16:30:39 -03:00
hueso
3a4d65ff5d Fixed mTLS certificate verification 2025-06-11 16:30:05 -03:00
hueso
22449e7ce1 Improved error handling 2025-05-30 14:31:28 -03:00
hueso
a59717a8db add CORS handling 2025-05-21 14:24:46 -03:00
hueso
e2b44c10f6 added chain ID check to avoid replay attacks 2025-05-09 14:25:56 -03:00
hueso
d220e615e9 return values as strings 2025-05-09 13:29:15 -03:00
hueso
184f2e5b27 added ISPB code to support other banks 2025-05-09 13:28:56 -03:00
hueso
18bf7c389b base64 is built-in, removed from requirements.txt 2025-05-09 13:14:57 -03:00
hueso
ae01e72776 mTLS 2025-05-02 13:43:38 -03:00
hueso
2acc72f2db Add README and schema 2025-04-04 18:49:04 -03:00
hueso
6092541f5d add requirements.txt 2025-03-28 14:56:18 -03:00
hueso
b47f7906c4 pixTimestamp -> txId 2025-03-28 14:21:04 -03:00
hueso
f6042cfe03 Pix amount in wei 2025-03-28 14:19:56 -03:00
6 changed files with 315 additions and 50 deletions

16
.env.example Normal file
View File

@@ -0,0 +1,16 @@
PRIVATE_KEY=0x....
CLIENT_ID=
CLIENT_SECRET=
DEV_APP_KEY=
DEBUG=false
ITP_TOKEN_URL = https://oauth.hm.bb.com.br/oauth/token
# Url de produção com autenticação mTLS.
#ITP_API_URL="https://api-bbpay.bb.com.br/checkout/v2"
# Url de homologação com autenticação mTLS.
ITP_API_URL = "https://api-bbpay.hm.bb.com.br/checkout/v2"
# Url de homologação sem autenticação mTLS.
#ITP_API_URL = "https://api.extranet.hm.bb.com.br/checkout/v2"
# 05/08/2025
#ITP_API_URL=https://checkout.mtls.api.bb.com.br
#ITP_API_URL=https://checkout.mtls.api.hm.bb.com.br

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.env
*.pem
venv

99
README.md Normal file
View File

@@ -0,0 +1,99 @@
# P2Pix ITP oracle backend
## Variáveis de ambiente necessárias no arquivo `.env`:
- `BB_CLIENT_ID`, `BB_CLIENT_SECRET`, e `DEV_APP_KEY`: Fornecidas pelo Banco do Brasil
- `PRIVATE_KEY`:
Chave privada Ethereum (em formato hexadecimal com prefixo '0x')
que vai assinar as liberações de pagamento)
# Endpoints
## 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
```
##### `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
```
### Envio de certificado
Para criar a cadeia de certificados pra enviar pro BB usar:
```
openssl pkcs12 -in <arquivo>.p12 -nokeys -legacy -out cert.pem
```
enviar o `cert.pem` no formulário do portal developers como "cadeia completa".
# 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/

View File

@@ -4,71 +4,72 @@ from os import getenv
from oauthlib.oauth2 import BackendApplicationClient
from requests_oauthlib import OAuth2Session
from flask import Flask, request
from flask_cors import CORS
from flask_restful import Resource, Api
from eth_abi.packed import encode_packed
import eth_account
from eth_hash.auto import keccak
from base64 import b85decode
from urllib3 import disable_warnings
from eth_utils import to_wei
# Load environment variables from .env file
load_dotenv()
app = Flask(__name__)
CORS(app)
api = Api(app)
class BBPay(Resource):
def __init__(self):
super().__init__()
self.setup_oauth()
def setup_oauth(self):
client = BackendApplicationClient(client_id=getenv("CLIENT_ID"))
self.oauth = OAuth2Session(client=client)
scope = ['checkout.solicitacoes-requisicao',
'checkout.participantes-requisicao',
'checkout.solicitacoes-info',
'checkout.participantes-info']
self.oauth.fetch_token(
token_url='https://oauth.hm.bb.com.br/oauth/token',
client_id=getenv("CLIENT_ID"),
client_secret=getenv("CLIENT_SECRET"), scope=scope)
# Url de homologação com autenticação mTLS.
#self.baseUrl = "https://api-bbpay.hm.bb.com.br/checkout/v2"
# Url de homologação sem autenticação mTLS.
self.baseUrl = "https://api.extranet.hm.bb.com.br/checkout/v2"
# Url de produção com autenticação mTLS.
#self.baseUrl = "https://api-bbpay.bb.com.br/checkout/v2"
self.verify_ssl = False
self.cert = 'key.pem'
self.verify_ssl = 'bb.pem'
self.baseUrl = getenv("ITP_API_URL")
self.token_url = getenv("ITP_TOKEN_URL")
self.params = {
'numeroConvenio': 701,
'gw-dev-app-key': getenv("DEV_APP_KEY")
}
self.scope = ['checkout.solicitacoes-requisicao',
'checkout.participantes-requisicao',
'checkout.solicitacoes-info',
'checkout.participantes-info']
self.oauth.fetch_token(
token_url=self.token_url,
client_id=getenv("CLIENT_ID"),
client_secret=getenv("CLIENT_SECRET"), scope=self.scope)
def __init__(self):
super().__init__()
self.setup_oauth()
class Register(BBPay):
def post(self):
data = request.get_json()
body = {
'numeroConvenio': 701,
'nomeParticipante': data['nomeParticipante'],
'nomeParticipante': data['chainID'],
'tipoDocumento': data['tipoDocumento'],
'numeroDocumento': data['numeroDocumento'],
'numeroConta': data['numeroConta'],
'numeroAgencia': data['numeroAgencia'],
'tipoConta': data['tipoConta'],
'codigoIspb': 0 # Código identificador do Sistema de Pagamentos Brasileiro. Atualmente aceitamos apenas Banco do Brasil, codigoIspb igual a 0
'codigoIspb': data['codigoIspb'] # Código identificador do Sistema de Pagamentos Brasileiro. Atualmente aceitamos apenas Banco do Brasil, codigoIspb igual a 0
}
response = self.oauth.post(
self.baseUrl+"/participantes",
params=self.params,
json=body,
verify=self.verify_ssl)
verify=self.verify_ssl,
cert=self.cert)
if response.status_code != 201:
return 'Upstream error', response.status_code
return response.json()
class Request(BBPay):
@@ -79,7 +80,8 @@ class Request(BBPay):
"numeroConvenio": 701,
"pagamentoUnico": True,
"descricaoSolicitacao": "P2Pix",
"valorSolicitacao": data['amount']
"valorSolicitacao": data['amount'],
"codigoConciliacaoSolicitacao": data['lockid']
},
# "devedor": {
# "tipoDocumento": 1,
@@ -98,7 +100,10 @@ class Request(BBPay):
self.baseUrl+"/solicitacoes",
params=self.params,
json=body,
verify=self.verify_ssl)
verify=self.verify_ssl,
cert=self.cert)
if response.status_code != 201:
return 'Upstream error', response.status_code
return response.json()
class Release(BBPay):
@@ -106,25 +111,35 @@ class Release(BBPay):
response = self.oauth.get(
self.baseUrl+f"/solicitacoes/{numeroSolicitacao}",
params=self.params,
verify=self.verify_ssl)
verify=self.verify_ssl,
cert=self.cert)
if response.status_code != 200:
return 'Upstream error', response.status_code
data = response.json()
numeroParticipante = data['repasse']['recebedores'][0]['identificadorRecebedor']
valorSolicitacao = data['valorSolicitacao']
pixTimestamp = data['timestampCriacaoSolicitacao']
pixTimestamp = encode_packed(['bytes32'],[b85decode(data['informacoesPix']['txId'])])
valorSolicitacao = to_wei(data['valorSolicitacao'], 'ether')
codigoEstadoSolicitacao = data['codigoEstadoSolicitacao']
if codigoEstadoSolicitacao != 0:
if codigoEstadoSolicitacao != 1:
return 'Pix not paid', 204
response = self.oauth.get(
self.baseUrl+f"/participantes/{numeroParticipante}",
params=self.params,
verify=self.verify_ssl,
cert=self.cert)
if response.status_code != 200:
return 'Upstream error', response.status_code
chainID = response.json()['nomeParticipante']
packed = encode_packed(['bytes32','uint80','bytes32'],
(str(numeroParticipante).encode(), int(valorSolicitacao), pixTimestamp.encode()) )
(f"{chainID}-{numeroParticipante}".encode(), int(valorSolicitacao), pixTimestamp) )
signable = eth_account.messages.encode_defunct(keccak(packed))
signature = eth_account.account.Account.sign_message(signable, private_key=getenv('PRIVATE_KEY')).signature.hex()
return {
'pixTarget': numeroParticipante,
'amount': valorSolicitacao,
'pixTimestamp': pixTimestamp,
'signature': signature }
'pixTarget': f"{chainID}-{numeroParticipante}",
'amount': str(valorSolicitacao),
'pixTimestamp': f"0x{pixTimestamp.hex()}",
'signature': f"0x{signature}"
}
# (CPF, nome, conta) -> participantID
# should be called before deposit
@@ -138,7 +153,6 @@ api.add_resource(Request, '/request')
# should be called before release
api.add_resource(Release, '/release/<int:numeroSolicitacao>')
if __name__ == '__main__':
if getenv("DEBUG"):
disable_warnings()
@@ -146,5 +160,3 @@ if __name__ == '__main__':
else:
from waitress import serve
serve(app, host=getenv("HOST","0.0.0.0"), port=getenv("PORT",5000))

123
helpix.json Normal file
View File

@@ -0,0 +1,123 @@
{
"info": {
"name": "Helpix",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "register",
"event": [],
"request": {
"method": "POST",
"header": [],
"auth": {
"type": "noauth"
},
"description": "",
"url": {
"raw": "{{baseUrl}}/register",
"protocol": "",
"host": [
"{{baseUrl}}"
],
"path": [
"register"
],
"query": [],
"variable": []
},
"body": {
"mode": "raw",
"raw": "{\n \"nomeParticipante\": \"Participante Teste\",\n \"tipoDocumento\": 1,\n \"numeroDocumento\": 64655215801,\n \"numeroConta\": 10331,\n \"numeroAgencia\": 551,\n \"tipoConta\": 1\n}",
"options": {
"raw": {
"language": "json"
}
}
}
}
},
{
"name": "release",
"event": [],
"request": {
"method": "GET",
"header": [],
"auth": {
"type": "noauth"
},
"description": "",
"url": {
"raw": "{{baseUrl}}/release/:requestID",
"protocol": "",
"host": [
"{{baseUrl}}"
],
"path": [
"release",
":requestID"
],
"query": [],
"variable": [
{
"key": "requestID",
"value": "1259843"
}
]
},
"body": {
"mode": "raw",
"options": {
"raw": {
"language": "json"
}
}
}
}
},
{
"name": "request",
"event": [],
"request": {
"method": "POST",
"header": [],
"auth": {
"type": "noauth"
},
"description": "",
"url": {
"raw": "{{baseUrl}}/request",
"protocol": "",
"host": [
"{{baseUrl}}"
],
"path": [
"request"
],
"query": [],
"variable": []
},
"body": {
"mode": "raw",
"raw": "{\n \"amount\": \"100\",\n \"pixTarget\": 145\n}",
"options": {
"raw": {
"language": "json"
}
}
}
}
},
{
"name": "bruno",
"event": []
}
],
"variable": [
{
"key": "baseUrl",
"value": "",
"type": "default"
}
]
}

12
requirements.txt Normal file
View File

@@ -0,0 +1,12 @@
python-dotenv
oauthlib
requests-oauthlib
flask
flask-restful
flask-cors
eth-abi
eth-account
eth-hash[pycryptodome]
eth-utils
urllib3
waitress