From 2d42c86e701b085bcc4cda8dc248d0e1e7e540ea Mon Sep 17 00:00:00 2001 From: scgbckbone Date: Wed, 3 Dec 2025 15:48:16 +0100 Subject: [PATCH] produce .gif BBQr with --bbqr flag --- README.md | 1 + psbt_faker/__init__.py | 43 ++++++++++++++++++++++++++++++++++++++---- requirements.txt | 5 +++++ setup.py | 7 +++++-- 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a65008f..403efa2 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ Options: num-outs -f, --fee INTEGER Miner's fee in Satoshis -2, --psbt2 Make PSBTv2 + -b, --bbqr Output BBQr OUTPUT.gif in CWD -s, --segwit [SS] Make inputs be segwit style -w, --wrapped [SS] Make inputs be wrapped segwit style (requires --segwit flag) diff --git a/psbt_faker/__init__.py b/psbt_faker/__init__.py index e821a0c..a0907b8 100755 --- a/psbt_faker/__init__.py +++ b/psbt_faker/__init__.py @@ -7,12 +7,15 @@ # That will create the command "psbt_faker" in your path... or just use "./main.py ..." here # # -import click +import click, io, pyqrcode, os from binascii import b2a_hex as _b2a_hex from base64 import b64encode from decimal import Decimal from .txn import fake_ms_txn, fake_txn, ADDR_STYLES from .multisig import from_simple_text +from bbqr import split_qrs +from PIL import Image, ImageDraw, ImageChops + b2a_hex = lambda a: str(_b2a_hex(a), 'ascii') #xfp2hex = lambda a: b2a_hex(a[::-1]).upper() @@ -36,6 +39,7 @@ def repeat_until_length(values, target_length): @click.option('--num-change', '-c', help="Number of change outputs (default 1) from num-outs", default=1) @click.option('--fee', '-f', help="Miner's fee in Satoshis", default=1000) @click.option('--psbt2', '-2', help="Make PSBTv2", is_flag=True, default=False) +@click.option('--bbqr', '-b', help="Output BBQr OUTPUT.gif in CWD", is_flag=True, default=False) @click.option('--segwit', '-s', help="[SS] Make inputs be segwit style", is_flag=True, default=False) @click.option('--wrapped', '-w', help="[SS] Make inputs be wrapped segwit style (requires --segwit flag)", is_flag=True, default=False) @click.option('--styles', '-a', help="Output address style (multiple ok). If multisig only applies to non-change addresses.", multiple=True, default=None, type=click.Choice(ADDR_STYLES)) @@ -48,7 +52,7 @@ def repeat_until_length(values, target_length): @click.option('--input-amount', '-n', help="Size of each input in sats (default 100k sats each input)", default=(100000,), multiple=True, type=int) @click.option('--incl-xpubs', '-I', help="[MS] Include XPUBs in PSBT global section", is_flag=True, default=False) def main(num_ins, sequence, num_change, num_outs, output_amount, out_psbt, testnet, xpub, segwit, fee, styles, base64, - partial, zero_xfp, multisig, locktime, input_amount, psbt2, incl_xpubs, wrapped): + partial, zero_xfp, multisig, locktime, input_amount, psbt2, incl_xpubs, wrapped, bbqr): '''Construct a valid PSBT which spends non-existant BTC to random addresses!''' if locktime == "current": @@ -99,8 +103,8 @@ def main(num_ins, sequence, num_change, num_outs, output_amount, out_psbt, testn change_outputs=change_outputs, psbt_v2=psbt2, sequences=sequence) - - out_psbt.write(psbt if not base64 else b64encode(psbt)) + res = b64encode(psbt) if base64 else psbt + out_psbt.write(res) print(f"\nFake PSBT would send {(sum(input_amount)/Decimal(1E8))} BTC to: ") print('\n'.join(" %.8f => %s %s" % (Decimal(amt)/Decimal(1E8),dest, ' (change back)' if chg else '') @@ -110,6 +114,37 @@ def main(num_ins, sequence, num_change, num_outs, output_amount, out_psbt, testn print("\nPSBT to be signed: " + out_psbt.name, end='\n\n') + if bbqr: + bbqr_out_file = os.path.splitext(out_psbt.name)[0] + ".gif" + vers, parts = split_qrs(res, type_code="U" if base64 else "P", encoding="2", + max_version=20) + qs = [pyqrcode.create(data, error='L', version=vers, mode='alphanumeric') for + data in parts] + + frames = [] + num_parts = len(parts) + for i in range(num_parts): + xbm = qs[i].xbm(scale=4, quiet_zone=10) + img = ImageChops.invert(Image.open(io.BytesIO(xbm.encode()))).convert('L') + if num_parts > 1: + # add progress bar + pw = img.width // num_parts + lm = (img.width - (pw * num_parts)) // 2 + draw = ImageDraw.Draw(img) + h = 4 // 2 + y = img.height - h - (4 // 2) - 1 + + for j in range(num_parts): + draw.rectangle((lm + (j * pw), y, lm + ((j + 1) * pw), y + h), fill=(128 if i != j else 0)) + + frames.append(img) + + if num_parts == 1: + frames[0].save(bbqr_out_file) + else: + frames[0].save(bbqr_out_file, format="gif", save_all=True, loop=0, + duration=250, default_image=False, append_images=frames[1:]) + if __name__ == '__main__': main() diff --git a/requirements.txt b/requirements.txt index a29ff45..dcd3b48 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,8 @@ click>=6.7 ecdsa # or can use python-secp256k1 + +# BBQr library +git+https://github.com/coinkite/BBQr.git@master#egg=bbqr&subdirectory=python +pyqrcode +Pillow \ No newline at end of file diff --git a/setup.py b/setup.py index e6f7913..3c13c83 100644 --- a/setup.py +++ b/setup.py @@ -25,8 +25,11 @@ if __name__ == '__main__': py_modules=['psbt_faker'], python_requires='>3.6.0', install_requires=[ - 'Click', - 'ecdsa', + "click>=6.7", + "ecdsa", + "pyqrcode", + "Pillow", + "bbqr @ git+https://github.com/coinkite/BBQr.git@master#subdirectory=python", ], entry_points=''' [console_scripts]