Compare commits

..

2 Commits

Author SHA1 Message Date
hueso
04a3ed6046 return values as string 2025-05-09 13:19:02 -03:00
hueso
9da3fe0e24 added ISPB code to support other banks 2025-05-09 13:18:23 -03:00
5 changed files with 54 additions and 104 deletions

View File

@@ -1,16 +0,0 @@
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
View File

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

View File

@@ -15,25 +15,23 @@
Chamado pelo vendedor antes de fazer `deposit` no smart contract. Chamado pelo vendedor antes de fazer `deposit` no smart contract.
#### Parametros requeridos: #### Parametros requeridos:
- `chainID` - `nomeParticipante`
- `tipoDocumento` - `tipoDocumento`
- `numeroDocumento` - `numeroDocumento`
- `numeroConta` - `numeroConta`
- `tipoConta` - `tipoConta`
- `codigoIspb`
``` exemplo ``` exemplo
curl --request POST \ curl --request POST \
--url http://localhost:5000/register \ --url http://localhost:5000/register \
--header 'content-type: application/json' \ --header 'content-type: application/json' \
--data '{ --data '{
"chainID": "1337", "nomeParticipante": "João Silva",
"tipoDocumento": 1, "tipoDocumento": 1,
"numeroDocumento": 12345678900, "numeroDocumento": 12345678900,
"numeroConta": 1234567890123456, "numeroConta": 1234567890123456,
"numeroAgencia": 123, "numeroAgencia": 123,
"tipoConta": 1, "tipoConta": 1
"codigoIspb": 0
}' }'
``` ```
#### Retorna em formato JSON: #### Retorna em formato JSON:
@@ -66,34 +64,13 @@ Chamado pelo comprador após pagar o Pix
curl http://localhost:5000/release/123 curl http://localhost:5000/release/123
``` ```
#### Retorna em formato JSON: #### Retorna em formato JSON:
- `chainid`-`pixTarget` - `pixTarget`
- `amount`: valor em wei - `amount`: valor em wei
- `pixTimestamp` - `pixTimestamp`
- `signature`: assinatura ethereum compatível - `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 # Observações
- O ambiente de produção requer autenticação mTLS
- Para ambiente de desenvolvimento use `DEBUG=true` - Para ambiente de desenvolvimento use `DEBUG=true`
- Em produção, o servidor usa Waitress como servidor WSGI - Em produção, o servidor usa Waitress como servidor WSGI
- Para mais informações, consulte a documentação oficial: https://developers.bb.com.br/ - Para mais informações, consulte a documentação oficial: https://developers.bb.com.br/

View File

@@ -4,7 +4,6 @@ from os import getenv
from oauthlib.oauth2 import BackendApplicationClient from oauthlib.oauth2 import BackendApplicationClient
from requests_oauthlib import OAuth2Session from requests_oauthlib import OAuth2Session
from flask import Flask, request from flask import Flask, request
from flask_cors import CORS
from flask_restful import Resource, Api from flask_restful import Resource, Api
from eth_abi.packed import encode_packed from eth_abi.packed import encode_packed
import eth_account import eth_account
@@ -17,59 +16,64 @@ from eth_utils import to_wei
load_dotenv() load_dotenv()
app = Flask(__name__) app = Flask(__name__)
CORS(app)
api = Api(app) api = Api(app)
class BBPay(Resource): class BBPay(Resource):
def __init__(self):
super().__init__()
self.setup_oauth()
def setup_oauth(self): def setup_oauth(self):
client = BackendApplicationClient(client_id=getenv("CLIENT_ID")) client = BackendApplicationClient(client_id=getenv("CLIENT_ID"))
self.oauth = OAuth2Session(client=client) self.oauth = OAuth2Session(client=client)
self.cert = 'key.pem' scope = ['checkout.solicitacoes-requisicao',
self.verify_ssl = 'bb.pem' 'checkout.participantes-requisicao',
self.baseUrl = getenv("ITP_API_URL") 'checkout.solicitacoes-info',
self.token_url = getenv("ITP_TOKEN_URL") '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,
cert='cert.pem')
# 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.params = { self.params = {
'numeroConvenio': 701, 'numeroConvenio': 701,
'gw-dev-app-key': getenv("DEV_APP_KEY") '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): class Register(BBPay):
def post(self): def post(self):
data = request.get_json() data = request.get_json()
body = { body = {
'numeroConvenio': 701, 'numeroConvenio': 701,
'nomeParticipante': data['chainID'], 'nomeParticipante': data['nomeParticipante'],
'tipoDocumento': data['tipoDocumento'], 'tipoDocumento': data['tipoDocumento'],
'numeroDocumento': data['numeroDocumento'], 'numeroDocumento': data['numeroDocumento'],
'numeroConta': data['numeroConta'], 'numeroConta': data['numeroConta'],
'numeroAgencia': data['numeroAgencia'], 'numeroAgencia': data['numeroAgencia'],
'tipoConta': data['tipoConta'], 'tipoConta': data['tipoConta'],
'codigoIspb': data['codigoIspb'] # 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( response = self.oauth.post(
self.baseUrl+"/participantes", self.baseUrl+"/participantes",
params=self.params, params=self.params,
json=body, json=body,
verify=self.verify_ssl, verify=self.verify_ssl,
cert=self.cert) cert='cert.pem')
if response.status_code != 201:
return 'Upstream error', response.status_code
return response.json() return response.json()
class Request(BBPay): class Request(BBPay):
@@ -80,8 +84,7 @@ class Request(BBPay):
"numeroConvenio": 701, "numeroConvenio": 701,
"pagamentoUnico": True, "pagamentoUnico": True,
"descricaoSolicitacao": "P2Pix", "descricaoSolicitacao": "P2Pix",
"valorSolicitacao": data['amount'], "valorSolicitacao": data['amount']
"codigoConciliacaoSolicitacao": data['lockid']
}, },
# "devedor": { # "devedor": {
# "tipoDocumento": 1, # "tipoDocumento": 1,
@@ -100,10 +103,7 @@ class Request(BBPay):
self.baseUrl+"/solicitacoes", self.baseUrl+"/solicitacoes",
params=self.params, params=self.params,
json=body, 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() return response.json()
class Release(BBPay): class Release(BBPay):
@@ -111,35 +111,25 @@ class Release(BBPay):
response = self.oauth.get( response = self.oauth.get(
self.baseUrl+f"/solicitacoes/{numeroSolicitacao}", self.baseUrl+f"/solicitacoes/{numeroSolicitacao}",
params=self.params, 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() data = response.json()
numeroParticipante = data['repasse']['recebedores'][0]['identificadorRecebedor'] numeroParticipante = data['repasse']['recebedores'][0]['identificadorRecebedor']
pixTimestamp = encode_packed(['bytes32'],[b85decode(data['informacoesPix']['txId'])]) pixTimestamp = b85decode(data['informacoesPix']['txId'])
valorSolicitacao = to_wei(data['valorSolicitacao'], 'ether') valorSolicitacao = to_wei(data['valorSolicitacao'], 'ether')
codigoEstadoSolicitacao = data['codigoEstadoSolicitacao'] codigoEstadoSolicitacao = data['codigoEstadoSolicitacao']
if codigoEstadoSolicitacao != 1: if codigoEstadoSolicitacao != 0:
return 'Pix not paid', 204 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'], packed = encode_packed(['bytes32','uint80','bytes32'],
(f"{chainID}-{numeroParticipante}".encode(), int(valorSolicitacao), pixTimestamp) ) (str(numeroParticipante).encode(), int(valorSolicitacao), pixTimestamp) )
signable = eth_account.messages.encode_defunct(keccak(packed)) signable = eth_account.messages.encode_defunct(keccak(packed))
signature = eth_account.account.Account.sign_message(signable, private_key=getenv('PRIVATE_KEY')).signature.hex() signature = eth_account.account.Account.sign_message(signable, private_key=getenv('PRIVATE_KEY')).signature.hex()
return { return {
'pixTarget': f"{chainID}-{numeroParticipante}", 'pixTarget': str(numeroParticipante),
'amount': str(valorSolicitacao), 'amount': str(valorSolicitacao),
'pixTimestamp': f"0x{pixTimestamp.hex()}", 'pixTimestamp': pixTimestamp.hex(),
'signature': f"0x{signature}" 'signature': signature }
}
# (CPF, nome, conta) -> participantID # (CPF, nome, conta) -> participantID
# should be called before deposit # should be called before deposit
@@ -153,6 +143,7 @@ api.add_resource(Request, '/request')
# should be called before release # should be called before release
api.add_resource(Release, '/release/<int:numeroSolicitacao>') api.add_resource(Release, '/release/<int:numeroSolicitacao>')
if __name__ == '__main__': if __name__ == '__main__':
if getenv("DEBUG"): if getenv("DEBUG"):
disable_warnings() disable_warnings()
@@ -160,3 +151,5 @@ if __name__ == '__main__':
else: else:
from waitress import serve from waitress import serve
serve(app, host=getenv("HOST","0.0.0.0"), port=getenv("PORT",5000)) serve(app, host=getenv("HOST","0.0.0.0"), port=getenv("PORT",5000))

View File

@@ -3,7 +3,6 @@ oauthlib
requests-oauthlib requests-oauthlib
flask flask
flask-restful flask-restful
flask-cors
eth-abi eth-abi
eth-account eth-account
eth-hash[pycryptodome] eth-hash[pycryptodome]