Skip to content

Commit b203da7

Browse files
committed
paint cancellation plan pdf
1 parent 6ffb3df commit b203da7

File tree

1 file changed

+171
-163
lines changed
  • electrum/plugins/timelock_recovery

1 file changed

+171
-163
lines changed

electrum/plugins/timelock_recovery/qt.py

+171-163
Original file line numberDiff line numberDiff line change
@@ -1127,196 +1127,204 @@ def _paint_recovery_plan_pdf(self, context: TimelockRecoveryContext, painter: QP
11271127
)
11281128

11291129
def _save_cancellation_plan_pdf(self, context: TimelockRecoveryContext, download_dialog: WindowModalDialog):
1130-
try:
1131-
cancellation_raw = context.cancellation_tx.serialize().upper()
1132-
if len(cancellation_raw) > 2300:
1133-
# Splitting the cancellation transaction into multiple QR codes is not implemented
1134-
# because it is unexpected to happen anyways.
1135-
raise Exception("Cancellation transaction is too large to be saved as a single QR code")
1136-
1137-
# Open a Save As dialog to get the file path
1138-
file_path, _selected_filter = QFileDialog.getSaveFileName(
1139-
download_dialog,
1140-
_("Save Cancellation Plan PDF..."),
1141-
os.path.join(self.base_dir, "timelock-cancellation-plan-{}.pdf".format(context.recovery_plan_id)),
1142-
_("PDF files (*.pdf)")
1143-
)
1144-
if not file_path:
1145-
return
1130+
# Open a Save As dialog to get the file path
1131+
file_path, _selected_filter = QFileDialog.getSaveFileName(
1132+
download_dialog,
1133+
_("Save Cancellation Plan PDF..."),
1134+
os.path.join(self.base_dir, "timelock-cancellation-plan-{}.pdf".format(context.recovery_plan_id)),
1135+
_("PDF files (*.pdf)")
1136+
)
1137+
if not file_path:
1138+
return
11461139

1147-
printer = self._create_pdf_printer()
1140+
painter = QPainter()
1141+
temp_file_path: Optional[str] = None
11481142

1149-
# Create painter
1150-
painter = QPainter()
1143+
try:
1144+
with tempfile.NamedTemporaryFile(dir=os.path.dirname(file_path), prefix=f"{os.path.basename(file_path)}-", delete=False) as temp_file:
1145+
temp_file_path = temp_file.name
1146+
printer = self._create_pdf_printer(temp_file_path)
11511147
if not painter.begin(printer):
11521148
return
1149+
self._paint_cancellation_plan_pdf(context, painter, printer)
1150+
painter.end()
1151+
shutil.move(temp_file_path, file_path)
1152+
download_dialog.show_message(_("File saved successfully"))
1153+
except (IOError, MemoryError) as e:
1154+
self.logger.exception(repr(e))
1155+
download_dialog.show_error(_("Error saving file"))
1156+
if temp_file_path is not None and os.path.exists(temp_file_path):
1157+
os.remove(temp_file_path)
1158+
finally:
1159+
if painter.isActive():
1160+
painter.end()
11531161

1154-
font_manager = FontManager(self.font_name, printer.resolution())
1155-
1156-
# Get page dimensions
1157-
page_rect = printer.pageRect(QPrinter.Unit.DevicePixel)
1158-
page_width = page_rect.width()
1159-
page_height = page_rect.height()
1160-
1161-
current_height = 0
1162-
page_number = 1
1162+
def _paint_cancellation_plan_pdf(self, context: TimelockRecoveryContext, painter: QPainter, printer: QPrinter):
1163+
cancellation_raw = context.cancellation_tx.serialize().upper()
1164+
if len(cancellation_raw) > 2300:
1165+
# Splitting the cancellation transaction into multiple QR codes is not implemented
1166+
# because it is unexpected to happen anyways.
1167+
raise Exception("Cancellation transaction is too large to be saved as a single QR code")
11631168

1164-
# Header
1165-
painter.setFont(font_manager.header_font)
1166-
painter.drawText(
1167-
QRectF(0, current_height, page_width, font_manager.header_line_spacing),
1168-
Qt.AlignmentFlag.AlignCenter,
1169-
f"Cancellation-Guide Date: {context.recovery_plan_created_at.strftime('%Y-%m-%d %H:%M:%S %Z (%z)')} ID: {context.recovery_plan_id} Page: {page_number}"
1170-
)
1171-
current_height += font_manager.header_line_spacing + 40
1169+
font_manager = FontManager(self.font_name, printer.resolution())
11721170

1173-
current_height += self._paint_scaled_logo(painter, page_width, current_height) + 40
1171+
# Get page dimensions
1172+
page_rect = printer.pageRect(QPrinter.Unit.DevicePixel)
1173+
page_width = page_rect.width()
1174+
page_height = page_rect.height()
11741175

1175-
# Title
1176-
painter.setFont(font_manager.title_font)
1177-
painter.drawText(
1178-
QRectF(0, current_height, page_width, font_manager.title_line_spacing),
1179-
Qt.AlignmentFlag.AlignCenter,
1180-
"Timelock-Recovery Cancellation Guide"
1181-
)
1182-
current_height += font_manager.title_line_spacing + 20
1176+
current_height = 0
1177+
page_number = 1
11831178

1184-
# Subtitle
1185-
painter.setFont(font_manager.subtitle_font)
1186-
painter.drawText(
1187-
QRectF(0, current_height, page_width, font_manager.subtitle_line_spacing + 20), Qt.AlignmentFlag.AlignCenter,
1188-
f"Electrum Version: {version.ELECTRUM_VERSION} - Plugin Version: {plugin_version}"
1189-
)
1190-
current_height += font_manager.subtitle_line_spacing + 60
1179+
# Header
1180+
painter.setFont(font_manager.header_font)
1181+
painter.drawText(
1182+
QRectF(0, current_height, page_width, font_manager.header_line_spacing),
1183+
Qt.AlignmentFlag.AlignCenter,
1184+
f"Cancellation-Guide Date: {context.recovery_plan_created_at.strftime('%Y-%m-%d %H:%M:%S %Z (%z)')} ID: {context.recovery_plan_id} Page: {page_number}"
1185+
)
1186+
current_height += font_manager.header_line_spacing + 40
11911187

1192-
# Main text
1193-
painter.setFont(font_manager.body_font)
1194-
explanation_text = (
1195-
f"This document is intended solely for the eyes of the owner of wallet: {context.wallet_name}. "
1196-
f"The Recovery Guide (the other document) will allow to transfer the funds from this wallet to "
1197-
f"a different wallet within {context.timelock_days} days. To prevent this from happening accidentally "
1198-
f"or maliciously by someone who found that document, you should periodically check if the Alert "
1199-
f"transaction has been broadcasted, using a Bitcoin block-explorer website such as:"
1200-
)
1201-
drawn_rect = painter.drawText(
1202-
QRectF(20, current_height, page_width - 40, page_height),
1203-
Qt.TextFlag.TextWordWrap,
1204-
explanation_text
1205-
)
1206-
current_height += drawn_rect.height() + 40
1188+
current_height += self._paint_scaled_logo(painter, page_width, current_height) + 40
12071189

1208-
# QR codes and links for transaction tracking
1209-
for link in [f"https://mempool.space/tx/{context.alert_tx.txid()}", f"https://blockstream.info/tx/{context.alert_tx.txid()}"]:
1210-
qr = qrcode.main.QRCode(
1211-
error_correction=qrcode.constants.ERROR_CORRECT_H,
1212-
)
1213-
qr.add_data(link)
1214-
qr.make()
1215-
qr_image = self._paint_qr(qr)
1190+
# Title
1191+
painter.setFont(font_manager.title_font)
1192+
painter.drawText(
1193+
QRectF(0, current_height, page_width, font_manager.title_line_spacing),
1194+
Qt.AlignmentFlag.AlignCenter,
1195+
"Timelock-Recovery Cancellation Guide"
1196+
)
1197+
current_height += font_manager.title_line_spacing + 20
12161198

1217-
qr_width = int(page_width * 0.2)
1218-
qr_x = (page_width - qr_width) / 2
1219-
painter.drawImage(QRectF(qr_x, current_height, qr_width, qr_width), qr_image)
1220-
current_height += qr_width + 20
1199+
# Subtitle
1200+
painter.setFont(font_manager.subtitle_font)
1201+
painter.drawText(
1202+
QRectF(0, current_height, page_width, font_manager.subtitle_line_spacing + 20), Qt.AlignmentFlag.AlignCenter,
1203+
f"Electrum Version: {version.ELECTRUM_VERSION} - Plugin Version: {plugin_version}"
1204+
)
1205+
current_height += font_manager.subtitle_line_spacing + 60
12211206

1222-
painter.setFont(font_manager.body_small_font)
1223-
painter.drawText(
1224-
QRectF(0, current_height, page_width, font_manager.body_small_line_spacing),
1225-
Qt.AlignmentFlag.AlignCenter,
1226-
link
1227-
)
1228-
current_height += font_manager.body_small_line_spacing + 20
1207+
# Main text
1208+
painter.setFont(font_manager.body_font)
1209+
explanation_text = (
1210+
f"This document is intended solely for the eyes of the owner of wallet: {context.wallet_name}. "
1211+
f"The Recovery Guide (the other document) will allow to transfer the funds from this wallet to "
1212+
f"a different wallet within {context.timelock_days} days. To prevent this from happening accidentally "
1213+
f"or maliciously by someone who found that document, you should periodically check if the Alert "
1214+
f"transaction has been broadcasted, using a Bitcoin block-explorer website such as:"
1215+
)
1216+
drawn_rect = painter.drawText(
1217+
QRectF(20, current_height, page_width - 40, page_height),
1218+
Qt.TextFlag.TextWordWrap,
1219+
explanation_text
1220+
)
1221+
current_height += drawn_rect.height() + 40
12291222

1230-
# Watch tower text
1231-
painter.setFont(font_manager.body_font)
1232-
drawn_rect = painter.drawText(
1233-
QRectF(20, current_height, page_width - 40, page_height - current_height),
1234-
Qt.TextFlag.TextWordWrap,
1235-
"It is also recommended to use a Watch-Tower service that will notify you immediately if the"
1236-
" Alert transaction has been broadcasted. For more details, visit: https://timelockrecovery.com ."
1237-
)
1238-
current_height += drawn_rect.height() + 40
1239-
1240-
# Cancellation transaction section
1241-
cancellation_text = (
1242-
"In case the Alert transaction has been broadcasted, and you want to stop the funds from "
1243-
"leaving this wallet, you can scan the QR code on page 2, and broadcast "
1244-
"the content using one of the following Bitcoin block-explorer websites:\n\n"
1245-
"• https://mempool.space/tx/push\n"
1246-
"• https://blockstream.info/tx/push\n"
1247-
"• https://coinb.in/#broadcast\n\n"
1248-
"If the transaction is not confirmed within reasonable time due to a low fee, you will have "
1249-
"to access the wallet and use Replace-By-Fee/Child-Pay-For-Parent to move the funds to a new "
1250-
"address on your wallet. (you can also pay to an Acceleration Service such as the one offered "
1251-
"by https://mempool.space)\n\n"
1252-
f"IMPORTANT NOTICE: If you lost the keys to access wallet {context.wallet_name} - do not broadcast the "
1253-
"transaction on page 2! In this case it is recommended to destroy all copies of this document."
1254-
)
1255-
painter.drawText(
1256-
QRectF(20, current_height, page_width - 40, page_height),
1257-
Qt.TextFlag.TextWordWrap,
1258-
cancellation_text
1223+
# QR codes and links for transaction tracking
1224+
for link in [f"https://mempool.space/tx/{context.alert_tx.txid()}", f"https://blockstream.info/tx/{context.alert_tx.txid()}"]:
1225+
qr = qrcode.main.QRCode(
1226+
error_correction=qrcode.constants.ERROR_CORRECT_H,
12591227
)
1228+
qr.add_data(link)
1229+
qr.make()
1230+
qr_image = self._paint_qr(qr)
12601231

1261-
# New page for cancellation transaction
1262-
printer.newPage()
1263-
page_number += 1
1264-
current_height = 20
1232+
qr_width = int(page_width * 0.2)
1233+
qr_x = (page_width - qr_width) / 2
1234+
painter.drawImage(QRectF(qr_x, current_height, qr_width, qr_width), qr_image)
1235+
current_height += qr_width + 20
12651236

1266-
# Header
1267-
painter.setFont(font_manager.header_font)
1237+
painter.setFont(font_manager.body_small_font)
12681238
painter.drawText(
1269-
QRectF(0, current_height, page_width, font_manager.header_line_spacing),
1239+
QRectF(0, current_height, page_width, font_manager.body_small_line_spacing),
12701240
Qt.AlignmentFlag.AlignCenter,
1271-
f"Cancellation-Guide Date: {context.recovery_plan_created_at.strftime('%Y-%m-%d %H:%M:%S %Z (%z)')} ID: {context.recovery_plan_id} Page: {page_number}"
1241+
link
12721242
)
1273-
current_height += font_manager.header_line_spacing + 20
1243+
current_height += font_manager.body_small_line_spacing + 20
12741244

1275-
# Cancellation transaction title
1276-
painter.setFont(font_manager.title_font)
1277-
painter.drawText(
1278-
QRectF(0, current_height, page_width, font_manager.title_line_spacing),
1279-
Qt.AlignmentFlag.AlignCenter,
1280-
"Cancellation Transaction"
1281-
)
1282-
current_height += font_manager.title_line_spacing + 20
1245+
# Watch tower text
1246+
painter.setFont(font_manager.body_font)
1247+
drawn_rect = painter.drawText(
1248+
QRectF(20, current_height, page_width - 40, page_height - current_height),
1249+
Qt.TextFlag.TextWordWrap,
1250+
"It is also recommended to use a Watch-Tower service that will notify you immediately if the"
1251+
" Alert transaction has been broadcasted. For more details, visit: https://timelockrecovery.com ."
1252+
)
1253+
current_height += drawn_rect.height() + 40
12831254

1284-
# Transaction ID
1285-
painter.setFont(font_manager.subtitle_font)
1286-
painter.drawText(
1287-
QRectF(0, current_height, page_width, font_manager.subtitle_line_spacing),
1288-
Qt.AlignmentFlag.AlignCenter,
1289-
f"Transaction Id: {context.cancellation_tx.txid()}"
1290-
)
1291-
current_height += font_manager.subtitle_line_spacing + 20
1255+
# Cancellation transaction section
1256+
cancellation_text = (
1257+
"In case the Alert transaction has been broadcasted, and you want to stop the funds from "
1258+
"leaving this wallet, you can scan the QR code on page 2, and broadcast "
1259+
"the content using one of the following Bitcoin block-explorer websites:\n\n"
1260+
"• https://mempool.space/tx/push\n"
1261+
"• https://blockstream.info/tx/push\n"
1262+
"• https://coinb.in/#broadcast\n\n"
1263+
"If the transaction is not confirmed within reasonable time due to a low fee, you will have "
1264+
"to access the wallet and use Replace-By-Fee/Child-Pay-For-Parent to move the funds to a new "
1265+
"address on your wallet. (you can also pay to an Acceleration Service such as the one offered "
1266+
"by https://mempool.space)\n\n"
1267+
f"IMPORTANT NOTICE: If you lost the keys to access wallet {context.wallet_name} - do not broadcast the "
1268+
"transaction on page 2! In this case it is recommended to destroy all copies of this document."
1269+
)
1270+
painter.drawText(
1271+
QRectF(20, current_height, page_width - 40, page_height),
1272+
Qt.TextFlag.TextWordWrap,
1273+
cancellation_text
1274+
)
12921275

1293-
# QR Code for cancellation transaction
1294-
qr = qrcode.main.QRCode(
1295-
error_correction=qrcode.constants.ERROR_CORRECT_Q,
1296-
)
1297-
qr.add_data(cancellation_raw)
1298-
qr.make()
1299-
qr_image = self._paint_qr(qr)
1276+
# New page for cancellation transaction
1277+
printer.newPage()
1278+
page_number += 1
1279+
current_height = 20
13001280

1301-
qr_width = int(page_width * 0.6)
1302-
qr_x = (page_width - qr_width) / 2
1303-
painter.drawImage(QRectF(qr_x, current_height, qr_width, qr_width), qr_image)
1304-
current_height += qr_width + 40
1281+
# Header
1282+
painter.setFont(font_manager.header_font)
1283+
painter.drawText(
1284+
QRectF(0, current_height, page_width, font_manager.header_line_spacing),
1285+
Qt.AlignmentFlag.AlignCenter,
1286+
f"Cancellation-Guide Date: {context.recovery_plan_created_at.strftime('%Y-%m-%d %H:%M:%S %Z (%z)')} ID: {context.recovery_plan_id} Page: {page_number}"
1287+
)
1288+
current_height += font_manager.header_line_spacing + 20
13051289

1306-
# Raw transaction text
1307-
painter.setFont(font_manager.body_font)
1308-
painter.drawText(
1309-
QRectF(20, current_height, page_width - 40, page_height),
1310-
Qt.TextFlag.TextWrapAnywhere,
1311-
cancellation_raw
1312-
)
1290+
# Cancellation transaction title
1291+
painter.setFont(font_manager.title_font)
1292+
painter.drawText(
1293+
QRectF(0, current_height, page_width, font_manager.title_line_spacing),
1294+
Qt.AlignmentFlag.AlignCenter,
1295+
"Cancellation Transaction"
1296+
)
1297+
current_height += font_manager.title_line_spacing + 20
13131298

1314-
painter.end()
1299+
# Transaction ID
1300+
painter.setFont(font_manager.subtitle_font)
1301+
painter.drawText(
1302+
QRectF(0, current_height, page_width, font_manager.subtitle_line_spacing),
1303+
Qt.AlignmentFlag.AlignCenter,
1304+
f"Transaction Id: {context.cancellation_tx.txid()}"
1305+
)
1306+
current_height += font_manager.subtitle_line_spacing + 20
13151307

1316-
download_dialog.show_message(_("File saved successfully"))
1317-
except Exception as e:
1318-
self.logger.exception(repr(e))
1319-
download_dialog.show_error(_("Error saving file"))
1308+
# QR Code for cancellation transaction
1309+
qr = qrcode.main.QRCode(
1310+
error_correction=qrcode.constants.ERROR_CORRECT_Q,
1311+
)
1312+
qr.add_data(cancellation_raw)
1313+
qr.make()
1314+
qr_image = self._paint_qr(qr)
1315+
1316+
qr_width = int(page_width * 0.6)
1317+
qr_x = (page_width - qr_width) / 2
1318+
painter.drawImage(QRectF(qr_x, current_height, qr_width, qr_width), qr_image)
1319+
current_height += qr_width + 40
1320+
1321+
# Raw transaction text
1322+
painter.setFont(font_manager.body_font)
1323+
painter.drawText(
1324+
QRectF(20, current_height, page_width - 40, page_height),
1325+
Qt.TextFlag.TextWrapAnywhere,
1326+
cancellation_raw
1327+
)
13201328

13211329
@classmethod
13221330
def _paint_qr(cls, qr: qrcode.main.QRCode) -> QImage:

0 commit comments

Comments
 (0)