Compare commits
11 Commits
04a3ed6046
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60a22e1b0b | ||
|
|
68f33a3249 | ||
|
|
2729be8d2c | ||
|
|
65db1edc06 | ||
|
|
3edac0ecc9 | ||
|
|
3a4d65ff5d | ||
|
|
22449e7ce1 | ||
|
|
a59717a8db | ||
|
|
e2b44c10f6 | ||
|
|
d220e615e9 | ||
|
|
184f2e5b27 |
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
|
||||||
33
README.md
33
README.md
@@ -15,23 +15,25 @@
|
|||||||
Chamado pelo vendedor antes de fazer `deposit` no smart contract.
|
Chamado pelo vendedor antes de fazer `deposit` no smart contract.
|
||||||
|
|
||||||
#### Parametros requeridos:
|
#### Parametros requeridos:
|
||||||
- `nomeParticipante`
|
- `chainID`
|
||||||
- `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 '{
|
||||||
"nomeParticipante": "João Silva",
|
"chainID": "1337",
|
||||||
"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:
|
||||||
@@ -64,13 +66,34 @@ 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:
|
||||||
- `pixTarget`
|
- `chainid`-`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/
|
||||||
105
bbpay.py
105
bbpay.py
@@ -4,6 +4,7 @@ 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
|
||||||
@@ -16,64 +17,59 @@ 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)
|
||||||
|
|
||||||
scope = ['checkout.solicitacoes-requisicao',
|
self.cert = 'key.pem'
|
||||||
'checkout.participantes-requisicao',
|
self.verify_ssl = 'bb.pem'
|
||||||
'checkout.solicitacoes-info',
|
self.baseUrl = getenv("ITP_API_URL")
|
||||||
'checkout.participantes-info']
|
self.token_url = getenv("ITP_TOKEN_URL")
|
||||||
|
|
||||||
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['nomeParticipante'],
|
'nomeParticipante': data['chainID'],
|
||||||
'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='cert.pem')
|
cert=self.cert)
|
||||||
|
if response.status_code != 201:
|
||||||
|
return 'Upstream error', response.status_code
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
class Request(BBPay):
|
class Request(BBPay):
|
||||||
@@ -84,7 +80,8 @@ 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,
|
||||||
@@ -93,7 +90,7 @@ class Request(BBPay):
|
|||||||
"formasPagamento": [{ "codigoTipoPagamento": "PIX", "quantidadeParcelas": 1}],
|
"formasPagamento": [{ "codigoTipoPagamento": "PIX", "quantidadeParcelas": 1}],
|
||||||
"repasse": {
|
"repasse": {
|
||||||
"tipoValorRepasse": "Percentual",
|
"tipoValorRepasse": "Percentual",
|
||||||
"recebedores": [{
|
"recebedores": [{
|
||||||
"identificadorRecebedor": data['pixTarget'],
|
"identificadorRecebedor": data['pixTarget'],
|
||||||
"tipoRecebedor": "Participante",
|
"tipoRecebedor": "Participante",
|
||||||
"valorRepasse": 100 }]
|
"valorRepasse": 100 }]
|
||||||
@@ -103,7 +100,10 @@ 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,25 +111,35 @@ 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 = b85decode(data['informacoesPix']['txId'])
|
pixTimestamp = encode_packed(['bytes32'],[b85decode(data['informacoesPix']['txId'])])
|
||||||
valorSolicitacao = to_wei(data['valorSolicitacao'], 'ether')
|
valorSolicitacao = to_wei(data['valorSolicitacao'], 'ether')
|
||||||
codigoEstadoSolicitacao = data['codigoEstadoSolicitacao']
|
codigoEstadoSolicitacao = data['codigoEstadoSolicitacao']
|
||||||
if codigoEstadoSolicitacao != 0:
|
if codigoEstadoSolicitacao != 1:
|
||||||
return 'Pix not paid', 204
|
return 'Pix not paid', 204
|
||||||
packed = encode_packed(['bytes32','uint80','bytes32'],
|
response = self.oauth.get(
|
||||||
(str(numeroParticipante).encode(), int(valorSolicitacao), pixTimestamp) )
|
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'],
|
||||||
|
(f"{chainID}-{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': str(numeroParticipante),
|
'pixTarget': f"{chainID}-{numeroParticipante}",
|
||||||
'amount': str(valorSolicitacao),
|
'amount': str(valorSolicitacao),
|
||||||
'pixTimestamp': pixTimestamp.hex(),
|
'pixTimestamp': f"0x{pixTimestamp.hex()}",
|
||||||
'signature': signature }
|
'signature': f"0x{signature}"
|
||||||
|
}
|
||||||
|
|
||||||
# (CPF, nome, conta) -> participantID
|
# (CPF, nome, conta) -> participantID
|
||||||
# should be called before deposit
|
# should be called before deposit
|
||||||
@@ -137,19 +147,16 @@ api.add_resource(Register, '/register')
|
|||||||
|
|
||||||
# (amount,pixtarget) -> requestID, QRcodeText
|
# (amount,pixtarget) -> requestID, QRcodeText
|
||||||
# should be called after lock
|
# should be called after lock
|
||||||
api.add_resource(Request, '/request')
|
api.add_resource(Request, '/request')
|
||||||
|
|
||||||
# (requestID) -> sig(pixTarget, amount, pixTimestamp)
|
# (requestID) -> sig(pixTarget, amount, pixTimestamp)
|
||||||
# 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()
|
||||||
app.run(debug=True)
|
app.run(debug=True)
|
||||||
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))
|
||||||
|
|
||||||
|
|
||||||
@@ -3,6 +3,7 @@ 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]
|
||||||
|
|||||||
Reference in New Issue
Block a user