Simulação de Transação Bitcoin (Testnet) em Python

Luciano Magalhães   |    Agosto, 2025   | Desenvolvimento / Blockchain

Banner do Projeto BTC

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ços tb1...);
  • Módulos do pacote btc_wallet_testnet (na pasta src/), que fornecem a base para derivação de chaves, endereços e integração com a biblioteca bitcoinlib;
  • 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.

In [1]:
# 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.

In [2]:
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.

In [3]:
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.

In [4]:
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.

In [5]:
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.

In [ ]:
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.

In [2]:
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.
In [ ]:

© Copyright 2025 | Luciano Magalhães