Compare commits
17 Commits
94cee1fa5e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60a22e1b0b | ||
|
|
68f33a3249 | ||
|
|
2729be8d2c | ||
|
|
65db1edc06 | ||
|
|
3edac0ecc9 | ||
|
|
3a4d65ff5d | ||
|
|
22449e7ce1 | ||
|
|
a59717a8db | ||
|
|
e2b44c10f6 | ||
|
|
d220e615e9 | ||
|
|
184f2e5b27 | ||
|
|
18bf7c389b | ||
|
|
ae01e72776 | ||
|
|
2acc72f2db | ||
|
|
6092541f5d | ||
|
|
b47f7906c4 | ||
|
|
f6042cfe03 |
16
.env.example
Normal file
16
.env.example
Normal 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
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.env
|
||||
*.pem
|
||||
venv
|
||||
99
README.md
Normal file
99
README.md
Normal 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/
|
||||
98
bbpay.py
98
bbpay.py
@@ -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
123
helpix.json
Normal 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
12
requirements.txt
Normal 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
|
||||
Reference in New Issue
Block a user