Simulação de Transação Bitcoin (Testnet) em Python
1. Contexto e Objetivo
Nesta simulação, iremos enviar uma transação Bitcoin na rede de testes (testnet) utilizando endereços Bech32 (padrão BIP84). O processo envolve a geração ou uso de uma carteira existente, recebimento de fundos via faucet, definição de parâmetros da transação (destinatário, valor e taxa), assinatura digital e envio para propagação na rede. O resultado final será o TXID que é o identificador único de uma transação na rede Bitcoin, com o link para consulta em um explorador de blocos da testnet.
2. Pré‑requisitos
Antes de prosseguir, certifique-se de que:
- 1. O Python 3.11 ou superior e o gerenciador Poetry estão instalados;
- 2. O kernel Jupyter está configurado para este projeto;
- 3. As dependências listadas no arquivo pyproject.toml; estão instaladas, incluindo a biblioteca bitcoinlib;
- 4. Você possui saldo de tBTC em um endereço da testnet para realizar a transação.
2.1 Relação com o projeto de geração de carteira
Este projeto de simulação de transação na testnet reutiliza componentes desenvolvidos anteriormente no projeto de
Geração de Carteira Bitcoin Bech32 (Testnet).
Em especial, são utilizados:
- Função
gerar_carteira_bech32: responsável pela criação da carteira compatível com o padrão BIP84 (endereçostb1...); - Módulos do pacote
btc_wallet_testnet(na pastasrc/), que fornecem a base para derivação de chaves, endereços e integração com a bibliotecabitcoinlib; - Banco SQLite persistente: garante que a carteira gerada possa ser aberta e reutilizada na simulação.
Portanto, o fluxo de simulação só é possível porque parte da lógica de criação e gerenciamento da carteira já foi consolidada no projeto anterior.
Para conhecer em detalhes como a carteira é gerada, consulte: Geração de Carteira Bitcoin Bech32 (Testnet).
3. 🔐 Avisos de Segurança
Antes de prosseguir com a simulação, é importante observar algumas recomendações para garantir segurança e clareza no uso deste projeto:
- Use este projeto exclusivamente na rede de testes (
testnet). Ele não é compatível com transações reais na rede principal do Bitcoin. - Nunca use dados reais de carteiras (como chave privada ou frase mnemônica) neste ambiente de testes. Isso evita riscos de segurança e perda de fundos.
- Proteja suas informações sensíveis. Mesmo em simulações, evite compartilhar dados privados publicamente ou reutilizá-los em outros contextos.
- Verifique se os endereços gerados começam com
tb1, padrão Bech32 da testnet. Isso ajuda a garantir que você está operando no ambiente correto.
4. Importação de Módulos
Aqui importamos a função gerar_carteira_bech32 do pacote
btc_wallet_testnet, responsável pela geração da carteira, e os
recursos da biblioteca bitcoinlib para manipulação de wallets,
assinatura e envio de transações na rede de testes.
# Importações iniciais
import sys
import os
from decimal import Decimal
from bitcoinlib.wallets import Wallet
# Caminho absoluto para a pasta "src"
caminho_src = os.path.abspath(os.path.join(os.getcwd(), "..", "src"))
if caminho_src not in sys.path:
sys.path.insert(0, caminho_src)
# Agora que o caminho está configurado, podemos importar do pacote
from btc_wallet_testnet import gerar_carteira_bech32
# Configurações iniciais
NETWORK = "testnet"
WALLET_NAME = "btc_testnet_simulacao"
# Diretório/arquivo de banco persistente para a bitcoinlib (evita 'Wallet 1 not found')
DB_DIR = os.path.join(os.getcwd(), "data")
os.makedirs(DB_DIR, exist_ok=True)
DB_PATH = os.path.join(DB_DIR, "bitcoinlib_testnet.db")
DB_URI = f"sqlite:///{DB_PATH}"
5. Geração ou Importação da Carteira
Nesta etapa, geramos a carteira Bech32 na testnet e a registramos em um banco SQLite persistente, evitando problemas de reabertura de conexão. Utilizamos parâmetros compatíveis com chave única (WIF), endereços Bech32 (BIP84) e SegWit nativo.
from bitcoinlib.wallets import Wallet, wallet_create_or_open, wallet_delete
# 1) Gerar carteira (WIF) pelo seu pacote
carteira = gerar_carteira_bech32()
print("\nEndereço:", carteira.address)
print("\nWIF:", carteira.wif)
print("\nCaminho derivado:", carteira.path)
# 2) (Opcional, recomendado em testnet) Remover carteira prévia com o mesmo nome
try:
wallet_delete(WALLET_NAME, db_uri=DB_URI)
except Exception:
pass
# 3) Criar/abrir carteira com DB persistente e parâmetros corretos para WIF + Bech32 + SegWit
w = wallet_create_or_open(
WALLET_NAME,
keys=carteira.wif,
scheme='single', # chave única (WIF)
witness_type='segwit', # SegWit nativo (P2WPKH)
encoding='bech32', # endereços tb1...
network=NETWORK, # testnet
db_uri=DB_URI # banco persistente em arquivo (evita 'Wallet 1 not found')
)
# 4) Confirmar endereço ativo e validar prefixo
addr_wallet = w.get_key().address
print("\nEndereço ativo no wallet bitcoinlib:", addr_wallet)
assert addr_wallet.startswith("tb1"), "Endereço não é Bech32 testnet"
Endereço: tb1qwpf95kndj9avl8ctx5trr54rmwh3cfvez7wn7a WIF: cQSCHzin5jkFCD9uwwreNGomJjyzV9vGzDaqyWAzmx4j1u4SMH7X Caminho derivado: m/84'/1'/0'/0/0 Endereço ativo no wallet bitcoinlib: tb1q58y8wthkc09m4zzrlyqu5x3r8zwtm9d2g24q9h
6. Obtenção de tBTC via Faucet (com verificação via API)
Para realizar a transação de teste, é necessário possuir saldo em Bitcoin na rede de testes (tBTC). O código abaixo exibe o endereço Bech32 ativo da carteira e abre no navegador um faucet confiável para solicitação de tBTC. Após solicitar, aguarde a confirmação ou pelo menos a propagação na rede antes de prosseguir para a consulta de saldo. Além disso, o código já consulta automaticamente a API pública do mempool.space para verificar se o endereço possui alguma transação (confirmada ou pendente), permitindo confirmar rapidamente se o faucet realmente enviou os fundos.
import webbrowser
import requests
# Exibir endereço ativo
endereco_ativo = w.get_key().address
print(f"Endereço Bech32 ativo (testnet): {endereco_ativo}")
# Abre o faucet no navegador (preenchimento manual do endereço)
url_faucet = "https://bitcoinfaucet.uo1.net/"
print("\nAbrindo faucet no navegador...")
webbrowser.open(url_faucet)
print("\n Cole o endereço Bech32 acima no campo 'BTC Address' do faucet e solicite tBTC.")
# Consulta inicial via API pública do mempool.space
url_api = f"https://mempool.space/testnet/api/address/{endereco_ativo}"
try:
dados = requests.get(url_api, timeout=10).json()
confirmadas_api = dados["chain_stats"]["funded_txo_count"]
pendentes_api = dados["mempool_stats"]["funded_txo_count"]
if confirmadas_api == 0 and pendentes_api == 0:
print("ℹ️ Nenhuma transação encontrada (confirmada ou pendente) para este endereço até o momento.")
else:
print(f"ℹ️ API indica {confirmadas_api} transações confirmadas e {pendentes_api} pendentes.")
except Exception as e:
print(f"⚠️ Não foi possível consultar a API do mempool.space: {e}")
Endereço Bech32 ativo (testnet): tb1q58y8wthkc09m4zzrlyqu5x3r8zwtm9d2g24q9h Abrindo faucet no navegador... Cole o endereço Bech32 acima no campo 'BTC Address' do faucet e solicite tBTC. ℹ️ API indica 1 transações confirmadas e 0 pendentes.
7. Consulta Automática de Saldo e Verificação de UTXOs
Nesta etapa, antes de iniciar o monitoramento, consultamos automaticamente a API pública do mempool.space para verificar se o endereço já recebeu alguma transação (confirmada ou pendente). Se o resultado for zero, o usuário é avisado e o monitoramento prossegue até detectar saldo ou atingir o tempo limite. Assim que um saldo é identificado, o código lista todos os UTXOs (Unspent Transaction Outputs) disponíveis, exibindo o TXID e o valor em satoshis de cada um, garantindo que haja fundos realmente gastáveis antes de prosseguir.
import time
import webbrowser
import sys
import requests
print("\nIniciando verificação de saldo na testnet...")
print(f"\nEndereço: {endereco_ativo}")
# 1) Consulta inicial via API pública
url_api = f"https://mempool.space/testnet/api/address/{endereco_ativo}"
try:
dados = requests.get(url_api, timeout=10).json()
confirmadas_api = dados["chain_stats"]["funded_txo_count"]
pendentes_api = dados["mempool_stats"]["funded_txo_count"]
if confirmadas_api == 0 and pendentes_api == 0:
print("\nNenhuma transação encontrada (confirmada ou pendente) para este endereço até o momento.")
else:
print(f"\nAPI indica {confirmadas_api} transações confirmadas e {pendentes_api} pendentes.")
except Exception as e:
print(f"\nNão foi possível consultar a API do mempool.space: {e}")
# 2) Abre o mempool.space para acompanhamento visual
webbrowser.open(f"https://mempool.space/testnet/address/{endereco_ativo}")
# 3) Configurações do monitoramento
tempo_limite_segundos = 300 # 5 minutos
intervalo_segundos = 30
inicio = time.time()
tentativa = 0
# Cabeçalho fixo
print("\nTentativa | Saldo Total (sat) | Saldo Confirmado (sat)")
print("-" * 45)
while True:
tentativa += 1
w.scan() # atualização completa sem argumentos
saldo_total = w.balance()
saldo_confirmado = w.balance('confirmed')
# Atualiza a mesma linha no console
sys.stdout.write(
f"\r{tentativa:^9} | {saldo_total:^18} | {saldo_confirmado:^22}"
)
sys.stdout.flush()
if saldo_total > 0:
print("\n Saldo detectado!")
# Lista UTXOs disponíveis
utxos = w.utxos()
if utxos:
print(f" {len(utxos)} UTXO(s) disponível(is) para gasto:")
for u in utxos:
# Acessa como dicionário
print(f"\n - TXID: {u['txid']} | Valor: {u['value']} sat")
else:
print("\n Nenhum UTXO disponível — não será possível enviar até que haja um UTXO confirmado.")
break
if time.time() - inicio > tempo_limite_segundos:
print("\n Tempo limite atingido. Saldo ainda não detectado.")
break
time.sleep(intervalo_segundos)
Iniciando verificação de saldo na testnet...
Endereço: tb1q58y8wthkc09m4zzrlyqu5x3r8zwtm9d2g24q9h
API indica 1 transações confirmadas e 0 pendentes.
Tentativa | Saldo Total (sat) | Saldo Confirmado (sat)
---------------------------------------------
1 | 1000 | 0
Saldo detectado!
1 UTXO(s) disponível(is) para gasto:
- TXID: 9a23e0a52d53f90f9548ca7f9fb4e62db1ce8a32e2529ed6a00d4936113acee8 | Valor: 1000 sat
8. Definição dos Parâmetros da Transação (Formato Híbrido com Cálculo Automático)
Nesta etapa, definimos o endereço de destino, o valor a ser enviado e a taxa de rede. O endereço pode ser informado manualmente ou gerado automaticamente pela própria carteira. Se o valor não for informado, o sistema calcula automaticamente o máximo enviável com base nos UTXOs disponíveis, considerando a taxa estimada e o limite de dust, para evitar erros de saldo insuficiente. Caso o saldo não seja suficiente nem para o mínimo enviável, o fluxo orienta a obter mais tBTC antes de prosseguir.
from decimal import Decimal, ROUND_DOWN
import math
LIMITE_POEIRA_SAT = 546 # Limite mínimo (~546 sat) antes de ser considerado "poeira" (dust)
TAMANHO_ESTIMADO_VBYTES = 170 # Estimativa conservadora do tamanho da transação em vbytes:
# Aqui assumimos:
# - 1 entrada P2WPKH (~68 vbytes)
# - 2 saídas P2WPKH (~31 vbytes cada: 1 para destino e 1 para troco)
# - Overhead (~10 vbytes)
# - Margem de segurança (~30 vbytes)
# Total aproximado: ~170 vbytes
def estimar_taxa_em_satoshis(taxa_por_kb, tamanho_vbytes=TAMANHO_ESTIMADO_VBYTES):
"""
Calcula a taxa estimada em satoshis para uma transação,
com base na taxa por kilobyte e no tamanho estimado em vbytes.
Parâmetros:
-----------
taxa_por_kb : int
Taxa em satoshis por kilobyte (ex.: 1500 sat/kB).
tamanho_vbytes : int
Tamanho estimado da transação em vbytes.
Passos do cálculo:
1. Multiplica a taxa por kB pelo tamanho estimado em vbytes.
2. Divide por 1000 para converter de "por kB" para "por byte".
3. Arredonda para cima com math.ceil() para garantir taxa suficiente.
"""
return math.ceil((taxa_por_kb * tamanho_vbytes) / 1000)
def satoshis_para_btc(satoshis):
"""Converte satoshis para BTC (Decimal)."""
return Decimal(satoshis) / Decimal(100_000_000)
def btc_para_satoshis(valor_btc):
"""Converte BTC (Decimal) para satoshis (inteiro)."""
return int((valor_btc * Decimal(100_000_000)).to_integral_value(rounding=ROUND_DOWN))
def definir_parametros_transacao(carteira, valor_padrao_btc=None, taxa_padrao=1500):
"""
Define os parâmetros para a criação de uma transação na rede Bitcoin Testnet.
- Permite endereço de destino manual ou gerado automaticamente.
- Calcula o valor máximo enviável quando o usuário não informa o valor.
- Considera taxa estimada e limite de poeira para evitar erros de saldo insuficiente.
"""
# Checa UTXOs disponíveis
utxos = carteira.utxos()
if not utxos:
print("\nNenhum UTXO disponível. Obtenha tBTC e execute novamente.")
return None, None, None
# Soma saldo disponível (em satoshis)
saldo_total_satoshis = sum(int(u["value"]) for u in utxos)
taxa_estimada_satoshis = estimar_taxa_em_satoshis(taxa_padrao, TAMANHO_ESTIMADO_VBYTES)
# Máximo enviável assumindo 1 entrada e 1 saída de destino
maximo_enviavel_satoshis = max(0, saldo_total_satoshis - taxa_estimada_satoshis)
# Se o valor máximo enviável for menor que o limite de poeira, não prossegue
if maximo_enviavel_satoshis < LIMITE_POEIRA_SAT:
print("\nSaldo insuficiente para um envio válido após taxa.")
print(f"\n - Saldo total: {saldo_total_satoshis} sat")
print(f"\n - Taxa estimada: {taxa_estimada_satoshis} sat (com {TAMANHO_ESTIMADO_VBYTES} vB e {taxa_padrao} sat/kB)")
print(f"\n - Mínimo do output (limite de poeira): {LIMITE_POEIRA_SAT} sat")
print("➡️ Solicite mais tBTC no faucet e tente novamente.")
return None, None, None
# Endereço de destino: manual ou automático
destino_input = input("Informe o endereço de destino (tb1...) ou pressione Enter para gerar automaticamente: ").strip()
if destino_input:
destino = destino_input
print(f"\nEndereço de destino informado: {destino}")
else:
destino = carteira.get_key(change=0).address
print(f"\nEndereço de destino gerado automaticamente: {destino}")
# Valor: manual ou máximo enviável
if valor_padrao_btc is None:
prompt_valor = f"Informe o valor em BTC (Enter para usar máx. enviável ≈ {satoshis_para_btc(maximo_enviavel_satoshis):.8f}): "
valor_informado = input(prompt_valor).strip()
if valor_informado:
valor_btc = Decimal(valor_informado)
else:
valor_btc = satoshis_para_btc(maximo_enviavel_satoshis).quantize(Decimal("0.00000001"))
else:
valor_informado = input(f"Informe o valor em BTC (ex.: {valor_padrao_btc}): ").strip()
valor_btc = Decimal(valor_informado or valor_padrao_btc)
taxa_por_kb = taxa_padrao
valor_satoshis = btc_para_satoshis(valor_btc)
# Validação final: não permitir valor acima do máximo enviável
if valor_satoshis > maximo_enviavel_satoshis:
print("\nValor solicitado excede o máximo enviável considerando a taxa.")
print(f"\n - Valor solicitado: {valor_satoshis} sat")
print(f"\n - Máx. enviável: {maximo_enviavel_satoshis} sat (saldo {saldo_total_satoshis} - taxa {taxa_estimada_satoshis})")
print("\n Ajuste o valor ou pressione Enter para usar o máximo enviável.")
return None, None, None
print(f"\nDestino: {destino}")
print(f"\nValor (BTC): {valor_btc} | Valor (sat): {valor_satoshis}")
print(f"\nTaxa (sat/kB): {taxa_por_kb} | Taxa estimada (sat): {taxa_estimada_satoshis}")
return destino, valor_btc, taxa_por_kb
# Uso no fluxo:
DESTINO, VALOR_BTC, TAXA_POR_KB = definir_parametros_transacao(w)
Informe o endereço de destino (tb1...) ou pressione Enter para gerar automaticamente:
Endereço de destino gerado automaticamente: tb1q58y8wthkc09m4zzrlyqu5x3r8zwtm9d2g24q9h
Informe o valor em BTC (Enter para usar máx. enviável ≈ 0.00000745):
Destino: tb1q58y8wthkc09m4zzrlyqu5x3r8zwtm9d2g24q9h Valor (BTC): 0.00000745 | Valor (sat): 745 Taxa (sat/kB): 1500 | Taxa estimada (sat): 255
------------ >>> Esclarecimento do Resultado do Item 8
- Como nenhum endereço foi informado, o sistema gerou automaticamente um novo endereço da própria carteira para receber o envio.
- Antes dessa etapa, o código já verificou que há pelo menos um UTXO disponível, garantindo que existe saldo realmente gastável.
- O valor da transação foi definido como 0.0001 BTC, que é o padrão quando não há entrada manual.
- A taxa de rede foi configurada como 1500 sat/kB, garantindo prioridade razoável de confirmação na testnet.
Agora, esses parâmetros serão utilizados no próximo item para criar, assinar e transmitir a transação para a rede de testes.
9. Criação, Assinatura, Envio e Registro Histórico da Transação com Validação de TXID
Nesta etapa, o código cria e envia uma transação na rede Bitcoin Testnet, escolhendo automaticamente entre o cenário com troco ou sem troco, de acordo com o saldo disponível e o limite de "poeira".
Após o envio, o TXID retornado pela biblioteca é validado no explorador mempool.space. Caso não seja localizado (por exemplo, se a biblioteca retornar um WTXID ou ID interno), o código tenta corrigir automaticamente buscando o TXID real pelo endereço de destino e valor enviado.
O TXID validado/corrigido é salvo na variável global e registrado em um arquivo JSON, junto com data, hora, valor enviado, taxa, cenário e endereço de destino, criando um histórico persistente que poderá ser utilizado no Item 10 mesmo após reiniciar o ambiente.
import json
import time
import requests
from datetime import datetime
# Variável global para armazenar o último TXID gerado
TXID_ITEM9 = None
# Caminho do arquivo para salvar o histórico de transações
CAMINHO_HISTORICO = os.path.join("data", "historico_transacoes_testnet.json")
# -----------------------------
# Funções auxiliares de validação
# -----------------------------
def _consulta_tx_por_txid(txid):
"""
Consulta a API do mempool.space (testnet) por TXID.
Retorna True/False conforme a transação seja localizada ou não.
"""
try:
# Consulta a API do mempool.space
url = f"https://mempool.space/testnet/api/tx/{txid}"
r = requests.get(url, timeout=15)
return r.status_code == 200
except Exception:
# Trata caso de não propagação/404/erro de rede
return False
def _buscar_txid_por_endereco(endereco_destino, valor_estimado_sat, tentativas=3, pausa_seg=5):
"""
Busca um TXID recente pelo endereço de destino, verificando as transações do endereço
e tentando identificar uma que contenha um output com o valor esperado (ou maior).
Útil quando a biblioteca retorna WTXID/ID interno em vez do TXID.
"""
try:
for _ in range(tentativas):
# Consulta a API de transações recentes por endereço
url = f"https://mempool.space/testnet/api/address/{endereco_destino}/txs"
r = requests.get(url, timeout=15)
if r.status_code != 200:
time.sleep(pausa_seg)
continue
txs = r.json() if isinstance(r.json(), list) else []
# Percorre transações do endereço para localizar um output compatível
for tx in txs:
txid_candidato = tx.get("txid", "")
vouts = tx.get("vout", [])
for vout in vouts:
sc = vout.get("scriptpubkey_address")
val = vout.get("value", 0)
# Critério: output que paga ao endereço de destino com valor compatível
if sc == endereco_destino and val >= max(1, int(valor_estimado_sat * 0.9)):
return txid_candidato
time.sleep(pausa_seg)
except Exception:
pass
return None
def _validar_ou_corrigir_txid(txid_inicial, endereco_destino, valor_enviado_sat):
"""
Valida o TXID no mempool.space. Se não localizar:
- tenta recuperar o TXID correto varrendo as transações do endereço de destino,
procurando por um output com valor compatível.
- retornando o TXID corrigido (se encontrado) ou o original (se não).
"""
# Tenta validar o TXID reportado pela biblioteca/carteira
if txid_inicial and _consulta_tx_por_txid(txid_inicial):
return txid_inicial, False # válido, sem correção
# Se não validar, tenta localizar por endereço/valor
txid_corrigido = _buscar_txid_por_endereco(endereco_destino, valor_enviado_sat)
if txid_corrigido and _consulta_tx_por_txid(txid_corrigido):
return txid_corrigido, True # corrigido com sucesso
# Não foi possível validar/corrigir
return txid_inicial, False
def criar_e_enviar_transacao(carteira, endereco_destino, valor_btc, taxa_por_kb):
"""
Cria e envia uma transação na rede Bitcoin Testnet,
escolhendo automaticamente entre transação com troco ou sem troco.
Valida/corrige o TXID no explorador antes de registrar.
Registra a transação em um histórico persistente (JSON).
Retorna o TXID (validado/corrigido) em caso de sucesso.
"""
global TXID_ITEM9
try:
# Converte BTC para satoshis
valor_satoshis = btc_para_satoshis(valor_btc)
# Obtém UTXOs disponíveis
utxos = carteira.utxos()
if not utxos:
raise Exception("\nNenhum UTXO disponível para gasto.")
# Calcula saldo e taxa estimada (em satoshis)
saldo_total_satoshis = sum(int(u["value"]) for u in utxos)
taxa_estimada_satoshis = estimar_taxa_em_satoshis(taxa_por_kb, TAMANHO_ESTIMADO_VBYTES)
maximo_enviavel_satoshis = max(0, saldo_total_satoshis - taxa_estimada_satoshis)
# Verifica se o saldo após taxa é suficiente
if maximo_enviavel_satoshis < LIMITE_POEIRA_SAT:
raise Exception(
f"\nSaldo insuficiente após taxa. Máx. enviável: {maximo_enviavel_satoshis} sat"
f"\nLimite de poeira: {LIMITE_POEIRA_SAT} sat."
)
# Calcula troco esperado
troco_esperado = saldo_total_satoshis - valor_satoshis - taxa_estimada_satoshis
# Cenário COM troco
if troco_esperado >= LIMITE_POEIRA_SAT:
print(f"\nCenário: COM troco | Valor: {valor_satoshis} sat")
tx = carteira.send_to(endereco_destino, valor_satoshis, fee=taxa_por_kb)
valor_enviado = valor_satoshis
cenario = "COM troco"
else:
# Cenário SEM troco: criar 1 output (enviar tudo menos a taxa) e transmitir via tx.send()
print("\nCenário: SEM troco (enviando tudo menos a taxa)")
valor_envio = maximo_enviavel_satoshis
if valor_envio < LIMITE_POEIRA_SAT:
raise Exception(
f"\nValor final ({valor_envio} sat) abaixo do limite de poeira ({LIMITE_POEIRA_SAT} sat)."
)
tx = carteira.transaction_create(
[(endereco_destino, valor_envio)],
fee=taxa_estimada_satoshis,
number_of_change_outputs=0,
max_utxos=1,
random_output_order=False
)
if hasattr(tx, "send") and callable(getattr(tx, "send")):
tx.send()
else:
raise Exception("\nA transação foi criada, mas o método tx.send() não está disponível.")
valor_enviado = valor_envio
cenario = "SEM troco"
# Obtém TXID do objeto retornado/atualizado
txid_reportado = (
getattr(tx, "txid", None)
or getattr(tx, "txid_hex", None)
or (getattr(getattr(tx, "transaction", None), "txid", None) if hasattr(tx, "transaction") else None)
)
if not txid_reportado:
raise Exception("\nNão foi possível obter o TXID da transação (retorno da biblioteca).")
# Valida/corrige TXID no explorador (resolve casos de WTXID/ID interno)
txid_final, houve_correcao = _validar_ou_corrigir_txid(txid_reportado, endereco_destino, valor_enviado)
# Armazena na variável global (sempre o TXID validado/corrigido)
TXID_ITEM9 = txid_final
# Registra no histórico persistente (inclui flag de correção)
registro = {
"data_hora": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"txid_reportado": txid_reportado,
"txid": txid_final,
"corrigido": houve_correcao,
"valor_enviado_sat": valor_enviado,
"taxa_sat": taxa_estimada_satoshis,
"cenario": cenario,
"endereco_destino": endereco_destino,
}
try:
with open(CAMINHO_HISTORICO, "r", encoding="utf-8") as f:
historico = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
historico = []
historico.append(registro)
with open(CAMINHO_HISTORICO, "w", encoding="utf-8") as f:
json.dump(historico, f, ensure_ascii=False, indent=4)
# Exibe informações finais
print("\nTransação criada e transmitida com sucesso!")
if houve_correcao:
print(f"\nAviso: o TXID reportado pela carteira foi corrigido para o TXID compatível com o explorador.")
print(f"\nTXID (reportado): {txid_reportado}")
print(f"\nTXID (final): {txid_final}")
else:
print(f"\nTXID: {txid_final}")
print(f"\nExplorer: https://mempool.space/testnet/tx/{txid_final}")
return txid_final
except Exception as e:
print("\nErro ao criar/enviar transação:", str(e))
TXID_ITEM9 = None
return None
# Uso no fluxo (mantém como no seu notebook):
if DESTINO and VALOR_BTC and TAXA_POR_KB:
criar_e_enviar_transacao(w, DESTINO, VALOR_BTC, TAXA_POR_KB)
else:
print("\nParâmetros de transação inválidos ou não definidos. Processo abortado.")
Cenário: SEM troco (enviando tudo menos a taxa) Transação criada e transmitida com sucesso! Aviso: o TXID reportado pela carteira foi corrigido para o TXID compatível com o explorador. TXID (reportado): 03dbcca84d8868c989288787e75969f6f3ae9510a7fe863d10299376fd5bf704 TXID (final): 9a23e0a52d53f90f9548ca7f9fb4e62db1ce8a32e2529ed6a00d4936113acee8 Explorer: https://mempool.space/testnet/tx/9a23e0a52d53f90f9548ca7f9fb4e62db1ce8a32e2529ed6a00d4936113acee8
10. Verificação e Monitoramento da Transação (com Histórico, Auto-Correção e Redundância de Exploradores)
Nesta etapa, o código obtém o TXID a partir da variável global do Item 9 ou do último registro salvo no histórico. Caso não haja registro, solicita ao usuário.
Se o TXID não for localizado na primeira consulta ao mempool.space, o código tenta corrigi-lo automaticamente buscando pelo endereço de destino e valor esperado no histórico recente do explorador.
Se ainda assim não encontrar, o sistema consulta um segundo explorador — o Blockstream Testnet Explorer — para aumentar a robustez e confiabilidade da verificação.
Após obter um TXID válido, o código exibe a frase “No dia X, às Y, ocorreu a transação…” e inicia o monitoramento contínuo até que a transação seja confirmada ou que o limite de tentativas seja atingido. Durante o monitoramento, o status é atualizado na mesma linha do console para evitar poluição visual.
import json
import time
import requests
from datetime import datetime, timezone
CAMINHO_HISTORICO = "historico_transacoes_testnet.json"
# ---------------------------
# Funções auxiliares (histórico e TXID)
# ---------------------------
def _obter_ultimo_registro():
"""
Lê o arquivo de histórico e retorna o último registro (ou None).
"""
try:
with open(CAMINHO_HISTORICO, "r", encoding="utf-8") as f:
historico = json.load(f)
return historico[-1] if historico else None
except (FileNotFoundError, json.JSONDecodeError):
return None
def obter_txid():
"""
Obtém o TXID a partir de:
- Variável global (Item 9), OU
- Último registro do histórico (arquivo JSON), OU
- Solicita ao usuário via input.
Retorna também metadados (endereço/valor/data) para auto-correção.
"""
# Tenta obter da variável global (Item 9)
if 'TXID_ITEM9' in globals() and TXID_ITEM9:
reg = _obter_ultimo_registro()
meta = {
"endereco_destino": (reg or {}).get("endereco_destino"),
"valor_enviado_sat": (reg or {}).get("valor_enviado_sat"),
"data_hora": (reg or {}).get("data_hora"),
}
return TXID_ITEM9, meta
# Tenta obter do histórico persistente
reg = _obter_ultimo_registro()
if reg:
print(
f"\nNo dia {reg['data_hora']}, ocorreu a transação "
f"{reg['cenario']} com TXID: {reg['txid']}"
)
meta = {
"endereco_destino": reg.get("endereco_destino"),
"valor_enviado_sat": reg.get("valor_enviado_sat"),
"data_hora": reg.get("data_hora"),
}
return reg["txid"], meta
# Solicita ao usuário caso não haja global nem histórico
txid_informado = input("\nInforme o TXID da transação: ").strip()
meta = {"endereco_destino": None, "valor_enviado_sat": None, "data_hora": None}
return (txid_informado if txid_informado else None), meta
# ---------------------------
# Funções auxiliares (consultas aos exploradores)
# ---------------------------
def _extrair_info_tx(dados, txid):
"""
Extrai e normaliza informações da transação a partir do JSON retornado
pelo explorador (formato Esplora).
"""
status = dados.get("status", {})
confirmado = status.get("confirmed", False)
altura_bloco = status.get("block_height", None)
confirmacoes = status.get("confirmations", 0)
taxa_sat = dados.get("fee", 0)
valor_enviado_sat = sum(vout.get("value", 0) for vout in dados.get("vout", []))
# Calcula tempo desde o envio (block_time se confirmado; senão, received)
hora_envio = status.get("block_time") or dados.get("received")
minutos_desde_envio = None
if hora_envio:
try:
ts = datetime.fromtimestamp(hora_envio, tz=timezone.utc)
except Exception:
ts = datetime.fromisoformat(str(hora_envio).replace("Z", "+00:00"))
tempo_passado = datetime.now(timezone.utc) - ts
minutos_desde_envio = int(tempo_passado.total_seconds() // 60)
return {
"txid": txid,
"confirmado": confirmado,
"confirmacoes": confirmacoes,
"altura_bloco": altura_bloco,
"valor_enviado_sat": valor_enviado_sat,
"taxa_sat": taxa_sat,
"minutos_desde_envio": minutos_desde_envio
}
def consultar_transacao_mempool(txid):
"""
Consulta a API do mempool.space (testnet) e retorna informações da transação.
"""
try:
# Consulta a API do mempool.space
url = f"https://mempool.space/testnet/api/tx/{txid}"
r = requests.get(url, timeout=15)
# Trata caso de não propagação/404/erro de rede
if r.status_code != 200:
return None
# Normaliza a resposta em um dicionário
return _extrair_info_tx(r.json(), txid)
except Exception:
# Em qualquer exceção, retorna None para permitir novas tentativas
return None
def consultar_transacao_blockstream(txid):
"""
Consulta a API do Blockstream Testnet Explorer e retorna informações da transação.
"""
try:
# Consulta a API do Blockstream (testnet)
url = f"https://blockstream.info/testnet/api/tx/{txid}"
r = requests.get(url, timeout=15)
# Trata caso de não propagação/404/erro de rede
if r.status_code != 200:
return None
# Normaliza a resposta em um dicionário
return _extrair_info_tx(r.json(), txid)
except Exception:
return None
def _buscar_txid_por_endereco(endereco_destino, valor_estimado_sat, tentativas=3, pausa_seg=5):
"""
Busca um TXID válido pelo endereço de destino e valor esperado.
- Percorre transações recentes do endereço no mempool.space.
- Procura um vout que pague ao endereço e tenha valor compatível.
"""
try:
for _ in range(tentativas):
# Consulta lista de transações do endereço
url = f"https://mempool.space/testnet/api/address/{endereco_destino}/txs"
r = requests.get(url, timeout=15)
if r.status_code != 200:
time.sleep(pausa_seg)
continue
txs = r.json() if isinstance(r.json(), list) else []
for tx in txs:
txid_candidato = tx.get("txid", "")
for vout in tx.get("vout", []):
sc = vout.get("scriptpubkey_address")
val = vout.get("value", 0)
# Critério: paga ao endereço e valor ~compatível (>=90% do estimado)
if sc == endereco_destino and (
valor_estimado_sat is None or
val >= max(1, int(valor_estimado_sat * 0.9))
):
return txid_candidato
time.sleep(pausa_seg)
except Exception:
pass
return None
# ---------------------------
# Monitoramento com redundância e auto-correção
# ---------------------------
def monitorar_transacao(intervalo_segundos=30, max_tentativas=40):
"""
Monitora a transação usando mempool.space e, se necessário, Blockstream Testnet Explorer.
Fluxo:
- Obtém TXID (global/histórico/usuário).
- Exibe anúncio em destaque (cor vinho + negrito).
- Tenta mempool.space; se não achar, tenta auto-correção via endereço/valor.
- Se ainda não achar, tenta Blockstream.
- Entra em loop até confirmar ou atingir o limite, atualizando a mesma linha.
"""
# Obtém o TXID e metadados (para auto-correção, se necessário)
txid, meta = obter_txid()
if not txid:
print("\nNenhum TXID informado. Monitoramento cancelado.")
return
# Linha destacada em negrito e cor vinho
print(f"\n\033[1;38;5;88mMonitorando TXID: {txid}\033[0m")
print(f"\nExplorer: https://mempool.space/testnet/tx/{txid}")
# Primeira tentativa no mempool.space
info = consultar_transacao_mempool(txid)
# Se não encontrar, tenta auto-correção pelo endereço/valor
if not info and (meta.get("endereco_destino") or meta.get("valor_enviado_sat")):
txid_candidato = _buscar_txid_por_endereco(
meta.get("endereco_destino"),
meta.get("valor_enviado_sat")
)
if txid_candidato:
info = consultar_transacao_mempool(txid_candidato)
if info:
print("\nAviso: TXID original não localizado. "
"TXID corrigido automaticamente para monitoramento.")
print(f"\nTXID (corrigido): {txid_candidato}")
txid = txid_candidato
print(f"\nExplorer: https://mempool.space/testnet/tx/{txid}")
# Se ainda não encontrar, tenta no Blockstream
if not info:
info = consultar_transacao_blockstream(txid)
if info:
print("\nAviso: transação localizada via Blockstream Testnet Explorer.")
print(f"Explorer alternativo: https://blockstream.info/testnet/tx/{txid}")
# Loop de consultas até confirmar ou atingir o limite
for tentativa in range(1, max_tentativas + 1):
# Exibe contador de tentativas na mesma linha
print(f"\rConsulta {tentativa}/{max_tentativas} ...", end='', flush=True)
# Consulta atual: prioriza mempool; se vazio, tenta Blockstream
info = info or consultar_transacao_mempool(txid) or consultar_transacao_blockstream(txid)
# Trata caso de não propagação/404/erro de rede
if not info:
print(
f"\rConsulta {tentativa}/{max_tentativas} - "
f"Ainda não localizado em exploradores.", end='', flush=True
)
else:
# Quebra de linha para exibir detalhes formatados
print()
print(f"Status: {'Confirmada' if info['confirmado'] else 'Pendente'}")
# se confirmado, mas confirmação == 0, força a exibição como 1, pois o campo confirmations ainda pode
# aparecer como 0 por alguns segundos/minutos, até que o explorador atualize o contador para 1.
confirmacoes = info.get('confirmacoes', 0)
if info['confirmado'] and confirmacoes == 0:
confirmacoes = 1
print(f"Confirmações: {confirmacoes}")
if info['confirmado']:
print(f"Altura do bloco: {info['altura_bloco']}")
print(f"Valor enviado: {info['valor_enviado_sat']} sat")
print(f"Taxa paga: {info['taxa_sat']} sat")
if info['minutos_desde_envio'] is not None:
print(f"Tempo desde envio: {info['minutos_desde_envio']} minuto(s)")
# Encerra assim que confirmar
if info['confirmado']:
print("\nTransação confirmada. Monitoramento encerrado.")
return
# Limpa 'info' para forçar nova consulta na próxima iteração
info = None
# Aguarda próximo ciclo, se não for a última tentativa
if tentativa < max_tentativas:
time.sleep(intervalo_segundos)
# Encerra por atingir o limite de tentativas
print("\n\nLimite de tentativas atingido. Monitoramento encerrado sem confirmação.")
# ---------------------------
# Uso no fluxo (executa automaticamente)
# ---------------------------
# Executa o monitoramento automaticamente quando o bloco é rodado
monitorar_transacao(intervalo_segundos=30, max_tentativas=40)
No dia 2025-09-01 13:54:41, ocorreu a transação SEM troco com TXID: 9a23e0a52d53f90f9548ca7f9fb4e62db1ce8a32e2529ed6a00d4936113acee8
Monitorando TXID: 9a23e0a52d53f90f9548ca7f9fb4e62db1ce8a32e2529ed6a00d4936113acee8
Explorer: https://mempool.space/testnet/tx/9a23e0a52d53f90f9548ca7f9fb4e62db1ce8a32e2529ed6a00d4936113acee8
Consulta 1/40 ...
Status: Confirmada
Confirmações: 1
Altura do bloco: 4655863
Valor enviado: 78372 sat
Taxa paga: 8780 sat
Tempo desde envio: 45265 minuto(s)
Transação confirmada. Monitoramento encerrado.