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
235 lines
7.2 KiB
Python
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()
|