walletpress/backend/core/pdf.py
cryptorugmunch e13bd4d774
Some checks are pending
CI / lint (push) Waiting to run
CI / test (push) Waiting to run
CI / security (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / license (push) Waiting to run
CI / ai-review (push) Waiting to run
docs: apply fleet-template (16-artifact scaffold)
Adds missing standard artifacts:
- README.md (if missing)
- AGENTS.md (AI agent contract)
- PLAN.md (current sprint)
- STATUS.md (where we are)
- DEVELOPMENT.md (dev workflow)
- DEPLOYMENT.md (deploy procedure)
- TESTING.md (test strategy)
- DECISIONS.md (ADR index + templates)
- .github/CODEOWNERS
- .github/workflows/ci.yml

Preserves all existing artifacts.

Refs: RugMunchMedia/fleet-template
2026-07-02 02:07:06 +07:00

235 lines
7.2 KiB
Python

"""PDF generation for paper wallets and birth certificates."""
from __future__ import annotations
import io
import logging
import time
from typing import Optional
import qrcode
from core.config import cfg
logger = logging.getLogger("wp.pdf")
try:
from reportlab.lib.pagesizes import letter
from reportlab.lib.units import inch
from reportlab.pdfgen import canvas
from reportlab.lib.utils import ImageReader
HAS_REPORTLAB = True
except ImportError:
HAS_REPORTLAB = False
try:
HAS_QR = True
except ImportError:
HAS_QR = False
def _qr_image(data: str, size: int = 200) -> Optional[ImageReader]:
if not HAS_QR:
return None
import io
qr = qrcode.QRCode(box_size=4, border=2)
qr.add_data(data)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white").convert("RGB")
buf = io.BytesIO()
img.save(buf, format="PNG")
buf.seek(0)
return ImageReader(buf)
def generate_paper_wallet(address: str, chain: str, private_key: str = "",
public_key: str = "", derivation_path: str = "",
created_at: float = 0.0, proof_leaf: str = "",
label: str = "") -> Optional[bytes]:
"""Generate a printable paper wallet PDF.
Returns PDF bytes or None if reportlab is not installed.
Paper wallets are for cold storage. Print on durable paper. Store safely.
"""
if not HAS_REPORTLAB:
return None
buf = io.BytesIO()
c = canvas.Canvas(buf, pagesize=letter)
w, h = letter
margin = 0.75 * inch
y = h - margin
# Title
c.setFont("Helvetica-Bold", 20)
c.drawString(margin, y, "WALLETPRESS PAPER WALLET")
y -= 30
c.setFont("Helvetica", 10)
c.drawString(margin, y, f"Generated: {time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime(created_at or time.time()))}")
y -= 14
c.drawString(margin, y, f"Software: WalletPress Pro v{cfg.version}")
y -= 14
c.drawString(margin, y, "Chain: " + chain.upper())
y -= 14
if label:
c.drawString(margin, y, f"Label: {label}")
y -= 14
y -= 20
# Warning box
c.setFillColorRGB(1, 0.9, 0.9)
c.rect(margin, y - 50, w - 2 * margin, 50, fill=1, stroke=0)
c.setFillColorRGB(0.8, 0, 0)
c.setFont("Helvetica-Bold", 11)
c.drawString(margin + 8, y - 16, "⚠ KEEP THIS DOCUMENT SECURE")
c.setFont("Helvetica", 9)
c.setFillColorRGB(0.5, 0, 0)
c.drawString(margin + 8, y - 32, "Anyone with this paper can control the crypto assets. Store in a safe, waterproof location.")
c.setFillColorRGB(0, 0, 0)
y -= 70
# Address section
c.setFont("Helvetica-Bold", 12)
c.drawString(margin, y, "Public Address")
y -= 16
c.setFont("Courier", 9)
c.drawString(margin, y, address)
y -= 30
# QR code for address
qr = _qr_image(f"{chain}:{address}")
if qr:
c.drawImage(qr, margin, y - 100, width=100, height=100)
y -= 120
# Private key section
if private_key:
c.setFont("Helvetica-Bold", 12)
c.setFillColorRGB(0.8, 0, 0)
c.drawString(margin, y, "PRIVATE KEY (KEEP SECRET)")
y -= 16
c.setFont("Courier", 8)
c.setFillColorRGB(0, 0, 0)
# Split long key into lines
key_lines = [private_key[i:i+48] for i in range(0, len(private_key), 48)]
for line in key_lines:
c.drawString(margin, y, line)
y -= 12
y -= 10
# Public key
if public_key:
c.setFont("Helvetica-Bold", 11)
c.drawString(margin, y, "Public Key")
y -= 14
c.setFont("Courier", 7)
pk_lines = [public_key[i:i+64] for i in range(0, len(public_key), 64)]
for line in pk_lines[:4]:
c.drawString(margin, y, line)
y -= 10
y -= 10
# Derivation path
if derivation_path:
c.setFont("Helvetica", 9)
c.drawString(margin, y, f"Derivation: {derivation_path}")
y -= 14
# Proof hash
if proof_leaf:
c.setFont("Helvetica", 7)
c.drawString(margin, y, f"Proof: {proof_leaf[:48]}...")
y -= 10
# Footer
c.setFont("Helvetica", 7)
c.setFillColorRGB(0.5, 0.5, 0.5)
c.drawString(margin, margin * 0.5, f"WalletPress Pro — walletpress.cc — {cfg.version} — MIT Open Source")
c.save()
return buf.getvalue()
def generate_birth_certificate(wallet_id: str, address: str, chain: str,
public_key: str = "", created_at: float = 0.0,
proof_leaf: str = "", root_hash: str = "",
committed: bool = False) -> Optional[bytes]:
"""Generate a Wallet Birth Certificate — printable provenance document.
This is a unique feature. It cryptographically proves when and how
this wallet was created. The Proof of Generation hash links this
document to the immutable audit trail.
"""
if not HAS_REPORTLAB:
return None
buf = io.BytesIO()
c = canvas.Canvas(buf, pagesize=letter)
w, h = letter
margin = 0.75 * inch
y = h - margin
# Certificate border
c.setStrokeColorRGB(0.77, 0.55, 0.30)
c.setLineWidth(3)
c.rect(margin - 10, margin - 10, w - 2 * margin + 20, h - 2 * margin + 20)
# Seal graphic
c.setFillColorRGB(0.77, 0.55, 0.30)
c.setFont("Helvetica-Bold", 28)
c.drawString(margin, y, "✦ WALLET BIRTH CERTIFICATE ✦")
y -= 36
c.setFont("Helvetica", 12)
c.setFillColorRGB(0.4, 0.4, 0.4)
c.drawString(margin, y, "This certifies that the following wallet was generated by WalletPress")
y -= 14
c.drawString(margin, y, "with cryptographic proof of its creation time, code version, and integrity.")
y -= 30
# Details
c.setFont("Helvetica-Bold", 11)
c.setFillColorRGB(0, 0, 0)
details = [
("Wallet ID", wallet_id),
("Address", address),
("Chain", chain.upper()),
("Created", time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime(created_at))),
("Public Key", public_key[:48] + "..." if len(public_key) > 48 else public_key),
]
for label, value in details:
c.setFont("Helvetica-Bold", 10)
c.drawString(margin, y, label + ":")
c.setFont("Courier", 9)
c.drawString(margin + 100, y, value)
y -= 16
y -= 10
# Proof section
c.setStrokeColorRGB(0.77, 0.55, 0.30)
c.setLineWidth(1)
c.rect(margin, y - 50, w - 2 * margin, 50)
c.setFont("Helvetica-Bold", 10)
c.drawString(margin + 6, y - 14, "Proof of Generation — Merkle Attestation")
c.setFont("Courier", 7)
c.drawString(margin + 6, y - 28, f"Leaf: {proof_leaf}")
if root_hash:
c.drawString(margin + 6, y - 40, f"Root: {root_hash}")
y -= 70
# QR for verification
qr = _qr_image(f"https://walletpress.cc/api/v1/proof/verify/{wallet_id}")
if qr:
c.drawImage(qr, w - margin - 100, y - 100, width=100, height=100)
y -= 120
# Trust text
c.setFont("Helvetica", 8)
c.setFillColorRGB(0.5, 0.5, 0.5)
c.drawString(margin, y, "Verify: walletpress.cc/api/v1/proof/verify/{wallet_id}")
y -= 10
c.drawString(margin, y, f"Software: WalletPress Pro v{cfg.version} — MIT Open Source — walletpress.cc")
c.save()
return buf.getvalue()