@@ -1127,196 +1127,204 @@ def _paint_recovery_plan_pdf(self, context: TimelockRecoveryContext, painter: QP
1127
1127
)
1128
1128
1129
1129
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
1146
1139
1147
- printer = self ._create_pdf_printer ()
1140
+ painter = QPainter ()
1141
+ temp_file_path : Optional [str ] = None
1148
1142
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 )
1151
1147
if not painter .begin (printer ):
1152
1148
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 ()
1153
1161
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" )
1163
1168
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 ())
1172
1170
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 ()
1174
1175
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
1183
1178
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
1191
1187
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
1207
1189
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
1216
1198
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
1221
1206
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
1229
1222
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 ,
1259
1227
)
1228
+ qr .add_data (link )
1229
+ qr .make ()
1230
+ qr_image = self ._paint_qr (qr )
1260
1231
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
1265
1236
1266
- # Header
1267
- painter .setFont (font_manager .header_font )
1237
+ painter .setFont (font_manager .body_small_font )
1268
1238
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 ),
1270
1240
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
1272
1242
)
1273
- current_height += font_manager .header_line_spacing + 20
1243
+ current_height += font_manager .body_small_line_spacing + 20
1274
1244
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
1283
1254
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
+ )
1292
1275
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
1300
1280
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
1305
1289
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
1313
1298
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
1315
1307
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
+ )
1320
1328
1321
1329
@classmethod
1322
1330
def _paint_qr (cls , qr : qrcode .main .QRCode ) -> QImage :
0 commit comments