Repository: spesmilo/electrum Branch: master Commit: dceece1cda9b Files: 776 Total size: 8.9 MB Directory structure: gitextract_3c6jsf9o/ ├── .cirrus.yml ├── .editorconfig ├── .gitattributes ├── .github/ │ └── ISSUE_TEMPLATE/ │ ├── 01_issue.yml │ └── config.yml ├── .gitignore ├── .gitmodules ├── AUTHORS ├── LICENCE ├── MANIFEST.in ├── README.md ├── RELEASE-NOTES ├── SECURITY.md ├── contrib/ │ ├── add_cosigner │ ├── android/ │ │ ├── Dockerfile │ │ ├── Makefile │ │ ├── Readme.md │ │ ├── apkdiff.py │ │ ├── apt.preferences │ │ ├── apt.sources.list │ │ ├── bitcoin_intent.xml │ │ ├── build.sh │ │ ├── buildozer_qml.spec │ │ ├── dl-ndk-ci.sh │ │ ├── get_apk_versioncode.py │ │ ├── make_apk.sh │ │ ├── make_barcode_scanner.sh │ │ └── p4a_recipes/ │ │ ├── README.md │ │ ├── cffi/ │ │ │ └── __init__.py │ │ ├── cryptography/ │ │ │ └── __init__.py │ │ ├── hostpython3/ │ │ │ └── __init__.py │ │ ├── libffi/ │ │ │ └── __init__.py │ │ ├── libiconv/ │ │ │ └── __init__.py │ │ ├── libsecp256k1/ │ │ │ └── __init__.py │ │ ├── libzbar/ │ │ │ └── __init__.py │ │ ├── openssl/ │ │ │ └── __init__.py │ │ ├── packaging/ │ │ │ └── __init__.py │ │ ├── ply/ │ │ │ └── __init__.py │ │ ├── plyer/ │ │ │ └── __init__.py │ │ ├── pycparser/ │ │ │ └── __init__.py │ │ ├── pycryptodomex/ │ │ │ └── __init__.py │ │ ├── pyjnius/ │ │ │ └── __init__.py │ │ ├── pyparsing/ │ │ │ └── __init__.py │ │ ├── pyqt6/ │ │ │ └── __init__.py │ │ ├── pyqt6sip/ │ │ │ └── __init__.py │ │ ├── pyqt_builder/ │ │ │ └── __init__.py │ │ ├── python3/ │ │ │ └── __init__.py │ │ ├── qt6/ │ │ │ └── __init__.py │ │ ├── setuptools/ │ │ │ └── __init__.py │ │ ├── sip/ │ │ │ └── __init__.py │ │ ├── six/ │ │ │ └── __init__.py │ │ ├── sqlite3/ │ │ │ └── __init__.py │ │ ├── toml/ │ │ │ └── __init__.py │ │ ├── tomli/ │ │ │ └── __init__.py │ │ └── util.py │ ├── apparmor/ │ │ ├── README.md │ │ └── apparmor.d/ │ │ ├── abstractions/ │ │ │ └── electrum │ │ ├── electrum.appimage │ │ └── usr.local.bin.electrum │ ├── ban_unicode.py │ ├── build-linux/ │ │ ├── appimage/ │ │ │ ├── .dockerignore │ │ │ ├── Dockerfile │ │ │ ├── README.md │ │ │ ├── apprun.sh │ │ │ ├── apt.preferences │ │ │ ├── apt.sources.list │ │ │ ├── build.sh │ │ │ ├── make_appimage.sh │ │ │ ├── make_type2_runtime.sh │ │ │ └── patches/ │ │ │ ├── python-3.11-reproducible-buildinfo.diff │ │ │ └── type2-runtime-reproducible-build.patch │ │ └── sdist/ │ │ ├── .dockerignore │ │ ├── Dockerfile │ │ ├── README.md │ │ ├── build.sh │ │ └── make_sdist.sh │ ├── build-wine/ │ │ ├── .dockerignore │ │ ├── Dockerfile │ │ ├── README.md │ │ ├── README_windows.md │ │ ├── apt.preferences │ │ ├── apt.sources.list │ │ ├── build-electrum-git.sh │ │ ├── build.sh │ │ ├── electrum.nsi │ │ ├── gpg_keys/ │ │ │ └── 7ED10B6531D7C8E1BC296021FC624643487034E5.asc │ │ ├── make_win.sh │ │ ├── patches/ │ │ │ └── libiconv-fix-pointer-buf.patch │ │ ├── prepare-wine.sh │ │ ├── pyinstaller.spec │ │ ├── sign.sh │ │ └── unsign.sh │ ├── build_tools_util.sh │ ├── deterministic-build/ │ │ ├── README.md │ │ ├── check_submodules.sh │ │ ├── find_restricted_dependencies.py │ │ ├── requirements-binaries-mac.txt │ │ ├── requirements-binaries.txt │ │ ├── requirements-build-android.txt │ │ ├── requirements-build-appimage.txt │ │ ├── requirements-build-base.txt │ │ ├── requirements-build-mac.txt │ │ ├── requirements-build-wine.txt │ │ ├── requirements-hw.txt │ │ └── requirements.txt │ ├── docker_notes.md │ ├── freeze_containers_distro.sh │ ├── freeze_packages.sh │ ├── generate_payreqpb2.sh │ ├── locale/ │ │ ├── build_cleanlocale.sh │ │ ├── build_locale.sh │ │ ├── push_locale.py │ │ └── stats.py │ ├── make_download │ ├── make_libsecp256k1.sh │ ├── make_libusb.sh │ ├── make_packages.sh │ ├── make_plugin │ ├── make_zbar.sh │ ├── osx/ │ │ ├── README.md │ │ ├── README_macos.md │ │ ├── apply_sigs.sh │ │ ├── cdrkit-deterministic.patch │ │ ├── compare_dmg │ │ ├── entitlements.plist │ │ ├── extract_sigs.sh │ │ ├── make_osx.sh │ │ ├── notarize_app.sh │ │ ├── package.sh │ │ ├── pyinstaller.spec │ │ └── sign_osx.sh │ ├── print_electrum_version.py │ ├── release.sh │ ├── release_www.sh │ ├── requirements/ │ │ ├── requirements-binaries-mac.txt │ │ ├── requirements-binaries.txt │ │ ├── requirements-build-android.txt │ │ ├── requirements-build-appimage.txt │ │ ├── requirements-build-base.txt │ │ ├── requirements-build-mac.txt │ │ ├── requirements-build-wine.txt │ │ ├── requirements-ci.txt │ │ ├── requirements-hw.txt │ │ └── requirements.txt │ ├── sign_packages │ ├── trigger_deploy.sh │ ├── udev/ │ │ ├── 20-hw1.rules │ │ ├── 51-coinkite.rules │ │ ├── 51-hid-digitalbitbox.rules │ │ ├── 51-safe-t.rules │ │ ├── 51-trezor.rules │ │ ├── 51-usb-keepkey.rules │ │ ├── 52-hid-digitalbitbox.rules │ │ ├── 53-hid-bitbox02.rules │ │ ├── 54-hid-bitbox02.rules │ │ ├── 55-usb-jade.rules │ │ └── README.md │ └── upload.sh ├── electrum/ │ ├── __init__.py │ ├── _vendor/ │ │ ├── __init__.py │ │ ├── distutils/ │ │ │ ├── LICENSE │ │ │ ├── __init__.py │ │ │ └── version.py │ │ └── pyperclip/ │ │ ├── LICENSE.txt │ │ ├── README.md │ │ └── __init__.py │ ├── address_synchronizer.py │ ├── base_crash_reporter.py │ ├── bip21.py │ ├── bip32.py │ ├── bip39_recovery.py │ ├── bip39_wallet_formats.json │ ├── bitcoin.py │ ├── blockchain.py │ ├── chains/ │ │ ├── mainnet/ │ │ │ ├── checkpoints.json │ │ │ ├── fallback_lnnodes.json │ │ │ └── servers.json │ │ ├── mutinynet/ │ │ │ ├── fallback_lnnodes.json │ │ │ └── servers.json │ │ ├── regtest/ │ │ │ └── servers.json │ │ ├── signet/ │ │ │ ├── checkpoints.json │ │ │ ├── fallback_lnnodes.json │ │ │ └── servers.json │ │ ├── testnet/ │ │ │ ├── checkpoints.json │ │ │ ├── fallback_lnnodes.json │ │ │ └── servers.json │ │ └── testnet4/ │ │ ├── checkpoints.json │ │ └── servers.json │ ├── channel_db.py │ ├── coinchooser.py │ ├── commands.py │ ├── constants.py │ ├── contacts.py │ ├── crypto.py │ ├── currencies.json │ ├── daemon.py │ ├── descriptor.py │ ├── dns_hacks.py │ ├── dnssec.py │ ├── exchange_rate.py │ ├── fee_policy.py │ ├── gui/ │ │ ├── __init__.py │ │ ├── common_qt/ │ │ │ ├── __init__.py │ │ │ ├── i18n.py │ │ │ ├── plugins.py │ │ │ └── util.py │ │ ├── default_lang.py │ │ ├── fonts/ │ │ │ └── PTMono.LICENSE │ │ ├── icons/ │ │ │ ├── clock5.pdn │ │ │ └── electrum.icns │ │ ├── messages.py │ │ ├── qml/ │ │ │ ├── __init__.py │ │ │ ├── android_res/ │ │ │ │ └── layout/ │ │ │ │ └── scanner_layout.xml │ │ │ ├── auth.py │ │ │ ├── components/ │ │ │ │ ├── About.qml │ │ │ │ ├── AddressDetails.qml │ │ │ │ ├── Addresses.qml │ │ │ │ ├── BIP39RecoveryDialog.qml │ │ │ │ ├── BalanceDetails.qml │ │ │ │ ├── ChannelDetails.qml │ │ │ │ ├── ChannelOpenProgressDialog.qml │ │ │ │ ├── Channels.qml │ │ │ │ ├── CloseChannelDialog.qml │ │ │ │ ├── ConfirmTxDialog.qml │ │ │ │ ├── Constants.qml │ │ │ │ ├── CpfpBumpFeeDialog.qml │ │ │ │ ├── ExceptionDialog.qml │ │ │ │ ├── ExportTxDialog.qml │ │ │ │ ├── GenericShareDialog.qml │ │ │ │ ├── History.qml │ │ │ │ ├── ImportAddressesKeysDialog.qml │ │ │ │ ├── ImportChannelBackupDialog.qml │ │ │ │ ├── InvoiceDialog.qml │ │ │ │ ├── Invoices.qml │ │ │ │ ├── LightningPaymentDetails.qml │ │ │ │ ├── LnurlPayRequestDialog.qml │ │ │ │ ├── LnurlWithdrawRequestDialog.qml │ │ │ │ ├── LoadingWalletDialog.qml │ │ │ │ ├── MessageDialog.qml │ │ │ │ ├── NetworkOverview.qml │ │ │ │ ├── NewWalletWizard.qml │ │ │ │ ├── NostrConfigDialog.qml │ │ │ │ ├── NostrSwapServersDialog.qml │ │ │ │ ├── NotificationPopup.qml │ │ │ │ ├── OpenChannelDialog.qml │ │ │ │ ├── OpenWalletDialog.qml │ │ │ │ ├── OtpDialog.qml │ │ │ │ ├── PasswordDialog.qml │ │ │ │ ├── Preferences.qml │ │ │ │ ├── ProxyConfigDialog.qml │ │ │ │ ├── RbfBumpFeeDialog.qml │ │ │ │ ├── RbfCancelDialog.qml │ │ │ │ ├── ReceiveDetailsDialog.qml │ │ │ │ ├── ReceiveDialog.qml │ │ │ │ ├── ReceiveRequests.qml │ │ │ │ ├── ScanDialog.qml │ │ │ │ ├── SendDialog.qml │ │ │ │ ├── ServerConfigDialog.qml │ │ │ │ ├── ServerConnectWizard.qml │ │ │ │ ├── SignVerifyMessageDialog.qml │ │ │ │ ├── SwapDialog.qml │ │ │ │ ├── SweepDialog.qml │ │ │ │ ├── TermsOfUseWizard.qml │ │ │ │ ├── TxDetails.qml │ │ │ │ ├── WalletDetails.qml │ │ │ │ ├── WalletMainView.qml │ │ │ │ ├── WalletSummary.qml │ │ │ │ ├── Wallets.qml │ │ │ │ ├── controls/ │ │ │ │ │ ├── AddressDelegate.qml │ │ │ │ │ ├── BalanceSummary.qml │ │ │ │ │ ├── BtcField.qml │ │ │ │ │ ├── ButtonContainer.qml │ │ │ │ │ ├── ChannelBar.qml │ │ │ │ │ ├── ChannelDelegate.qml │ │ │ │ │ ├── CoinDelegate.qml │ │ │ │ │ ├── ElCheckBox.qml │ │ │ │ │ ├── ElComboBox.qml │ │ │ │ │ ├── ElDialog.qml │ │ │ │ │ ├── ElListView.qml │ │ │ │ │ ├── ElRadioButton.qml │ │ │ │ │ ├── ElTextArea.qml │ │ │ │ │ ├── FeeMethodComboBox.qml │ │ │ │ │ ├── FeePicker.qml │ │ │ │ │ ├── FiatField.qml │ │ │ │ │ ├── FlatButton.qml │ │ │ │ │ ├── FormattedAmount.qml │ │ │ │ │ ├── Heading.qml │ │ │ │ │ ├── HelpButton.qml │ │ │ │ │ ├── HelpDialog.qml │ │ │ │ │ ├── HistoryItemDelegate.qml │ │ │ │ │ ├── InfoBanner.qml │ │ │ │ │ ├── InfoTextArea.qml │ │ │ │ │ ├── InvoiceDelegate.qml │ │ │ │ │ ├── LightningNetworkStatusIndicator.qml │ │ │ │ │ ├── OnchainNetworkStatusIndicator.qml │ │ │ │ │ ├── PaneInsetBackground.qml │ │ │ │ │ ├── PasswordField.qml │ │ │ │ │ ├── PasswordStrengthIndicator.qml │ │ │ │ │ ├── Piechart.qml │ │ │ │ │ ├── PrefsHeading.qml │ │ │ │ │ ├── ProxyConfig.qml │ │ │ │ │ ├── QRImage.qml │ │ │ │ │ ├── QRScan.qml │ │ │ │ │ ├── RequestExpiryComboBox.qml │ │ │ │ │ ├── SeedKeyboard.qml │ │ │ │ │ ├── SeedKeyboardKey.qml │ │ │ │ │ ├── SeedTextArea.qml │ │ │ │ │ ├── ServerConfig.qml │ │ │ │ │ ├── ServerConnectModeComboBox.qml │ │ │ │ │ ├── ServerDelegate.qml │ │ │ │ │ ├── Tag.qml │ │ │ │ │ ├── TextHighlightPane.qml │ │ │ │ │ ├── Toaster.qml │ │ │ │ │ ├── ToggleLabel.qml │ │ │ │ │ ├── TxInput.qml │ │ │ │ │ └── TxOutput.qml │ │ │ │ ├── main.qml │ │ │ │ └── wizard/ │ │ │ │ ├── WCConfirmExt.qml │ │ │ │ ├── WCConfirmSeed.qml │ │ │ │ ├── WCCosignerKeystore.qml │ │ │ │ ├── WCCreateSeed.qml │ │ │ │ ├── WCEnterExt.qml │ │ │ │ ├── WCHaveMasterKey.qml │ │ │ │ ├── WCHaveSeed.qml │ │ │ │ ├── WCImport.qml │ │ │ │ ├── WCKeystoreType.qml │ │ │ │ ├── WCMultisig.qml │ │ │ │ ├── WCProxyConfig.qml │ │ │ │ ├── WCScriptAndDerivation.qml │ │ │ │ ├── WCServerConfig.qml │ │ │ │ ├── WCShowMasterPubkey.qml │ │ │ │ ├── WCTermsOfUseRequest.qml │ │ │ │ ├── WCWalletName.qml │ │ │ │ ├── WCWalletPassword.qml │ │ │ │ ├── WCWalletType.qml │ │ │ │ ├── WCWelcome.qml │ │ │ │ ├── Wizard.qml │ │ │ │ └── WizardComponent.qml │ │ │ ├── java_classes/ │ │ │ │ └── org/ │ │ │ │ └── electrum/ │ │ │ │ ├── biometry/ │ │ │ │ │ ├── BiometricActivity.java │ │ │ │ │ └── BiometricHelper.java │ │ │ │ └── qr/ │ │ │ │ └── SimpleScannerActivity.java │ │ │ ├── qeaddressdetails.py │ │ │ ├── qeaddresslistmodel.py │ │ │ ├── qeapp.py │ │ │ ├── qebiometrics.py │ │ │ ├── qebip39recovery.py │ │ │ ├── qebitcoin.py │ │ │ ├── qechanneldetails.py │ │ │ ├── qechannellistmodel.py │ │ │ ├── qechannelopener.py │ │ │ ├── qeconfig.py │ │ │ ├── qedaemon.py │ │ │ ├── qefx.py │ │ │ ├── qeinvoice.py │ │ │ ├── qeinvoicelistmodel.py │ │ │ ├── qelnpaymentdetails.py │ │ │ ├── qemodelfilter.py │ │ │ ├── qenetwork.py │ │ │ ├── qepiresolver.py │ │ │ ├── qeqr.py │ │ │ ├── qeqrscanner.py │ │ │ ├── qerequestdetails.py │ │ │ ├── qeserverlistmodel.py │ │ │ ├── qeswaphelper.py │ │ │ ├── qetransactionlistmodel.py │ │ │ ├── qetxdetails.py │ │ │ ├── qetxfinalizer.py │ │ │ ├── qetypes.py │ │ │ ├── qewallet.py │ │ │ ├── qewizard.py │ │ │ └── util.py │ │ ├── qt/ │ │ │ ├── __init__.py │ │ │ ├── address_dialog.py │ │ │ ├── address_list.py │ │ │ ├── amountedit.py │ │ │ ├── balance_dialog.py │ │ │ ├── bip39_recovery_dialog.py │ │ │ ├── channel_details.py │ │ │ ├── channels_list.py │ │ │ ├── completion_text_edit.py │ │ │ ├── confirm_tx_dialog.py │ │ │ ├── console.py │ │ │ ├── contact_list.py │ │ │ ├── custom_model.py │ │ │ ├── exception_window.py │ │ │ ├── fee_slider.py │ │ │ ├── history_list.py │ │ │ ├── invoice_list.py │ │ │ ├── lightning_dialog.py │ │ │ ├── lightning_tx_dialog.py │ │ │ ├── locktimeedit.py │ │ │ ├── main_window.py │ │ │ ├── my_treeview.py │ │ │ ├── network_dialog.py │ │ │ ├── new_channel_dialog.py │ │ │ ├── password_dialog.py │ │ │ ├── paytoedit.py │ │ │ ├── plugins_dialog.py │ │ │ ├── qrcodewidget.py │ │ │ ├── qrreader/ │ │ │ │ ├── __init__.py │ │ │ │ └── qtmultimedia/ │ │ │ │ ├── __init__.py │ │ │ │ ├── camera_dialog.py │ │ │ │ ├── crop_blur_effect.py │ │ │ │ ├── validator.py │ │ │ │ ├── video_overlay.py │ │ │ │ ├── video_surface.py │ │ │ │ └── video_widget.py │ │ │ ├── qrtextedit.py │ │ │ ├── qrwindow.py │ │ │ ├── rate_limiter.py │ │ │ ├── rbf_dialog.py │ │ │ ├── rebalance_dialog.py │ │ │ ├── receive_tab.py │ │ │ ├── request_list.py │ │ │ ├── seed_dialog.py │ │ │ ├── send_tab.py │ │ │ ├── settings_dialog.py │ │ │ ├── stylesheet_patcher.py │ │ │ ├── swap_dialog.py │ │ │ ├── transaction_dialog.py │ │ │ ├── update_checker.py │ │ │ ├── util.py │ │ │ ├── utxo_dialog.py │ │ │ ├── utxo_list.py │ │ │ ├── wallet_info_dialog.py │ │ │ └── wizard/ │ │ │ ├── __init__.py │ │ │ ├── server_connect.py │ │ │ ├── terms_of_use.py │ │ │ ├── wallet.py │ │ │ └── wizard.py │ │ ├── stdio.py │ │ └── text.py │ ├── harden_memory_linux.py │ ├── hw_wallet/ │ │ ├── __init__.py │ │ ├── cmdline.py │ │ ├── plugin.py │ │ ├── qt.py │ │ └── trezor_qt_pinmatrix.py │ ├── i18n.py │ ├── interface.py │ ├── invoices.py │ ├── json_db.py │ ├── keystore.py │ ├── lnaddr.py │ ├── lnchannel.py │ ├── lnhtlc.py │ ├── lnmsg.py │ ├── lnonion.py │ ├── lnpeer.py │ ├── lnrater.py │ ├── lnrouter.py │ ├── lnsweep.py │ ├── lntransport.py │ ├── lnurl.py │ ├── lnutil.py │ ├── lnverifier.py │ ├── lnwatcher.py │ ├── lnwire/ │ │ ├── README.md │ │ ├── onion_wire.csv │ │ └── peer_wire.csv │ ├── lnworker.py │ ├── logging.py │ ├── lrucache.py │ ├── mnemonic.py │ ├── mpp_split.py │ ├── network.py │ ├── old_mnemonic.py │ ├── onion_message.py │ ├── payment_identifier.py │ ├── paymentrequest.proto │ ├── paymentrequest.py │ ├── paymentrequest_pb2.py │ ├── pem.py │ ├── plot.py │ ├── plugin.py │ ├── plugins/ │ │ ├── README │ │ ├── __init__.py │ │ ├── audio_modem/ │ │ │ ├── __init__.py │ │ │ ├── manifest.json │ │ │ └── qt.py │ │ ├── bitbox02/ │ │ │ ├── __init__.py │ │ │ ├── bitbox02.py │ │ │ ├── manifest.json │ │ │ └── qt.py │ │ ├── coldcard/ │ │ │ ├── README.md │ │ │ ├── __init__.py │ │ │ ├── cmdline.py │ │ │ ├── coldcard.py │ │ │ ├── manifest.json │ │ │ └── qt.py │ │ ├── digitalbitbox/ │ │ │ ├── __init__.py │ │ │ ├── cmdline.py │ │ │ ├── digitalbitbox.py │ │ │ ├── manifest.json │ │ │ └── qt.py │ │ ├── jade/ │ │ │ ├── __init__.py │ │ │ ├── cmdline.py │ │ │ ├── jade.py │ │ │ ├── jadepy/ │ │ │ │ ├── README.md │ │ │ │ ├── __init__.py │ │ │ │ ├── jade.py │ │ │ │ ├── jade_error.py │ │ │ │ ├── jade_serial.py │ │ │ │ └── jade_tcp.py │ │ │ ├── manifest.json │ │ │ └── qt.py │ │ ├── keepkey/ │ │ │ ├── __init__.py │ │ │ ├── client.py │ │ │ ├── clientbase.py │ │ │ ├── cmdline.py │ │ │ ├── keepkey.py │ │ │ ├── manifest.json │ │ │ └── qt.py │ │ ├── labels/ │ │ │ ├── Labels.qml │ │ │ ├── __init__.py │ │ │ ├── cmdline.py │ │ │ ├── labels.py │ │ │ ├── manifest.json │ │ │ ├── qml.py │ │ │ └── qt.py │ │ ├── ledger/ │ │ │ ├── __init__.py │ │ │ ├── cmdline.py │ │ │ ├── ledger.py │ │ │ ├── manifest.json │ │ │ └── qt.py │ │ ├── nwc/ │ │ │ ├── __init__.py │ │ │ ├── cmdline.py │ │ │ ├── manifest.json │ │ │ ├── nwcserver.py │ │ │ └── qt.py │ │ ├── psbt_nostr/ │ │ │ ├── __init__.py │ │ │ ├── manifest.json │ │ │ ├── psbt_nostr.py │ │ │ ├── qml/ │ │ │ │ ├── PsbtReceiveDialog.qml │ │ │ │ └── main.qml │ │ │ ├── qml.py │ │ │ └── qt.py │ │ ├── revealer/ │ │ │ ├── LICENSE_DEJAVU.txt │ │ │ ├── SIL Open Font License.txt │ │ │ ├── SourceSans3-Bold.otf │ │ │ ├── __init__.py │ │ │ ├── hmac_drbg.py │ │ │ ├── manifest.json │ │ │ ├── qt.py │ │ │ └── revealer.py │ │ ├── safe_t/ │ │ │ ├── __init__.py │ │ │ ├── client.py │ │ │ ├── clientbase.py │ │ │ ├── cmdline.py │ │ │ ├── manifest.json │ │ │ ├── qt.py │ │ │ ├── safe_t.py │ │ │ └── transport.py │ │ ├── swapserver/ │ │ │ ├── __init__.py │ │ │ ├── cmdline.py │ │ │ ├── manifest.json │ │ │ ├── server.py │ │ │ └── swapserver.py │ │ ├── timelock_recovery/ │ │ │ ├── __init__.py │ │ │ ├── intro.txt │ │ │ ├── manifest.json │ │ │ ├── qt.py │ │ │ └── timelock_recovery.py │ │ ├── trezor/ │ │ │ ├── __init__.py │ │ │ ├── clientbase.py │ │ │ ├── cmdline.py │ │ │ ├── manifest.json │ │ │ ├── qt.py │ │ │ └── trezor.py │ │ ├── trustedcoin/ │ │ │ ├── __init__.py │ │ │ ├── cmdline.py │ │ │ ├── common_qt.py │ │ │ ├── manifest.json │ │ │ ├── qml/ │ │ │ │ ├── ChooseSeed.qml │ │ │ │ ├── Disclaimer.qml │ │ │ │ ├── KeepDisable.qml │ │ │ │ ├── ShowConfirmOTP.qml │ │ │ │ └── Terms.qml │ │ │ ├── qml.py │ │ │ ├── qt.py │ │ │ └── trustedcoin.py │ │ └── watchtower/ │ │ ├── __init__.py │ │ ├── cmdline.py │ │ ├── manifest.json │ │ ├── server.py │ │ └── watchtower.py │ ├── qrreader/ │ │ ├── __init__.py │ │ ├── abstract_base.py │ │ └── zbar.py │ ├── qrscanner.py │ ├── ripemd.py │ ├── rsakey.py │ ├── scripts/ │ │ ├── README.md │ │ ├── bip39_recovery.py │ │ ├── block_headers.py │ │ ├── bruteforce_pw.py │ │ ├── estimate_fee.py │ │ ├── get_history.py │ │ ├── ln_features.py │ │ ├── peers.py │ │ ├── quick_start.py │ │ ├── servers.py │ │ ├── txbroadcast.py │ │ ├── txradar.py │ │ ├── update_default_servers.py │ │ └── watch_address.py │ ├── segwit_addr.py │ ├── simple_config.py │ ├── slip39.py │ ├── sql_db.py │ ├── storage.py │ ├── submarine_swaps.py │ ├── synchronizer.py │ ├── trampoline.py │ ├── transaction.py │ ├── txbatcher.py │ ├── util.py │ ├── utils/ │ │ ├── __init__.py │ │ ├── memory_leak.py │ │ └── stacktracer.py │ ├── verifier.py │ ├── version.py │ ├── wallet.py │ ├── wallet_db.py │ ├── wizard.py │ ├── wordlist/ │ │ ├── chinese_simplified.txt │ │ ├── english.txt │ │ ├── japanese.txt │ │ ├── portuguese.txt │ │ ├── slip39.txt │ │ └── spanish.txt │ └── x509.py ├── electrum-env ├── electrum.desktop ├── fastlane/ │ └── metadata/ │ └── android/ │ └── en-US/ │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt ├── org.electrum.electrum.metainfo.xml ├── pubkeys/ │ ├── Animazing.asc │ ├── Emzy.asc │ ├── ThomasV.asc │ ├── bauerj.asc │ ├── felixb_f321x.asc │ ├── kyuupichan.asc │ ├── sombernight.asc │ ├── sombernight_releasekey.asc │ └── wozz.asc ├── run_electrum ├── setup.cfg ├── setup.py └── tests/ ├── __init__.py ├── anchor-vectors.json ├── bip-0341/ │ └── wallet-test-vectors.json ├── blinded-onion-message-onion-test.json ├── cause_carbon_wallet.json ├── fiat_fx_data/ │ └── BitFinex_EUR ├── plugins/ │ ├── __init__.py │ ├── test_revealer.py │ ├── test_timelock_recovery/ │ │ └── default_wallet │ └── test_timelock_recovery.py ├── qml/ │ ├── __init__.py │ ├── qt_util.py │ ├── test_qml_qeconfig.py │ ├── test_qml_qetransactionlistmodel.py │ └── test_qml_types.py ├── regtest/ │ ├── regtest.sh │ ├── run_bitcoind.sh │ └── run_electrumx.sh ├── regtest.py ├── slip39-vectors.json ├── test_bitcoin.py ├── test_blockchain.py ├── test_bolt11.py ├── test_callbackmgr.py ├── test_coinchooser.py ├── test_commands.py ├── test_contacts.py ├── test_daemon.py ├── test_descriptor.py ├── test_fee_policy.py ├── test_history_export/ │ ├── history_no_fx_client_4_5_2_9dk_with_ln.csv │ ├── history_no_fx_client_4_5_2_9dk_with_ln.json │ ├── history_with_fx_client_4_5_2_9dk_with_ln.csv │ └── history_with_fx_client_4_5_2_9dk_with_ln.json ├── test_i18n.py ├── test_interface.py ├── test_invoices.py ├── test_jsondb.py ├── test_lnchannel.py ├── test_lnhtlc.py ├── test_lnmsg.py ├── test_lnpeer.py ├── test_lnpeermgr.py ├── test_lnrouter.py ├── test_lntransport.py ├── test_lnurl.py ├── test_lnutil.py ├── test_lnwallet.py ├── test_mnemonic.py ├── test_mpp_split.py ├── test_network.py ├── test_onion_message.py ├── test_payment_identifier.py ├── test_psbt.py ├── test_simple_config.py ├── test_storage_upgrade/ │ ├── client_1_9_8_seeded │ ├── client_2_0_4_importedkeys │ ├── client_2_0_4_multisig │ ├── client_2_0_4_seeded │ ├── client_2_0_4_trezor_multiacc │ ├── client_2_0_4_trezor_singleacc │ ├── client_2_0_4_watchaddresses │ ├── client_2_1_1_importedkeys │ ├── client_2_1_1_multisig │ ├── client_2_1_1_seeded │ ├── client_2_1_1_trezor_multiacc │ ├── client_2_1_1_trezor_singleacc │ ├── client_2_1_1_watchaddresses │ ├── client_2_2_0_importedkeys │ ├── client_2_2_0_multisig │ ├── client_2_2_0_seeded │ ├── client_2_2_0_trezor_multiacc │ ├── client_2_2_0_trezor_singleacc │ ├── client_2_2_0_watchaddresses │ ├── client_2_3_2_importedkeys │ ├── client_2_3_2_multisig │ ├── client_2_3_2_seeded │ ├── client_2_3_2_trezor_multiacc │ ├── client_2_3_2_trezor_singleacc │ ├── client_2_3_2_watchaddresses │ ├── client_2_4_3_importedkeys │ ├── client_2_4_3_multisig │ ├── client_2_4_3_seeded │ ├── client_2_4_3_trezor_multiacc │ ├── client_2_4_3_trezor_singleacc │ ├── client_2_4_3_watchaddresses │ ├── client_2_5_4_importedkeys │ ├── client_2_5_4_multisig │ ├── client_2_5_4_seeded │ ├── client_2_5_4_trezor_multiacc │ ├── client_2_5_4_trezor_singleacc │ ├── client_2_5_4_watchaddresses │ ├── client_2_6_4_importedkeys │ ├── client_2_6_4_multisig │ ├── client_2_6_4_seeded │ ├── client_2_6_4_watchaddresses │ ├── client_2_7_18_importedkeys │ ├── client_2_7_18_multisig │ ├── client_2_7_18_seeded │ ├── client_2_7_18_trezor_singleacc │ ├── client_2_7_18_watchaddresses │ ├── client_2_8_3_importedkeys │ ├── client_2_8_3_importedkeys_flawed_previous_upgrade_from_2_7_18 │ ├── client_2_8_3_multisig │ ├── client_2_8_3_seeded │ ├── client_2_8_3_trezor_singleacc │ ├── client_2_8_3_watchaddresses │ ├── client_2_9_3_importedkeys │ ├── client_2_9_3_importedkeys_keystore_changes │ ├── client_2_9_3_multisig │ ├── client_2_9_3_old_seeded_with_realistic_history │ ├── client_2_9_3_seeded │ ├── client_2_9_3_trezor_singleacc │ ├── client_2_9_3_watchaddresses │ ├── client_3_2_3_ledger_standard_keystore_changes │ ├── client_3_3_8_xpub_with_realistic_history │ └── client_4_5_2_9dk_with_ln ├── test_storage_upgrade.py ├── test_transaction.py ├── test_txbatcher.py ├── test_util.py ├── test_verifier.py ├── test_wallet.py ├── test_wallet_vertical.py ├── test_wizard.py └── test_x509.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .cirrus.yml ================================================ # unittests using the 'latest' runtime python-dependencies task: container: image: $ELECTRUM_IMAGE cpu: 1 memory: 2G matrix: - name: "unittests: py$ELECTRUM_PYTHON_VERSION" env: ELECTRUM_IMAGE: python:$ELECTRUM_PYTHON_VERSION matrix: - env: ELECTRUM_PYTHON_VERSION: 3.10 - env: ELECTRUM_PYTHON_VERSION: 3.11 - env: ELECTRUM_PYTHON_VERSION: 3.12 - env: ELECTRUM_PYTHON_VERSION: 3.13 - env: ELECTRUM_PYTHON_VERSION: 3.14 - name: "unittests: py3.14, debug-mode" env: ELECTRUM_PYTHON_VERSION: 3.14 # enable additional checks: PYTHONASYNCIODEBUG: "1" PYTHONDEVMODE: "1" pip_cache: folder: ~/.cache/pip fingerprint_script: echo $ELECTRUM_IMAGE && cat $ELECTRUM_REQUIREMENTS_CI && cat $ELECTRUM_REQUIREMENTS tag_script: - git tag libsecp_build_cache: folder: contrib/_saved_secp256k1_build fingerprint_script: sha256sum ./contrib/make_libsecp256k1.sh populate_script: - apt-get update - apt-get -y install automake libtool - ./contrib/make_libsecp256k1.sh - mkdir contrib/_saved_secp256k1_build - cp electrum/libsecp256k1.so.* contrib/_saved_secp256k1_build/ install_script: - apt-get update # qml test reqs: - apt-get -y install libgl1 libegl1 libxkbcommon0 libdbus-1-3 - pip install -r $ELECTRUM_REQUIREMENTS_CI # electrum itself: - export ELECTRUM_ECC_DONT_COMPILE=1 - pip install ".[tests,qml_gui]" version_script: - python3 --version - pip freeze --all pytest_script: - > coverage run --source=electrum \ "--omit=electrum/gui/*,electrum/plugins/*,electrum/scripts/*" \ -m pytest tests -v - coverage report coveralls_script: - if [ ! -z "$COVERALLS_REPO_TOKEN" ] && [ "$ELECTRUM_PYTHON_VERSION" = "3.10" ] ; then coveralls ; fi env: LD_LIBRARY_PATH: contrib/_saved_secp256k1_build/ ELECTRUM_REQUIREMENTS_CI: contrib/requirements/requirements-ci.txt ELECTRUM_REQUIREMENTS: contrib/requirements/requirements.txt # following CI_* env vars are set up for coveralls CI_NAME: "CirrusCI" CI_BUILD_NUMBER: $CIRRUS_BUILD_ID CI_JOB_ID: $CIRRUS_TASK_ID CI_BUILD_URL: "https://cirrus-ci.com/task/$CIRRUS_TASK_ID" CI_BRANCH: $CIRRUS_BRANCH CI_PULL_REQUEST: $CIRRUS_PR # in addition, COVERALLS_REPO_TOKEN is set as an "override" in https://cirrus-ci.com/settings/... depends_on: - "linter: Flake8 Mandatory" # unittests using the ~same frozen dependencies that are used in the released binaries # note: not using pinned pyqt here, due to "qml_gui" extra task: container: image: $ELECTRUM_IMAGE cpu: 1 memory: 2G name: "unittests: py3.10, frozen-deps" pip_cache: folder: ~/.cache/pip fingerprint_script: echo $ELECTRUM_IMAGE && cat contrib/requirements/requirements*.txt && cat contrib/deterministic-build/requirements*.txt tag_script: - git tag libsecp_build_cache: folder: contrib/_saved_secp256k1_build fingerprint_script: sha256sum ./contrib/make_libsecp256k1.sh populate_script: - apt-get update - apt-get -y install automake libtool - ./contrib/make_libsecp256k1.sh - mkdir contrib/_saved_secp256k1_build - cp electrum/libsecp256k1.so.* contrib/_saved_secp256k1_build/ install_script: - apt-get update # qml test reqs: - apt-get -y install libgl1 libegl1 libxkbcommon0 libdbus-1-3 - pip install -r contrib/deterministic-build/requirements-build-base.txt - pip install -r contrib/requirements/requirements-ci.txt # electrum itself: - export ELECTRUM_ECC_DONT_COMPILE=1 - pip install -r contrib/deterministic-build/requirements.txt -r contrib/deterministic-build/requirements-binaries.txt - pip install ".[tests,qml_gui]" version_script: - python3 --version - pip freeze --all pytest_script: - pytest tests -v env: ELECTRUM_IMAGE: python:3.10 LD_LIBRARY_PATH: contrib/_saved_secp256k1_build/ depends_on: - "linter: Flake8 Mandatory" task: name: "locale: upload to crowdin" container: image: $ELECTRUM_IMAGE cpu: 1 memory: 1G pip_cache: folder: ~/.cache/pip fingerprint_script: echo Locale && echo $ELECTRUM_IMAGE && cat $ELECTRUM_REQUIREMENTS_CI install_script: - apt-get update - apt-get -y install gettext qt6-l10n-tools - pip install -r $ELECTRUM_REQUIREMENTS_CI - pip install requests submodules_script: - git submodule update --init locale_script: - contrib/locale/push_locale.py env: ELECTRUM_IMAGE: python:3.10 ELECTRUM_REQUIREMENTS_CI: contrib/requirements/requirements-ci.txt # in addition, crowdin_api_key is set as an "override" in https://cirrus-ci.com/settings/... # - api key is for crowdin account: "SomberNight_CI_BOT" # - see https://crowdin.com/settings#api-key depends_on: - "unittests: py3.10" only_if: $CIRRUS_BRANCH == 'master' task: name: "Regtest functional tests" compute_engine_instance: image_project: cirrus-images image: family/docker-builder platform: linux cpu: 1 memory: 1G pip_cache: folder: ~/.cache/pip fingerprint_script: echo Regtest && echo docker_builder && cat $ELECTRUM_REQUIREMENTS bitcoind_cache: folder: /tmp/bitcoind populate_script: mkdir -p /tmp/bitcoind install_script: - apt-get update - apt-get -y install curl jq bc - python3 -m pip install --user --upgrade pip # install electrum - export ELECTRUM_ECC_DONT_COMPILE=1 # we build manually to make caching it easier - python3 -m pip install .[tests] --ignore-installed # ignore installed system installed attrs # install e-x some commits after 1.18.0 tag - python3 -m pip install git+https://github.com/spesmilo/electrumx.git@0b260d4345242cc41e316e97d7de10ae472fd172 - "BITCOIND_VERSION=$(curl https://bitcoincore.org/en/download/ | grep -E -i --only-matching 'Latest version: [0-9\\.]+' | grep -E --only-matching '[0-9\\.]+')" - BITCOIND_FILENAME=bitcoin-$BITCOIND_VERSION-x86_64-linux-gnu.tar.gz - BITCOIND_PATH=/tmp/bitcoind/$BITCOIND_FILENAME - BITCOIND_URL=https://bitcoincore.org/bin/bitcoin-core-$BITCOIND_VERSION/$BITCOIND_FILENAME - tar -xaf $BITCOIND_PATH || (rm -f /tmp/bitcoind/* && curl --output $BITCOIND_PATH $BITCOIND_URL && tar -xaf $BITCOIND_PATH) - cp -a bitcoin-$BITCOIND_VERSION/* /usr/ libsecp_build_cache: folder: contrib/_saved_secp256k1_build fingerprint_script: sha256sum ./contrib/make_libsecp256k1.sh populate_script: - apt-get -y install automake libtool - ./contrib/make_libsecp256k1.sh - mkdir contrib/_saved_secp256k1_build - cp electrum/libsecp256k1.so.* contrib/_saved_secp256k1_build/ bitcoind_service_background_script: - tests/regtest/run_bitcoind.sh electrumx_service_background_script: - tests/regtest/run_electrumx.sh # if any test fails, the test will get aborted (--failfast) and the wallet directories will be # available for download in the Cirrus UI regtest_script: - sleep 10s - python3 -m unittest tests/regtest.py --failfast || TEST_EXIT_CODE=$? - tar -czf test_wallets.tar.gz /tmp/alice /tmp/bob /tmp/carol || true - exit ${TEST_EXIT_CODE:-0} on_failure: wallet_artifacts: path: "test_wallets.tar.gz" env: LD_LIBRARY_PATH: contrib/_saved_secp256k1_build/ ELECTRUM_REQUIREMENTS: contrib/requirements/requirements.txt PIP_BREAK_SYSTEM_PACKAGES: 1 # ElectrumX exits with an error without this: ALLOW_ROOT: 1 depends_on: - "linter: Flake8 Mandatory" task: container: image: $ELECTRUM_IMAGE cpu: 1 memory: 1G pip_cache: folder: ~/.cache/pip fingerprint_script: echo Flake8 && echo $ELECTRUM_IMAGE && cat $ELECTRUM_REQUIREMENTS install_script: - pip install "flake8==7.3.0" "flake8-bugbear==25.10.21" flake8_script: - flake8 . --count --select="$ELECTRUM_LINTERS" --ignore="$ELECTRUM_LINTERS_IGNORE" --show-source --statistics --exclude "*_pb2.py,electrum/_vendor/" env: ELECTRUM_IMAGE: python:3.10 ELECTRUM_REQUIREMENTS: contrib/requirements/requirements.txt matrix: - name: "linter: Flake8 Mandatory" env: # list of error codes: # - https://flake8.pycqa.org/en/latest/user/error-codes.html # - https://pycodestyle.pycqa.org/en/latest/intro.html#error-codes # - https://github.com/PyCQA/flake8-bugbear/tree/8c0e7eb04217494d48d0ab093bf5b31db0921989#list-of-warnings ELECTRUM_LINTERS: E9,E101,E129,E273,E274,E703,E71,E722,F5,F6,F7,F8,W191,W29,B,B909 ELECTRUM_LINTERS_IGNORE: B007,B009,B010,B036,B042,F541,F841 - name: "linter: Flake8 Non-Mandatory" env: ELECTRUM_LINTERS: E,F,W,C90,B ELECTRUM_LINTERS_IGNORE: "" allow_failures: true task: name: "linter: ban unicode" container: image: python:3.10 cpu: 1 memory: 1G main_script: - contrib/ban_unicode.py # Cron jobs configured in https://cirrus-ci.com/settings/... # - job "nightly" on branch "master" at "0 30 2 * * ?" (every day at 02:30Z) task: name: "build: Windows" matrix: - trigger_type: manual only_if: $CIRRUS_CRON == "" - trigger_type: automatic only_if: $CIRRUS_CRON == "nightly" container: dockerfile: contrib/build-wine/Dockerfile cpu: 1 memory: 3G pip_cache: folders: - contrib/build-wine/.cache/win*/wine_pip_cache fingerprint_script: - echo $CIRRUS_TASK_NAME - git ls-files -s contrib/deterministic-build/*.txt - git ls-files -s contrib/build-wine/ build2_cache: folders: - contrib/build-wine/.cache/win*/build fingerprint_script: - echo $CIRRUS_TASK_NAME - cat contrib/make_libsecp256k1.sh | sha256sum - cat contrib/make_libusb.sh | sha256sum - cat contrib/make_zbar.sh | sha256sum - git ls-files -s contrib/build-wine/ build_script: - cd contrib/build-wine - ./make_win.sh binaries_artifacts: path: "contrib/build-wine/dist/*" env: CIRRUS_WORKING_DIR: /opt/wine64/drive_c/electrum CIRRUS_DOCKER_CONTEXT: contrib/build-wine depends_on: - "unittests: py3.10" task: name: "build: Android (QML $APK_ARCH)" matrix: - trigger_type: manual only_if: $CIRRUS_CRON == "" - trigger_type: automatic only_if: $CIRRUS_CRON == "nightly" timeout_in: 90m container: dockerfile: contrib/android/Dockerfile cpu: 8 memory: 24G env: APK_ARCH: arm64-v8a packages_tld_folder_cache: folder: packages fingerprint_script: - echo $CIRRUS_TASK_NAME && cat contrib/deterministic-build/requirements.txt && cat contrib/make_packages.sh - git ls-files -s contrib/android/ p4a_cache: folders: - ".buildozer/android/platform/build-$APK_ARCH/packages" - ".buildozer/android/platform/build-$APK_ARCH/build" fingerprint_script: # note: should *at least* depend on Dockerfile and p4a_recipes/, but contrib/android/ is simplest - git ls-files -s contrib/android/ - echo "qml $APK_ARCH" build_script: - ./contrib/android/make_apk.sh qml "$APK_ARCH" debug binaries_artifacts: path: "dist/*" depends_on: - "unittests: py3.10" ## mac build disabled, as Cirrus CI no longer supports Intel-based mac builds #task: # name: "build: macOS" # macos_instance: # image: catalina-xcode-11.3.1 # env: # TARGET_OS: macOS # pip_cache: # folder: ~/Library/Caches/pip # fingerprint_script: # - echo $CIRRUS_TASK_NAME # - git ls-files -s contrib/deterministic-build/*.txt # - git ls-files -s contrib/osx/ # build2_cache: # folder: contrib/osx/.cache # fingerprint_script: # - echo $CIRRUS_TASK_NAME # - cat contrib/make_libsecp256k1.sh | shasum -a 256 # - cat contrib/make_libusb.sh | shasum -a 256 # - cat contrib/make_zbar.sh | shasum -a 256 # - git ls-files -s contrib/osx/ # install_script: # - git fetch --all --tags # build_script: # - ./contrib/osx/make_osx.sh # sum_script: # - ls -lah dist # - shasum -a 256 dist/*.dmg # binaries_artifacts: # path: "dist/*" task: name: "build: AppImage" matrix: - trigger_type: manual only_if: $CIRRUS_CRON == "" - trigger_type: automatic only_if: $CIRRUS_CRON == "nightly" compute_engine_instance: image_project: cirrus-images image: family/docker-builder platform: linux cpu: 2 memory: 2G pip_cache: folder: contrib/build-linux/appimage/.cache/pip_cache fingerprint_script: - echo $CIRRUS_TASK_NAME - git ls-files -s contrib/deterministic-build/*.txt - git ls-files -s contrib/build-linux/appimage/ build2_cache: folder: contrib/build-linux/appimage/.cache/appimage fingerprint_script: - echo $CIRRUS_TASK_NAME - cat contrib/make_libsecp256k1.sh | sha256sum - git ls-files -s contrib/build-linux/appimage/ build_script: - ./contrib/build-linux/appimage/build.sh binaries_artifacts: path: "dist/*" depends_on: - "unittests: py3.10" task: container: dockerfile: contrib/build-linux/sdist/Dockerfile cpu: 1 memory: 1G pip_cache: folder: ~/.cache/pip fingerprint_script: - echo $CIRRUS_TASK_NAME - git ls-files -s contrib/deterministic-build/*.txt - git ls-files -s contrib/build-linux/sdist/ build_script: - ./contrib/build-linux/sdist/make_sdist.sh binaries_artifacts: path: "dist/*" matrix: - name: "build: tarball" - name: "build: source-only tarball" env: OMIT_UNCLEAN_FILES: 1 depends_on: - "unittests: py3.10" task: name: "check submodules" container: image: python:3.10 cpu: 1 memory: 1G fetch_script: - git fetch --all --tags check_script: - ./contrib/deterministic-build/check_submodules.sh only_if: $CIRRUS_TAG != '' ================================================ FILE: .editorconfig ================================================ # see https://EditorConfig.org root = true [*] indent_style = space trim_trailing_whitespace = true end_of_line = lf charset = utf-8 [*.py] indent_size = 4 insert_final_newline = true [*.sh] indent_size = 4 insert_final_newline = true ================================================ FILE: .gitattributes ================================================ # Auto detect text files and perform end-of-line normalization (to LF) * text=auto # These Windows files should have CRLF line endings in checkout *.bat text eol=crlf *.ps1 text eol=crlf # Never perform LF normalization on these files *.ico binary *.jar binary *.png binary *.zip binary ================================================ FILE: .github/ISSUE_TEMPLATE/01_issue.yml ================================================ name: Issue description: Submit a new issue. #labels: [bug] body: - type: markdown attributes: value: | ## Read this first! * This issue tracker is for bug reports and development, not general questions. * There is no private support! Scammers will reply to you here and tell you to go to their external website or contact them privately in email/telegram/whatsapp/etc. They will try to **steal** your coins! Be extremely suspicious of anyone offering help in a non-public way. Scammers will want to chat with you in private as then other people will not get a chance to point out the scam. * Do not post issues about non-**Bitcoin** versions of Electrum. ---- #- type: checkboxes # attributes: # label: Is there an existing issue for this already? # #description: Please search to see if this issue is already being tracked. # options: # - label: I have searched the existing issues # required: true - type: textarea id: main-text-body attributes: label: Description #description: Tell us what went wrong placeholder: Please search existing issues for duplicates. validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: # - name: Electrum Security Policy # url: https://github.com/spesmilo/electrum/blob/master/SECURITY.md # about: View security policy - name: Community Forum url: https://bitcointalk.org/index.php?board=98.0 about: Ask non-development-related questions here ================================================ FILE: .gitignore ================================================ .git/ ####-*.patch **/*.pyc *.swp build/ dist/ *.egg/ Electrum.egg-info/ .devlocaltmp/ *_trial_temp packages env/ /.venv*/ .buildozer .buildozer_*/ bin/ .idea .mypy_cache .vscode electrum_data .DS_Store contrib/trigger_website contrib/trigger_binaries # tests/tox .tox/ .cache/ .coverage .pytest_cache # build workspaces contrib/build-wine/tmp/ contrib/build-wine/build/ contrib/build-wine/.cache/ contrib/build-wine/dist/ contrib/build-wine/signed/ contrib/build-linux/appimage/build/ contrib/build-linux/appimage/.cache/ contrib/osx/.cache/ contrib/osx/build-venv/ contrib/android/android_debug.keystore contrib/android/.cache/ contrib/secp256k1/ contrib/zbar/ contrib/libusb/ contrib/.venv_make_packages/ # shared objects electrum/*.so electrum/*.so.* electrum/*.dll electrum/*.dylib contrib/osx/*.dylib ================================================ FILE: .gitmodules ================================================ [submodule "electrum/locale"] path = electrum/locale url = https://github.com/spesmilo/electrum-locale ignore = dirty [submodule "electrum/plugins/keepkey/keepkeylib"] path = electrum/plugins/keepkey/keepkeylib url = https://github.com/spesmilo/electrum-keepkeylib.git ================================================ FILE: AUTHORS ================================================ ThomasV - Creator and maintainer. Animazing / Tachikoma - Styled the new GUI. Mac version. Azelphur - GUI stuff. Coblee - Alternate coin support and py2app support. Deafboy - Ubuntu packages. Soren Stoutner - Debian packages and some Qt GUI layout. EagleTM - Bugfixes. ErebusBat - Mac distribution. Genjix - Porting pro-mode functionality to lite-gui and worked on server Slush - Work on the server. Designed the original Stratum spec. Julian Toash (Tuxavant) - Various fixes to the client. rdymac - Website and translations. kyuupichan - Miscellaneous. ================================================ FILE: LICENCE ================================================ The MIT License (MIT) Copyright (c) 2011-2024 The Electrum developers Copyright (c) 2011-2024 Thomas Voegtlin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: MANIFEST.in ================================================ include LICENCE RELEASE-NOTES AUTHORS include README.md include electrum.desktop include *.py include run_electrum include org.electrum.electrum.metainfo.xml recursive-include packages *.py recursive-include packages cacert.pem include contrib/requirements/requirements*.txt include contrib/deterministic-build/requirements*.txt include contrib/*.sh graft electrum graft tests graft contrib/udev exclude electrum/*.so exclude electrum/*.so.0 exclude electrum/*.dll exclude electrum/*.dylib global-exclude __pycache__ global-exclude *.py[co~] global-exclude *.py.orig global-exclude *.py.rej global-exclude .git # We include both source (.po) and compiled (.mo) locale files (if present). # When building the "sourceonly" tar.gz, the build script explicitly deletes the compiled files. # exclude electrum/locale/locale/*/LC_MESSAGES/electrum.mo ================================================ FILE: README.md ================================================ # Electrum - Lightweight Bitcoin client ``` Licence: MIT Licence Author: Thomas Voegtlin Language: Python (>= 3.10) Homepage: https://electrum.org/ ``` [![Build Status](https://api.cirrus-ci.com/github/spesmilo/electrum.svg?branch=master)](https://cirrus-ci.com/github/spesmilo/electrum) [![Test coverage statistics](https://coveralls.io/repos/github/spesmilo/electrum/badge.svg?branch=master)](https://coveralls.io/github/spesmilo/electrum?branch=master) [![Help translate Electrum online](https://d322cqt584bo4o.cloudfront.net/electrum/localized.svg)](https://crowdin.com/project/electrum) ## Getting started _(If you've come here looking to simply run Electrum, [you may download it here](https://electrum.org/#download).)_ Electrum itself is pure Python, and so are most of the required dependencies, but not everything. The following sections describe how to run from source, but here is a TL;DR: ``` $ sudo apt-get install libsecp256k1-dev $ ELECTRUM_ECC_DONT_COMPILE=1 python3 -m pip install --user ".[gui,crypto]" ``` ### Not pure-python dependencies #### Qt GUI If you want to use the Qt interface, install the Qt dependencies: ``` $ sudo apt-get install python3-pyqt6 ``` #### libsecp256k1 For elliptic curve operations, [libsecp256k1](https://github.com/bitcoin-core/secp256k1) is a required dependency. If you "pip install" Electrum, by default libsecp will get compiled locally, as part of the `electrum-ecc` dependency. This can be opted-out of, by setting the `ELECTRUM_ECC_DONT_COMPILE=1` environment variable. For the compilation to work, besides a C compiler, you need at least: ``` $ sudo apt-get install automake libtool ``` If you opt out of the compilation, you need to provide libsecp in another way, e.g.: ``` $ sudo apt-get install libsecp256k1-dev ``` #### cryptography Due to the need for fast symmetric ciphers, [cryptography](https://github.com/pyca/cryptography) is required. Install from your package manager (or from pip): ``` $ sudo apt-get install python3-cryptography ``` #### hardware-wallet support If you would like hardware wallet support, [see this](https://github.com/spesmilo/electrum-docs/blob/master/hardware-linux.rst). ### Running from tar.gz If you downloaded the official package (tar.gz), you can run Electrum from its root directory without installing it on your system; all the pure python dependencies are included in the 'packages' directory. To run Electrum from its root directory, just do: ``` $ ./run_electrum ``` You can also install Electrum on your system, by running this command: ``` $ sudo apt-get install python3-setuptools python3-pip $ python3 -m pip install --user . ``` This will download and install the Python dependencies used by Electrum instead of using the 'packages' directory. It will also place an executable named `electrum` in `~/.local/bin`, so make sure that is on your `PATH` variable. ### Development version (git clone) _(For OS-specific instructions, see [here for Windows](contrib/build-wine/README_windows.md), and [for macOS](contrib/osx/README_macos.md))_ Check out the code from GitHub: ``` $ git clone https://github.com/spesmilo/electrum.git $ cd electrum $ git submodule update --init ``` Run install (this should install dependencies): ``` $ python3 -m pip install --user -e . ``` Create translations (optional): ``` $ sudo apt-get install gettext $ ./contrib/locale/build_locale.sh electrum/locale/locale electrum/locale/locale ``` Finally, to start Electrum: ``` $ ./run_electrum ``` ### Run tests Run unit tests with `pytest`: ``` $ pytest tests -v ``` (can be parallelized with `-n auto` option, using [`pytest-xdist`](https://github.com/pytest-dev/pytest-xdist) plugin) To run a single file, specify it directly like this: ``` $ pytest tests/test_bitcoin.py -v ``` ## Creating Binaries - [Linux (tarball)](contrib/build-linux/sdist/README.md) - [Linux (AppImage)](contrib/build-linux/appimage/README.md) - [macOS](contrib/osx/README.md) - [Windows](contrib/build-wine/README.md) - [Android](contrib/android/Readme.md) ## Contributing Any help testing the software, reporting or fixing bugs, reviewing pull requests and recent changes, writing tests, or helping with outstanding issues is very welcome. Implementing new features, or improving/refactoring the codebase, is of course also welcome, but to avoid wasted effort, especially for larger changes, we encourage discussing these on the issue tracker or IRC first. Besides [GitHub](https://github.com/spesmilo/electrum), most communication about Electrum development happens on IRC, in the `#electrum` channel on Libera Chat. The easiest way to participate on IRC is with the web client, [web.libera.chat](https://web.libera.chat/#electrum). Please improve translations on [Crowdin](https://crowdin.com/project/electrum). ================================================ FILE: RELEASE-NOTES ================================================ # Release 4.7.1 (Feb 26, 2026) * Qt GUI (desktop): - new: changelog website accessible from "Help" toolbar menu (#10433) - new: show translation completion percentage in language names (#10479) - new: allow changing font size in console (#10494) - changed: validate Electrum server address input with UI feedback (#10441) - changed: stop showing anchor icon for lightning channels with anchor outputs (3979d70) - fix: broken addresses tab for imported watch-only wallets (#10436) * QML GUI & Android: - new: show translation completion percentage in language names (#10479) - changed: validate Electrum server address input with UI feedback (#10441) - fix: handle Java import error causing startup crash on Android 7 and 8 devices (#10484) * Onchain / Wallet: - fix: improved fee estimation for replacement transactions (#10453) * Database: - fix: handle upgrade failure for users with pending Lightning HTLCs (#10489) * Lightning: - changed: send channel_update alongside node_announcement gossip messages (#10475) - fix: improved safety when revealing preimage on-chain (#10442) - fix: don't attempt to fetch gossip from Tor peers without a proxy enabled (#10448) - fix: handle peer sending back our own channel_update (#10493) * Dependencies: - changed: bump electrum-ecc (and libsecp256k1) from 0.7.0 to 0.7.1 (#10495) * Builds/binaries: - Android: - changed: bump docker base image to Debian 13 (#10452) * Contrib: - changed: translation: stop sorting source strings (6c1e085) - changed: freeze_packages.py: use stdlib "venv" instead of 3rd party virtualenv (4f7b6e8) - fix: add_cosigner.py: compatibility with Python 3.13 (b495ee7) * Plugins: - new: hook 'qt_utxo_menu' for Qt GUI UTXO list (cfe2a57) * Hardware wallets: - new: support Ledger Nano Gen 5 (#10457) * Translations: Call for Proofreaders and Translators. - Localisation of the UI has always been a community effort. Recently we found several examples of vandalism and malicious behaviour among the translated strings, including multiple bitcoin addresses injected into UI strings. One user sent funds to one such address and hence lost money. (see spesmilo/electrum-locale#46) - We added some automated safeguards to try and prevent this in the future, including basic regexes and an LLM proofreader. We also made the ongoing git diffs for updating the frozen translations much smaller to make it realistic to ~review. (see spesmilo/electrum-locale#47, #49, #51) - However, the best solution would be per-language human review. If we had 1-3 proofreaders per language on Crowdin, we could restrict the set of translated strings that gets included in the binaries to the "proofreader-approved" strings. We ask interested people to start contributing and apply to be proofreaders. To get proofreader permissions, send us an email or come to irc, with your crowdin username and some proof of work (such as activity on crowdin in our or another project, contributions to open-source, having an established identity on github/bitcointalk/stackoverflow/..., etc -- just prove being a human and being well-intentioned). (see https://github.com/spesmilo/electrum-locale/issues/47#issuecomment-3914866337) # Release 4.7.0 (Jan 22, 2026) * Qt GUI (desktop): - new: "Submarine Payments": support reverse swaps to external address (#10303) Allows doing onchain payments from the wallet's lightning balance. - changed: flag console usage in crash reports (#10219) - changed: add "Tools" text to the tools button for increased visibility (#10277) - changed: improved UI feedback for send change to lightning function (#10247) - fix: improve Network Tab behavior when switching connection mode (#10280) - fix: re-add fiat values to csv/json history export (#10209) - fix: not proposing tx batching in some cases (#10204) * QML GUI & Android: - new: allow manual editing of fee/feerate (#10371) This also allows sending sub-1 sat/b transactions on Android. - new: support biometric authentication (#10340) Allows using the Android system lockscreen (e.g. fingerprints) to unlock the wallet and authorize payments. The previous optional built-in PIN code authentication is removed. - changed: make UI compatible with edge-to-edge layout (#10178) - changed: fee histogram colors: extend color palette to cover sub-1 s/b (#10307) - changed: enforce the usage of a single password for all wallet files (#10345) - changed: allow tap-to-focus in the qr code scanner (#10385) - fix: allow opening passwordless wallets (#10423) - fix: also protect address private keys from screenshots (#10426) * Lightning: - new: support LNURL-withdraw/LUD-3 (#9993) Allows scanning QR codes to receive funds on lightning (e.g. ATMs, vouchers). - changed: refactor handling of incoming htlcs (#10230) - changed: collect htlcs failed back to us before re-splitting (#10274) - fix: allow spending channel reserve if anchor channels are closed but not redeemed (2d17252) - fix: logic bug in liquidity hint calculation (#10305) - fix: race resulting in "Not enough balance" error when doing concurrent payments (#10325) - fix: self payments (and rebalance function) (#10271) - fix: gossip exchange with Core Lightning nodes (#10347) - fix: only wait for pending htlcs to get removed if peer is connected (1845143) * Electrum protocol: - new: add support for Electrum Protocol version 1.6 (#10295) See https://electrum-protocol.readthedocs.io/en/latest/protocol-changes.html#version-1-6 Min required version is still 1.4. - changed: prevent connecting to server with different genesis hash (#10281) - changed: add warmup budget before batching server rpc calls for faster startup (#10281) - changed: optimistically guess scripthash status on new blocks to reduce network traffic and improve privacy (#10290) - fix: flush network buffer before disconnecting from server (6423323) * Onchain / Wallet: - changed: non-SPV verified transactions now considered unconfirmed (#10216) - changed: always enforce dnssec validation for Openalias (#10349) * Submarine swaps: - new: cli commands to get swap statistics for swapserver operators (#10198): 'swapserver_get_history' and 'swapserver_get_summary' * CLI/RPC: - new: add 'export_lightning_preimage' command (#10242) - changed: return lightning preimage from 'check_hold_invoice' command (#10242) - changed: 'add_peer' now blocks until the connection is established (#10283) - changed: 'version_info' now shows OpenSSL version (828fc56) - fix: print warnings to stderr so output is still valid json (7bfe2dd) - fix: imply enabled proxy when starting with proxy cli option (#10326) * Plugins: - changed: plugins can now use existing cli command names without colliding with builtin commands (9c4c7f0) - changed: Timelock Recovery: check if locking address is ours or script (#10272) - removed: payserver plugin, now an external plugin, moved to spesmilo/electrum-payserver (d36b753) * Hardware wallets: - Coldcard: fix: compatibility with ckcc-protocol v1.5.0 (2172dad) * Contrib: - new: add README to scripts/ directory (5a14a58) * Dependencies: - changed: bump min required electrum-aionostr to 0.1.0 (e188102) * Builds/binaries: - Android: - new: support 16kb page size (#10148) - changed: bump Android target SDK version to 35 (#10178) - changed: bump OpenSSL from 1.1.1w to 3.0.18 (#10332) - changed: switch from cryptography to pycryptodomex (#10332) - changed: bump python version from 3.10.18 to 3.11.14 (#10388) - AppImage: - changed: migrate AppImage build to use modern/maintained appimagetool (#10019) # Release 4.6.2 (Aug 25, 2025) * General: - changed: minrelayfee clamps from [1, 50] to [0.1, 50] sat/vbyte (#10096) - new: add support for "mutinynet" signet test network (#10134) - new: network: don't request same tx from server that we just broadcast to it (#10111) - new: logging: add config.LOGS_MAX_TOTAL_SIZE_BYTES: to limit size on disk (#10159) * QML GUI (Android): - fix: cannot open keystore-encryption-only wallets (#10171) - fix: wizard: restoring from seed broken if already opened a wallet (#10117) - fix: handle invoice validation errors on save (#10122) - fix: sweep: handle network errors gracefully (#10108) - fix: sweep: handle unexpected script_types (#10145) * Qt GUI (desktop): - fix: wizard: hardware device: handle missing xpub (#10109) - fix: wizard: enable-keystore for bip39 seeds and hw devices (#10123) * Lightning: - fix: slow down peers sending too much gossip, and other rate-limits (#10153) * Submarine swaps: several bug fixes and improved reliability. * CLI/RPC: - changed: onchain_history: add back from_height/to_height params (#10119) - changed: reverse_swap: new mandatory parameter 'prepayment' (#10165) - new: get_submarine_swap_providers: added command to fetch swap providers (#10158) * Plugins: - Nostr Cosigner: fix: don't allow saving tx without txid (#10128) # Release 4.6.1 (Aug 5, 2025) * QML GUI (Android): - fix: QR scanner crashes due to null/orphaned View in hierarchy (#10071) - fix: creating a tx with a pre-segwit watchonly wallet (#10042) * CLI/RPC: - fix several bugs related to new hold_invoice APIs. This required minor breaking changes in the new APIs. (#10059, #10082) - add max_cltv, max_fee_msat parameters to lnpay command (#10067) * Hardware wallets: - bitbox02: bump required and bundled library to 7.0.0 (#10040) This should add support for the new BitBox02 "Nova" devices. * General: - rework crash reporter (#10052) - show additional confirmation popup on clicking "Send" - remove the "Never" button and the corresponding config option. The crash reporter is now always shown on uncaught exceptions. This unifies some code paths: the crash-reporter-disabled case was untested and buggy. - don't show reporter multiple times for the "same" exception - new: network: parallelize block-header-chunks downloads (#10033) * Lightning: - wallet: don't spend reserve utxo to create new reserve utxo (#10091) * various UI fixes (#10060, #10062, #10081, ...) # Release 4.6.0 (July 16, 2025) * A 'Terms of Use' screen was added to the install wizard. While the licence remains unchanged, we ask users to agree with the fact that we are not a custodial service or a money transmitter. The Terms of Use screen also makes it clear that all issues are to be resolved in public, and that there is no user support via private channels. * Nostr support: (using new dependency: electrum-aionostr) Electrum now uses Nostr in the context of submarine swaps, and in several plugins. Electrum will not connect to Nostr by default, only if required. * Submarine swaps over Nostr: The Electrum client will connect to Nostr in order to discover submarine swap providers, and to perform related RPCs. This means that: - Anyone can become a swap provider (you need to run an Electrum daemon with the 'swapserver' plugin). Submarine swap providers advertise their fees and their liquidity on Nostr. See https://electrum.readthedocs.io/en/latest/swapserver.html for set-up documentation. - Submarine swap providers do not need to provide an HTTP endpoint, since RPCs are performed via Nostr. They also do not need to have public lightning channels. - Because a decentralized service needs to be trustless, the option to perform zero-confirmation swaps has been removed from Electrum. Note that Electrum connections to Nostr relays are only initiated when the user uses the swap service, and the nostr public key used by the client is ephemeral. In contrast, swap providers use a persisted identity. * Third-party plugins: - Electrum supports the installation of plugins distributed by third-parties as ZIP files. While it has long been possible to install third-party plugins when running Electrum from python sources, the same is now possible when using desktop binaries (Windows, MacOS, Linux). Third-party plugins are installed as ZIP files in the user's electrum data directory. - In order to prevent plugin installation by malware, third-party plugins can only be enabled if the user enters a plugin authorization password (distinct from the wallet password). Setting up that plugin authorization password requires administrator permissions on the local machine; a password-derived public key must be written in the system. * Lightning: - Anchor channels (#9264): Newly created channels use anchor commitments by default. Since sweeping outputs from anchor channels may require external UTXOs, lightning can no longer be enabled in wallets that do not have a software keystore (hardware wallets, watching-only wallets). Existing wallets that are in that situation cannot create new channels. - wallets with anchor channels must always have utxos available (#9536) - support added for onion messages (only CLI for now) (#9039) - lots of fixes and improvements (#8857, #8547, #9700, #9083, ...) * Qt Desktop GUI: - migrate from Qt5 to Qt6 (#9189) - new: screenshot "protection" on Windows (#9898). Inspired by Windows Recall, by default screenshots will contain black rectangles in place of the Electrum windows, to try to avoid leaking secret keys. This is opt-out using a config variable. - exposed option to connect to only a single server (--oneserver) - Wallet file encryption: - Non-multisig hardware wallet files can now be encrypted with either using the hardware device or (new) a password. (#5561) - The option to have a password-protected wallet without file encryption has been removed from the Qt GUI. It is still possible to create such a wallet using the command line. - Wallet unlocking: - Wallets can be unlocked in the Qt GUI. When a password-protected wallet is unlocked, its password is kept in memory, and signing transactions will not require to enter the password. The unlocked state is rendered by the 'open lock' icon in the status bar. - If a wallet needs to sweep anchor channel outputs using extra UTXOs, the operations will be performed without requiring the user password if the wallet is unlocked. If the wallet is locked, the status bar will show a 'password required' button. - Transaction batching: When creating a new payment, if the output can be added to an existing mempool transaction, the 'New transaction' window will show a drop-down menu, proposing a list of transactions that can be batched with the current payment. This replaces the previous 'batch' option checkbox, and gives more control to the user. - Keystore enabling/disabling (Qt): - It is now possible to add a seed to an existing watching-only wallet, or to a keystore within a multisig wallet. Similarly, it is possible to pair a watching-only keystore with a hardware device. These operations are performed from the 'Wallet Information' dialog. - Lightning address contacts: - It is now possible to create contacts with (lnurl type) lightning addresses as payment identifier. - show warnings on wallet close if there are sensitive pending operations, e.g. when in the middle of doing a swap (#9715) - some performance improvements for large wallets (#9958, #9967, #9968) - qr-reader: macos: add runtime requesting of camera permission (#9955) * Accounting rules: In order to properly handle on-chain transactions created by lightning channel force closures, we consider that funds successfully redeemed from a script with several possible recipients have never left the final recipient's wallet. This avoids having to write balance changes that are cancelled later. The corresponding addresses are rendered in the GUI as 'accounting addresses' (in orange). * New plugins: - Nostr Wallet Connect: This plugin allows remote control of Electrum lightning wallets via Nostr NIP-47. (#9675) - Nostr Cosigner: This plugin facilitates the exchange of PSBTs between cosigners of a multisig wallet. It replaces the former 'Cosigner pool' plugin. Instead of relying on a central server, it uses Nostr to send/receive PSBTs. (#9261) - Timelock Recovery: A timelock based inheritance scheme. See timelockrecovery.com (#9589) * CLI: - The command line help has been improved; parameters are documented in the same docstring as the command they belong to. - If the --wallet parameter passed to a command is a simple filename, it is now interpreted as relative to the users wallets directory, rather than to the current working directory. - Plugins may add extra commands to the CLI. Plugin commands must be prefixed with the plugin's internal name. - Support for hold invoices. - new commands: - listconfig, helpconfig, unsetconfig - onchain_capital_gains (was previously a field of onchain_history) - {add,settle,cancel,check}_hold_invoice - send_onion_message, get_blinded_path_via - wait_for_sync * General: - Mitigate against dust attacks; Add option to avoid spending from used addresses. (#9636) - Restrict process memory access on Linux. (#9749) - locale: syntax-check i18n translations at runtime. Malformed translation strings are now less likely to cause errors: instead we fallback to the original English string (#10011) - fix: would sometimes hang on startup if system clock jumped backwards (#9802) * QML GUI (Android): - "Sweep key" feature ported to mobile - Estimate amount when Max is checked - exposed option to connect to only a single server (--oneserver) * Android: - replace QR code scanning library to make scanning fun again (#9983) - properly ask for (notification) OS permission access. (#9682) - add option to prevent the app touching the screen brightness (#9321) * Electrum protocol: add padding and some noise to messages (#9875) * Hardware wallets: - Coldcard: add feature to upload multisig wallet configuration to Coldcard via USB. - KeepKey: we now vendor our fork of keepkeylib, instead of using the unmaintained upstream as an external dependency (#9650) - Ledger: - rm support for "HW.1" and "Nano" (non-S) devices (#9652) - rm dependency: btchip-python (#9370) * Builds/binaries: - new minimum OS requirements: - Windows: x86_64, Windows 10 (1809) note: 32-bit Windows is no longer supported. - macOS: 11 "Big Sur" - Linux AppImage: x86_64, glibc 2.31 ("debian 11"-equivalent) * Dependencies: - the minimum required python version was increased: 3.8->3.10 (#9418) - new first-party dep: electrum-aionostr - forked the seemingly unmaintained davestgermain/aionostr library - new first-party dep: electrum-ecc - split out our existing libsecp256k1 python bindings into this separate package # Release 4.5.8 (Oct 23, 2024) * Qt Desktop GUI: - fix: regression: bump_fee and dscancel dialogs erroring (#9273) # Release 4.5.7 (Oct 21, 2024) * General: - new: add new historical exchange rate providers: Bitfinex and Bitstamp - fix: wizard regression: 2fa wallet setup erroring (#9253) - fix: python 3.13 compat: could not connect to some self-signed electrum servers with weird TLS certs. As workaround, set pre-3.13 behaviour (#9258) * Lightning: - fix: send update_fee right away after channel_reestablish (3a465593) This fixes a race that can result in a force-closure if we try sending a payment very soon after reestablishing the channel. * Qt Desktop GUI: - fix: show fee warnings also in the transaction dialog (c4fe2796) # Release 4.5.6 (Oct 16, 2024) * General: - new: add support for testnet4 (#9197) - fix: wizard: allow passphrase for some '2fa' seeds (#9088) - fix: trustedcoin wallet wizard continuation if file has keystore-only encryption (#9237) - fix: trustedcoin: sanitize error messages coming from 2fa server - fix: new wizard did not set keystore password if storage was not encrypted (#9147) - changed: set stricter UNIX permissions for log files (fa8595b1) * QML GUI (Android): - new: show seed passphrase in WalletDetails (#9204) - new: set max screen brightness when displaying QR codes (79c08536) - fix: crash due to ConcurrentModificationException (450b9a0) - fix: issue deactivating PIN when no wallet loaded (#8366) - fix: only allow Channel Backup import on Lightning-enabled wallets (8d9bcda) * Qt Desktop GUI: - fix: scanning multi (privkeys, addresses) from QR (4dc64e4) * Hardware wallets: - ColdCard: new: export multisig wallet to coldcard over USB (#7682) - Trezor: - new: add support for new device "Safe 5" (#9171) - update: fix compat with and bump pinned library to 0.13.9 (#9141) - Ledger: - new: add support for new device "Flex" (#9179) - update: bump pinned library to 0.3.0, raise max lib to <0.4 (719292f8) - Jade: update: bump library to 1.0.31 (9a84bb32) * CLI/RPC: - changed: require wallet password for lnpay and similar commands (#9236) (This is in addition to the wallet needing to be loaded, and requiring read access to the config file) * Builds/binaries: - changed: include unit tests in tarballs (#9207) - android: - changed: set target_sdk_version to 34 (2917fde5) - update: bump python version (3.8->3.10) (08127a60) - work towards F-Droid inclusion: - reproducible apks: strip file path prefix from .pyc files (6ebdbf04) - add fastlane metadata for f-droid (#9211) - change versionCode calculation (#9221) - build.gradle: set android.dependenciesInfo.includeInApk=false (af18df10) - contrib/release_www.sh: put android versionCode in "version" file (#9233) # Release 4.5.5 (May 30, 2024) * General: - fix: timeout error shadowed by aiorpcx cancellation bug (#8954) - changed: Fiat exchange rates: do not overwrite the locally saved historical data. Instead, merge old and new data (a2fb70d6). This also ~fixes the CoinGecko historical API by only asking for the last 365 days. - update: support latest revision of SLIP-39 mnemonic spec (to restore) (#9059) * Lightning: - new: unify max fee bounds for payments, make it configurable (#9041) - changed: trampoline fees: instead of hardcoded list, use exponential search, capped by configurable budget (#9033) - fix: opening new channels with peer that has .onion address (#9002) * Dependencies: - remove bitstring (#9020) * QML GUI (Android): - new: add tx options to ConfirmTxDialog, RbfBumpFeeDialog (#8909) - various UI fixes (#9018, 472a65eb) * Qt Desktop GUI: - fix: save notes whenever modified (#8951) - fix: offline 2fa wallet creation failing in some cases (#9037) - various UI fixes (#8962, #8874, #9012, 1047200a, #9058) * Hardware wallets: - Bitbox02: fix: call pairing dialog when necessary (#8971) - Jade: update: bump library to 1.0.29 (#9007) * Binaries: - new: add AppArmor profiles for tarball and AppImage (#9003) # Release 4.5.4 (March 14, 2024) * General: - fix: failing WalletDB upgrade(58) in 4.5.3 (#8913), for wallets with partial txs saved into the history as local txs * Lightning: - changed: use longer final_cltv_delta for client-normal-swap, to give more time for user to come back online while doing the swap (#8940) - changed: create trampoline onions even when directly paying a trampoline forwarder node (777c2ffb) * Hardware wallets: - Trezor: - fix: allow adding SLIP-19 ownership proofs to complete inputs (#8910) * Plugins: - fix: a race in swapserver when handling server-normal-swaps (#8825) # Release 4.5.3 (February 23, 2024) * General: - changed: label tx sizes as "vbytes", and feerates as "sat/vbyte" (#8864) - fix: wizard regression not able to use HWW as cosigner for new wallets (643fbec) - fix: onchain invoice paid detection broken if jsonpatch enabled (#8842) - fix: program not starting because of bad "proxy" config value (#8837) - fix: wizard: don't log sensitive values: replace blacklist with whitelist (638fdf11) * Qt Desktop GUI: - new: basic "add server as bookmark" functionality (#8865) - fix: potential race condition in wizard page construction (c78a90a) - fix: don't use lightning invoice when user specifies MAX amount (#8900) - various UI fixes (#8874, 2882c4b, #8889, 66af6e6) * QML GUI (Android): - fix potential concurrency issue loading wallet (#8355) - fix: wizard: fails to restore from 2fa seed: KeyError: 'x1' (#8861) - various UI fixes (50a53aa, 0a6b2d5, #8782, 6738e1e, c0b8927, 016e500, #8898) * Hardware wallets: - Trezor: - new: support SLIP-19 ownership proofs, for trezor-based Standard_Wallets (#8871) - fix: regression in sign_transaction for trezor one for multisig (#8813) * CLI/RPC: - changed: nicer error messages and error-passing (#8888) * Lightning: - fix: timing issue in lnpeer.reestablish_channel, for replaying unacked updates (79d88dcb) # Release 4.5.2 (January 20, 2024) * Qt Desktop GUI: - fix crash during startup/wizard-open (#8833) # Release 4.5.1 (January 19, 2024) * Lightning: - fix: MPP regression when using gossip that made paying small invoices fail (95c55c542) - fix: better handle dataloss (#8814) - allow manually requesting force-close in WE_ARE_TOXIC state - fix some timing issues * General: - localization: never translate CLI/RPC (0e5a1380) - localization: simplify how default language is chosen (0e5a1380) * QML GUI (Android): - bump min required android version from android 5.0 to 6.0 (#8761) (older versions have not been working in practice since at least 4.4.0) - properly refresh history if addresses are deleted from imported wallets (#8782) - fix crash when LNURLp is scanned/pasted (#8822) - fix crash for new wallets having cosigner using hww #8808) - fix crash in finalizer when txid is undefined (#8807) - various UI fixes (291f0ce, 3d9996a, ec81f00) * Qt Desktop GUI: - also support unfinished wallets when opened through File>Open (#8809) - fix handler for OpenFileEventFilter (6a28ef5) # Release 4.5.0 (January 12, 2024) * General: - remove SSL options from config (012ce1c) - make number of logfiles to keep configurable (5e8b14f) - refactored SimpleConfig and added ConfigVars (#8454) - incremental writes of wallet file (#8493) - add warnings and prompt users when signing txs with non-default sighashes (#8687) - refactored bip21/bolt11/lnurl/etc-handling into PaymentIdentifiers (#8462) - add option to merge duplicate outputs (#8474) - fix: consider bip21 URIs as invalid if they contain unknown req-* param (#8781) * Lightning: - fix BOLT-04 "MUST set `short_channel_id` to the `short_channel_id` used by the incoming onion" (ca93af2) - add support for hold invoices (1acf426) - add support for bundled payments (c4eb7d8) - various MPP improvements (#7987, ..) - support large channels (40f2087) - new flow for normal submarine swaps (fd10ae3) - the client now uses hold invoices, just like the server - the client waits until HTLCs are received before going on-chain - the user may cancel the swaps during that waiting time - don't create invoice with duplicate route hints (a3997f8) - don't set channel OPEN before channel_ready has been both sent and received (#8641) - if trampoline is enabled, do not add non-trampoline nodes to invoices (120faa4) * QML GUI (Android): - port to Qt6 (#8545) - fix regression for lnurl-pay (#8585) - fix invoice amount bounds check (#8582) - fix places where text was rendered off-screen for certain translations (#8611) - fix lnworker undefined when node alias requested (#8635) - fix BIP39 cosigner script type must be same as primary (8cd95f1) - fix: never use current fiat exchange rate for old historical amounts (#8788) - better handle android back-gesture (#8464) - new: show private key in address details (016b5eb) - new: show tx inputs in TxDetails and other dialogs (#8772) - new: label sync plugin toggle (b6863b4) - fix: properly suggest paying BOLT11 invoice onchain if insufficient balance (0a80460) - new: message sign & verify (e5e1e46) - new: allow never expiring payment requests (#8631) - new: add coins/UTXOs to addresses list, add filters (cf91d2e) - new: delete addresses from imported wallet (#8675) - new: add support for lightning address and openalias (03dd38b) - new: add setting to allow screenshots everywhere (0dae1733) - simplify welcome page for first-start network settings (#8737) - various UI fixes (b846eab, #8634, 9ed5f7b, 941f425, b20a4b9, af61b9d, 0fb47c8, 2995bc8, ..) * Qt Desktop GUI: - port wizard to new implementation - fix fiat balance sorting in address list window (#8469, #8478) - remove thousands separator when copying numbers to clipboard (#8479) - new: option to use extra trampoline for legacy payments (b2053c6) - new: send change to lightning option for on-chain payments (649ce97) - new: notes tab for saving text in the (encrypted) wallet file (d691aa07) - simplify welcome page for first-start network settings (#8737) - various UI fixes (#8587, #6526, ..) * Hardware wallets: - Trezor: allow multiple change outputs (#3920) - Trezor: support external pre-signed inputs (#8324) - Bitbox02: update to 6.2.0 (#8459) * Plugins: - new: swapserver plugin (#8489) * Builds/binaries: - update bundled zbar, for security fixes (#8805) # Release 4.4.6 (August 18, 2023) (security update) * Lightning: - security fix: multiple lightning-related security issues have been fixed. See disclosures: - https://github.com/spesmilo/electrum/security/advisories/GHSA-9gpc-prj9-89x7 - https://github.com/spesmilo/electrum/security/advisories/GHSA-8r85-vp7r-hjxf - fix: cannot sweep from channel after local-force-close, if using imported channel backup (#8536). Fixing this required adding a new field (local_payment_pubkey) to the channel backup import/export format and bumping its version number (v0->v1). Both v0 and v1 can be imported, and we only export v1 backups. When you force close a channel, the GUI will prompt you to save a backup. In that case, you must export the backup using the updated Electrum, and not rely on a backup made with an older release of Electrum. Note that if you request a force close from the remote node or co-op close, you do not need to save a channel backup. - fix: we would sometimes attempt sending MPP even if not supported by the invoice (2cf6173c) * QML GUI: - fix lnurl-pay when config.BTC_AMOUNTS_ADD_THOUSANDS_SEP is True (5b4df759) * Hardware wallets: - Trezor: support longer than 9 character PIN codes (#8526) - Jade: support more custom-built DIY Jade devices (#8546) * Builds/binaries: - include AppStream metainfo.xml in tarballs (#8501) * fix: exceptions in some callbacks got lost and not logged (3e6580b9) # Release 4.4.5 (June 20, 2023) * Hardware wallets: - jade: fix regression in sign_transaction (#8463) * Lightning: - fix "rebalance_channels" function (#8468) * enforce that we run with python asserts enabled, regardless of platform (d1c88108) # Release 4.4.4 (May 31, 2023) * QML GUI: - fix creating multisig wallets involving BIP39 seeds (#8432) - fix "cannot scroll to open a lightning channel" (#8446) - wizard: "confirm seed" screen to normalize whitespaces (#8442) - fix assert on address details screen (#8420) * Qt GUI: - better handle some expected errors in SwapDialog (#8430) * libsecp256k1: bump bundled version to 0.3.2 (10574bb1) # Release 4.4.3 (May 11, 2023) * Intentionally break multisig wallets that have heterogeneous master keys. Versions 4.4.0 to 4.4.2 of Electrum for Android did not check that master keys used the same script type. This may have resulted in the creation of multisig wallets that cannot be spent from with any existing version of Electrum. It is not sure whether any users are affected by this; if there are any, we will publish instructions on how to spend those coins (#8417, #8418). * Qt GUI: - handle expected errors in DSCancelDialog (#8390) - persist addresses tab toolbar "show/hide" state (b40a608b) * QML GUI: - implement bip39 account detection (0e0c7980) - add share toolbutton for outputs in TxDetails (#8410) * Hardware wallets: - Ledger: - fix old bitcoin app support (<2.1): "no sig for ..." (#8365) - bump req ledger-bitcoin (0.2.0+), adapt to API change (30204991) * Lightning: - limit max feature bit we accept to 10_000 (#8403) - do not disconnect on "warning" messages (6fade55d) * fix wallet.get_tx_parents for chain of unconf txs (#8391) * locale: translate more strings when using "default" lang (a0c43573) * wallet: persist frozen state of addresses to disk right away (#8389) # Release 4.4.2 (May 4, 2023) * Qt GUI: - fix undefined var check in swap_dialog (#8341) - really fix "recursion depth exceeded" for utxo privacy analysis (#8315) * QML GUI: - fix signing txs for 2fa wallets (#8368) - fix for wallets with encrypted-keystore but unencrypted-storage (#8374) - properly delete wizard components after use (#8357) - avoid entering loadWallet if daemon is already busy loading (#8355) - no auto capitalization on import and master key text fields (5600375d) - remove Qt virtual keyboard and add Seedkeyboard for seed entry (#8371, #8352) - add runtime toggling of android SECURE_FLAG, to allow screenshots (#8351) - restrict cases where server is shown "lagging" (53d61c01) * fix hardened char "h" vs "'" needed for some hw wallets (#8364, 499f5153) * fix digitalbitbox(1) support (22b8c4e3) * fix wrong type for "history_rates" config option (#8367) * fix issues with wallet.get_tx_parents (a1bfea61, 56fa8325) # Release 4.4.1 (April 27, 2023) * Qt GUI: - fix sweeping (#8340) - fix send tab input_qr_from_camera (#8342) - fix crash reporter showing if send fails on typical errors (#8312) - bumpfee: disallow targeting an abs fee. only allow feerate (#8318) * QML GUI: - fix offline-signing or co-signing pre-segwit txs (#8319) - add option to show onchain address in ReceiveDetailsDialog (#8331) - fix strings unique to QML did not get localized/translated (#8323) - allow paying bip21 uri onchain that has both onchain and bolt11 if we cannot pay on LN (#8334, 312e50e9) - virtual keyboard: make buttons somewhat larger (75e65c5c) - fix(?) Android crash with some OS-accessibility settings (#8344) - fix channelopener.connectStr qr scan popping under (#8335) - fix restoring from old mpk (watchonly for "old" seeds) (#8356) * libsecp256k1: add runtime support for 0.3.x, bump bundled to 0.3.1 * forbid paying to "http:" lnurls (enforce https or .onion) (1b5c7d46) * fix wallet.bump_fee "decrease payment" erroring on too high target fee rate (#8316) * fix performance regressions in tx logic (ee521545, 910832c1) * fix "recursion depth exceeded" for utxo privacy analysis (#8315) # Release 4.4.0 (April 18, 2023) * New Android app, using QML instead of Kivy - Using Qt 5.15.7, PyQt 5.15.9 - This release still on python3.8 - Feature parity with Kivy - Android Back button used throughout, for cancel/close/back - Note: two topbar menus; tap wallet name for wallet menu, tap network orb for application menu - Note: long-press Receive/Send for list of payment requests/invoices * Qt GUI improvements - New onchain transaction creation flow, with configurable preview - Various options have been moved to toolbars, where their effect can be more directly observed. * Privacy features: - lightning: support for option scid_alias. - Qt GUI: UTXO privacy analysis: this dialog displays all the wallet transactions that are either parent of a UTXO, or can be related to it through address reuse (Note that in the case of address reuse, it does not display children transactions.) - Coins tab: New menu that lets users easily spend a selection of UTXOs into a new channel, or into a submarine swap (Qt GUI). * Internal: - Lightning invoices are regenerated every time routing hints are deprecated due to liquidity changes. - Script descriptors are used internally to sign transactions. # Release 4.3.4 - Copyright is Dubious (January 26, 2023) * Lightning: - make sending trampoline payments more reliable (5251e7f8) - use different trampoline feature bits than eclair (#8141) * invoice-handling: fix get_request_by_addr incorrectly mapping addresses to request ids when an address was reused (#8113) * fix a deadlock in wallet.py (52e2da3a) * CLI: detect if daemon is already running (c7e2125f) * add an AppStream metainfo.xml file for Linux packagers (#8149) * payserver plugin: -replaced vendored qrcode lib -added tabs for on-chain and lightning invoices -revamped html and javascript # Release 4.3.3 - (January 3, 2023) * Lightning: - fix handling failed HTLCs in gossip-based routing (#7995) - fix LN cooperative-chan-close to witness v1 addr (#8012) * PSBTs: - never put ypub/zpub in psbts, only plain xpubs (#8036) - for witness v0 txins, put both UTXO and WIT_UTXO in psbt (#8039) * Hardware wallets: - Trezor: optimize signing speed by not serializing tx (#8058) - Ledger: - modify plugin to support new bitcoin app v2.1.0 (#8041), - added a deprecation warning when using Ledger HW.1 devices. Ledger itself stopped supporting HW.1 some years ago, and it is becoming a maintenance burden for us to keep supporting it. Please migrate away from these devices. Support will be removed in a future release. * Binaries: - tighten build system to only use source pkgs in more places (#7999, #8000) - Windows: - use debian makensis instead of upstream windows exe (#8057) - stop using debian sid, build missing dep instead (98d29cba) - AppImage: fix failing to run on certain systems (#8011) * commands: - getinfo() to show if running in testnet mode (#8044) - add a "convert_currency" command (for fiat FX rate) (#8091) * Qt wizard: fix QR code not shown during 2fa wallet creation (#8071) * rework Tor-socks-proxy detection to reduce Tor-log-spam (#7317) * Android: add setting to enable debug logs (#7409) * fix payserver (merchant) js for electrum 4.3 invoice api (0fc90e07) * bip21: more robust handling of URIs that include a "lightning" key (ac1d53f0, 2fd762c3, #8047) # Release 4.3.2 - (September 26, 2022) * When creating new requests, reuse addresses of expired requests (fixes #7927). * Index requests by ID instead of receiving address. This affects the following commands: get_request, get_invoice, list_requests, list_invoices, delete_request, delete_invoice * Trampoline routing: remember routes that have failed. Try other routes instead of systematically raising tampoline fees. * Fix sweep to_local output from channel backup (#7959) * Harden build script for macOS binary: avoid using precompiled wheels from PyPI for most packages (#7918) * The Windows/AppImage/Android binaries are now built on debian using the snapshot.debian.org archive instead of ubuntu. This should help with historical reproducibility. (#7926) # Release 4.3.1 - (August 17, 2022) * build: we now also distribute a "source-only" Linux-packager-friendly tarball (d0de44a7, #7594), in addition to the current "normal" tarball. The "source-only" tarball excludes compiled locale files, generated protobuf files, and does not vendor our runtime python dependencies (the packages/ folder). * fix os.chmod when running in tmpfs on Linux (#7681) * (Qt GUI) some improvements for high-DPI monitors (38881129) * bring kivy request dialog more in-line with Qt (#7929) * rm support of "legacy" (without static_remotekey) LN channels. Opening these channels were never supported in a release version, only during development prior to the first lightning-capable release. Wallets with such channels will have to close them. (1f403d1c, 7b8e257e) * Qt: fix duplication of some OS notifications on onchain txs (#7943) * fix multiple recent regressions: - handle NotEnoughFunds when trying to pay LN invoice (#7920) - handle NotEnoughFunds when trying to open LN channel (#7921) - labels of payment requests were not propagated to history/addresses (#7919) - better default labels of outgoing txs (#7942) - kivy: dust-valued requests could not be created for LN (#7928) - when closing LN channels, future (timelocked) txs were not shown in history (#7930) - kivy: fix deleting "local" tx from history (#7933) - kivy: fix paying amountless LN invoice (#7935) - Qt: better handle unparseable URIs (#7941) # Release 4.3.0 - (August 5, 2022) * This version introduces a set of UI modifications that simplify the use of Lightning. The idea is to abstract payments from the payment layer, and to suggest solutions when a lightning payment is hindered by liquidity issues. - Invoice unification: on-chain and lightning invoices have been merged into a unique type of invoice, and the GUI has a single 'create request' button. Unified invoices contain both a lightning invoice and an onchain fallback address. - The receive tab of the GUI can display, for each payment request, a lightning invoice, a BIP21 URI, or an onchain address. If the request is paid off-chain, the associated on-chain address will be recycled in subsequent requests. - The receive tab displays whether a payment can be received using Lightning, given the current channel liquidity. If a payment cannot be received, but may be received after a channel rebalance or a submarine swap, the GUI will propose such an operation. - Similarly, if channels do not have enough liquidity to pay a lightning invoice, the GUI will suggest available alternatives: rebalance existing channels, open a new channel, perform a submarine swap, or pay to the provided onchain fallback address. - A single balance is shown in the GUI. A pie chart reflects how that balance is distributed (on-chain, lightning, unconfirmed, frozen, etc). - The semantics of the wallet balance has been modified: only incoming transactions are considered in the 'unconfirmed' part of the balance. Indeed, if an outgoing transaction does not get mined, that is not going to decrease the wallet balance. Thus, change outputs of outgoing transactions are not subtracted from the confirmed balance. (Before this change, the arithmetic values of both incoming and outgoing transactions were added to the unconfirmed balance, and could potentially cancel each other.) * In addition, the following new features are worth noting: - support for the Blockstream Jade hardware wallet (#7633) - support for LNURL-pay (LUD-06) (#7839) - updated trampoline feature bit in invoices (#7801) - the claim transactions of reverse swaps are not broadcast until the parent transaction is confirmed. This can be overridden by manually broadcasting the local transaction. - the fee of submarine swap transactions can be bumped (#7724) - better error handling for trampoline payments, which should improve payment success rate (#7844) - channel backups are removed automatically when the corresponding channel is redeemed (#7513) # Release 4.2.2 - (May 27, 2022) * Lightning: - watching onchain outputs: significant perf. improvements (#7781) - enforce relative order of some msgs during chan reestablishment, lack of which can lead to unwanted force-closures (#7830) - fix: in case of a force-close containing incoming HTLCs, we were redeeming all HTLCs that we know the preimage for. This might publish the preimage of an incomplete MPP. (1a5ef554, e74e9d8e) * Hardware wallets: - smarter pairing during sign_transaction (238619f1) - keepkey: fix pairing with device using a workaround (#7779) * fix AppImage failing to run on certain systems (#7784) * fix "Automated BIP39 recovery" not scanning change paths (#7804) * bypass network proxy for localhost electrum server (#3126) * security fix: remove support of "file://" URIs from BIP70 payment requests, which could be used to trigger "open()" on arbitrary files (see https://github.com/spesmilo/electrum/security/advisories/GHSA-4fh4-hx35-r355) # Release 4.2.1 - (March 26, 2022) * Binaries: - Windows: we are dropping support for Windows 7. (#7728) Version 4.2.0 already unintentionally broke compatibility with Win7 and there is no easy way to restore and maintain support. Existing users can keep using version 4.1.5 for now, but should consider upgrading or changing their OS. Win8.1 still works but only Win10 is regularly tested. - bump bundled Python version (win, mac, appimage) to 3.9.11, (android) to 3.8.13 (1bb7ef92, #7721) (note these include a fix to an openssl DOS-vector CVE-2022-0778) - windows: bump pyinstaller to 4.10 and wine to 7.0 (#7721) * Kivy GUI: - fix "Child Pays For Parent" not working on Android (#7723) - revert to defaulting the UI language to English (25fee6a6) * Qt GUI: - macOS: fix opening "Preferences" segfaulting for some (#7725) - more resilient startup: better error-handling and fallback (#7447) * Library: - fix LN error/warning message-handling, and fix regression that errors during channel-open were not properly shown in GUI (a92dede4) - during LN chan open, do not backup wallet automatically (#7733) - Imported wallets: fix delete_address rm-ing too many txs (#7587) - fix potential deadlock in wallet.py (d3476b6b) * Hardware wallets: - ledger: add progress indicator to sign_transaction (#7516) * fix the "--portable" flag for AppImage, and for pip installs (#7732) # Release 4.2.0 - (March 16, 2022) * The minimum python version was increased to 3.8 (#7661) * Lightning: - redesigned MPP splitting algorithm (#7202) - trampoline: implement multi-trampoline MPP (#7623) - implement option_shutdown_anysegwit, and allow dust limits below 546 sat (#7542) - implement option_channel_type (#7636) - implement modern closing negotiation (#7586, #7680) * improve support for "lightning:" URIs on all platforms (#7301) * Qt GUI: - add setting "show amounts with msat precision" (5891e039) - add setting "add thousand separators to bitcoin amounts" (#7427) * CLI/RPC: - implement Unix sockets and make them the default (#7545, #7566) - add "bumpfee" command (#7438) * Kivy GUI: - show network setup on first start before wallet creation (#7464) - add "Child Pays For Parent" option (#7487) - improved locale handling (22bb52d5, 7cb11ced, 4293d6ec) * Hardware wallets: - trezor: bump trezorlib to 0.13 (#7590) - bitbox02: bump bitbox02 to 6.0, support send-to-taproot (#7693) - ledger: support "Ledger Nano S Plus" (#7692) * Library: - added support for sighash types beside "ALL" (#7453) - signmessage: also accept Trezor-type sigs for segwit addrs (#7668) - network: make request timeout configurable (#7696) - paytomany (onchain txout batching) now allows multiple max("!") amounts with specified weights (#7492) * Binary builds - AppImage: changed base image from ubuntu 16.04 to 18.04 (5d0aa63a) * migrated from Travis CI to Cirrus CI (#7431) * Lots of other minor bugfixes and usability improvements. # Release 4.1.5 - (July 19, 2021) * Builds/binaries: - macOS: the .dmg binary should now be reproducible * Kivy/Android: fix paying bip70 invoices (regression) (90579ccf) * fix: payment requests not saved if process is killed (6a049d99) * Lightning: improve payment success when using trampoline (3a7f5373) * add support for signet test network (#7282) * Qt GUI: - allow restoring from SLIP39 seeds (#6917) - rework QR code scanning on Windows and macOS (#7365) - support smaller window sizes, decrease minimums (#7385) * GUIs: add "funded or unused" filter option to Addresses tab (#5823) # Release 4.1.4 - (June 17, 2021) * Kivy/Android: fix a regression where a non-LN wallet could not open the settings (c49d6995) * CLI/RPC: fix "close_wallet" command (#7348) # Release 4.1.3 - (June 16, 2021) * Builds/binaries: - Android: the binaries (APKs) should now be reproducible (#7263) - AppImage: fix some startup issues by including libxcb deps (#7198) * Lightning: - smarter LN pathfinding (if trampoline is disabled): - estimate liquidity in channels using previous attempts (#7152) - consider inflight HTLCs and try to route around them (#7292) - bugfix: add more safety checks to avoid "batch RBF" feature merging LN funding txs (#7298) - remove HTLC value upper limit of ~42 mBTC (#7328) - Kivy GUI: implement freezing LN channels (11bb39ee) * imported wallets: when enabling the "Use change addresses" option, change will now be sent to a random unused imported address. (#7330) As before, by default, change is sent back to the "from address". * seed generation: make sure newly created electrum seeds don't have correct bip39 checksum by chance (#6001) * other minor fixes # Release 4.1.2 - (April 8, 2021) * Qt GUI: - fix some crashes when exiting (#6889) - make sure pressing Ctrl-C always quits (c41cd4ae) * Kivy GUI (Android): - fix bug with scrollbar, again (#7155) - 2fa wallets: fix making transactions (#7190) - implement freezing addresses (#7178) * Android: use more modern application launcher/icon (#7187) # Release 4.1.1 - (April 2, 2021) * fix Qt crash with the swap dialog * fix Kivy bug with scrollbar (#7155) * fix localization issues (#7158 #4621) * fix python crash with swaps (#7160) * other minor fixes # Release 4.1.0 - Kangaroo (March 30, 2021) This version is our second major release with support for the Lightning Network. While our initial Lightning release was mostly about implementing the protocol, this release brings features that are specifically aimed at keeping Electrum lightweight and trustless, while avoiding single points of failure. Most of the features listed below are user-visible. * The wallet creation wizard no longer asks for a seed type, and creates segwit wallets with bech32 addresses. Older seed types can still be created with the command line. * Paid invoices (both incoming and outgoing) are automatically removed from the send/receive lists of the GUI (one confirmation is needed for onchain invoices). Once removed from the list, invoice details can still be accessed from the transaction history. In Qt, invoice lists have been renamed to 'Sending queue' and 'Receiving queue'. * Lightning: - recoverable channels (see below) - trampoline payments (see below) - support multi-part-payment - support upfront-shutdown-script * Recoverable channels (option): - Recovery data is added to the channel funding transaction using an OP_RETURN. This makes it possible to recover a static backup of the channel from the wallet seed. Please note that static backups only allow users to request a force-close of the channel with the remote node, so that funds not locked in HTLCs can be recovered. This assumes that the remote node is still online, did not lose its data, and accepts to force close the channel. - This option is only available for standard wallets with an Electrum seed. It is not available for hardware wallets, because it requires a deterministic derivation of the nodeID. It is also not available in watching-only wallets, for the same reason. If a wallet can have recoverable channels but has an old nodeID, users who want to use that feature need to close all their existing channels, and to restore their wallet from seed. - Channel recovery data uses 20 bytes (16 bytes of the remote NodeID plus 4 magic bytes) and is encrypted so that only the wallet that owns it can decrypt it. However, blockchain analysis will be able to tell that the transaction was probably created by Electrum. - If the 'use recoverable channels' option is enabled, other nodes cannot open a channel to Electrum. - If a channel is force-closed, the information in the on-chain backup is not sufficient to retrieve the funds in the to_local output, in case the wallet is lost in a boating accident before expiration of the CSV delay. For that reason, an additional backup is presented to the user if they force-close a channel. * Trampoline routing (option): Trampoline is a solution that allows light clients to delegate path-finding on the Lightning Network, so that they do not have to download the entire network graph. Trampoline routing was originally proposed by Bastien Teinturier and is used in the Phoenix wallet. Here is how Trampoline works in Electrum: - Trampoline is enabled by default, in order to prevent unwanted download of the network gossip. If trampoline is disabled, the gossip will be downloaded, regardless of the existence of channels. - Because there is no discovery mechanism for trampoline nodes, the list of available trampolines is hardcoded in the client (it will remain so until support for trampoline routing is announced in gossip). 3 trampoline nodes are currently available on mainnet: ACINQ, Electrum and Hodlister. - If Trampoline is enabled: - payments use trampoline routing. - gossip is disabled. - the wallet can only open channels with trampoline nodes. - pre-existing channels with non-trampoline nodes are frozen for sending. - There are two types of trampoline payments: legacy and trampoline end-to-end. Legacy payments are possible with any receiver, but they offer less privacy than end-to-end trampoline payments. Electrum decides whether to perform legacy or end-to-end based on the features in the invoice: - OPTION_TRAMPOLINE_ROUTING_OPT (bit 25) for Electrum - OPTION_TRAMPOLINE_ROUTING_OPT_ECLAIR (bit 51) for Eclair/Phoenix - When performing a legacy payment, Electrum will add a second trampoline node to the route in order to protect the privacy of the payer and payee. It will fall back to a single trampoline if the two-trampoline strategy has failed for all trampolines. (Note: two-trampoline payments are currently not possible if the first trampoline is the ACINQ node, and is disabled for that node.) - Similar to Phoenix, the fee and CLTV delay are found by trial-and-error. If there is a second trampoline in the route, we use the same fee/CLTV for both. This trial-and-error is temporary; the final specification should add fee information in the failure messages, so that we will be able to better fine-tune trampoline fees. * Qt: The increase fee dialog now has advanced options, and offers the choice between different RBF strategies. * Watchtowers: The 'use_local_watchtower' feature is deprecated, and it has been removed from the Qt GUI. The 'use_remote_watchtower' setting has been renamed to 'use_watchtower'. * Password unification (Android only): When the Android app is started, the entered password is checked against all wallets in the directory. If the test passes: - all wallets are encrypted - new wallets will use the unified password - password updates are performed on all wallets Whether the password is unified can be seen in the GUI: In the 'Settings' dialog, the description for the password setting is 'Change password for this wallet' if the password is not unified, and becomes 'Change password' if password is unified. * Submarine swaps are now available on kivy/android. * Android PIN reset: If the password is unified, the PIN can be reset by providing the password. * Android: on-chain fees have been removed from the settings dialog. Instead, the fee slider is shown to the user every time an on-chain transaction will be performed (sending a payment, opening a channel, initiating a submarine swap) * BIP-0350: use bech32m for witness version 1+ addresses (4315fa43). We have supported sending to any witness version since Electrum 3.0, using BIP-0173 (bech32) addresses. BIP-0350 makes a breaking change in address encoding, and recommends using a new encoding (bech32m) for sending to witness version 1 and later. * Block explorer: allow setting a custom URL in Qt GUI (#6965) # Release 4.0.9 - (Dec 18, 2020) * fixes a regression introduced in 4.0.8, that prevents from paying BIP70 invoices (#6859) * reflect frozen channels and disconnected peers in the displayed 'can send/can receive' amounts. # Release 4.0.8 - (Dec 17, 2020) * fix decoding BIP21 URIs with uppercase schema (d40bedb2) * psbt: put full derivation paths into PSBT by default (c8155129) * invoices: allow address-reuse (#6609, #6852) * A few other minor bugfixes. # Release 4.0.7 - (Dec 9, 2020) * kivy: fix open channel with 'max' amount * kivy: fix regression introduced in last release (a9fc440) * other minor GUI fixes * Dependencies: as part of adapting to new dnspython (#6828), - python-ecdsa is no longer needed at all, - cryptography is now required (min 2.6), the user can no longer choose between cryptography and pycryptodomex # Release 4.0.6 - (Dec 4, 2020) * Fix 'Max' button issue for submarine swaps button (#6770) * Fix 'Max' button in kivy (#6169) * Various fixes for Kivy/Android install wizard * More robust account keypath for BitBox02 (#6766) # Release 4.0.5 - (Nov 18, 2020) * Fix .dmg binary hanging on recently released macOS 11 Big Sur (#6461) * Lightning: - bugfix: during LN channel opening, if the client crashed at the wrong moment, the channel might not get fully persisted to disk, and would need manual console-tinkering to recover (#6656) - Lightning is enabled by default. Electrum will not connect to the Lightning Network until the user opens a channel. (#6639) - smarter node recommendation (to open channels with) (#6705) * user interface: some minor changes that aim to improve usability * Ledger: - fix enumerating devices with new bitcoin app (1.5.1) (b78cbcff) - fix compat with HW.1 (200f547a) * A few other minor bugfixes. # Release 4.0.4 - (Oct 15, 2020) * PSBT: fix regression in 4.0.3 where UTXO data was not included in QR codes (#6600) * new feature: "Cancel tx" (#6641). The Qt/kivy GUI allows cancelling an unconfirmed RBF tx by double-spending its inputs to self. * Windows binary: - fix some issues with QR scanning by building zbar ourselves (#6593) - when using setup exe, also install a debug binary (#6603) * Ledger: fix "The derivation path is unusual" warnings (#6512) (needs Bitcoin app 1.4.8+ installed on device) * A few other minor bugfixes and usability improvements. # Release 4.0.3 - (Sep 11, 2020) * PSBT: restore compatibility with Bitcoin Core following CVE-2020-14199: we now allow a PSBT input to have both UTXO and WITNESS_UTXO (#6429). (PSBTs created since 4.0.1 already contained UTXO for segwit inputs) * Hardware wallets: - bitbox02: better multisig UX: implement get_soft_device_id (#6386) - coldcard: fix "show address" for multisig (#6517) - all: run all device communication on a dedicated thread (#6561). This should resolve some threading issues. * new feature: "Automated BIP39 recovery" (#6219, #6155) When restoring from a BIP39 seed, add option to scan many known derivation paths for history, and show them to user to choose from. * show derivation path of keystores in Qt GUI Wallet>Information (#4700) * fix "signtransaction" RPC command (#6502) * Dependencies: pyaes is no longer needed (#6563) * The tar.gz source dist now bundles make_libsecp256k1.sh, to help users getting libsecp256k1 (#6323). * A few other minor bugfixes and usability improvements. # Release 4.0.2 - (July 8, 2020) - rm old corrupted non-bip70 invoices (#6345) - other minor fixes # Release 4.0.1 - (July 3, 2020) * Lightning Network support (experimental) - Our implementation of Lightning relies on Electrum servers to query channel states. Since servers can lie about the state of a channel, users should either use a server that they trust, or setup a private watchtower (see below). A watchtower is also recommended for lightning wallets that remain offline for extended periods of time (the default CSV 'to_self_delay' is 1 week). Please note that Electrum Personal Server (EPS) cannot be used with lightning wallets, because channels funding addresses are arbitrary. - Lightning funds cannot be restored from seed. Instead, users need to create static backups of their channels. Static backups cannot be used to perform lightning transactions, they can only be used to trigger a remote-force-close of a channel. - Lightning-enabled wallet files must not be copied. Instead, a backup of the wallet can be created from the Qt menu, and it will contain static backups of all its channels. Backups can also be exported for each channel (e.g. via QR code), and imported in another wallet. Since backups are encrypted with a key derived from the wallet's xpub, they can only be imported into another instance of the same wallet, or a watch-only version of it. The force-close is not triggered automatically when the backup is imported; imported backups can live inside a wallet file. - Lightning can be enabled in the GUI (Wallet>Information) or from the CLI (init_lightning). Lightning is currently restricted to HD p2wpkh wallets (including watch-only and hardware wallets). The Qt GUI, CLI/RPC, and the kivy GUI (Android) all have LN support, with feature-richness in that order. - LN protocol details: dataloss_protect and static_remotekey are required; varonion and payment_secret are implemented, MPP not yet. Channels are not announced ('private'), forwarding is disabled. We do not serve gossip queries, only consume them. - Submarine swaps: the GUI integrates a service that offers atomically exchanging on-chain and lightning bitcoins for a fee. Electrum Technologies runs a central server for this, powered by the Boltz backend. - Watchtowers: Electrum can run a local watchtower (GUI setting), or it can connect to a remote watchtower. A watchtower contains pre-signed transactions and does not need your private keys. A local watchtower will watch your channels whenever an Electrum instance is running, without needing access to your wallet file. An Electrum daemon can be configured to be used as a remote watchtower by setting 'watchtower_address', 'watchtower_user' and 'watchtower_password'. * Partially Signed Bitcoin Transactions (PSBT, BIP-174) are supported (#5721). The previous Electrum partial transaction format is no longer supported, i.e. this is an incompatible change. Users should make sure that all instances of Electrum they use to co-sign or offline sign, are updated together. * Hardware wallets: several fixes in general; notable changes: - The BitBox02 is now supported (#5993) - Multisig support for Coldcard (#5440) - Compatibility with latest Trezor fw (#6064, #6198, #5692) * Dependencies (see README for install instructions): - libsecp256k1 is now required (previously optional). python-ecdsa remains a dependency but it is now only used for DNSSEC. - Added: either one of pycryptodomex or cryptography is now required, mainly due to LN (previously pycryptodomex was optional, for fast AES) - Removed: jsonrpclib-pelix, the JSON-RPC library used for CLI/daemon * Qt GUI: several changes, notably: - Separation between output selection and transaction finalization. - Coin selection moved to the Coins tab, and it affects all txns, e.g. RBF fee-bumping, LN channel opens, submarine swaps. - Editable tx preview dialog that allows e.g. changing the locktime, toggling RBF, and manual coinjoins. * HTTP PayServer: The configuration of a bitcoin-accepting website using Electrum has been simplified and requires fewer steps (see documentation). The Payserver supports BIP70 and Lightning payments. * Android: - We now build two APKs, one for ARMv7 and one for ARMv8 - The kivy GUI now supports importing BIP39 seeds - Each wallet on kivy now can have a separate generic password, using which the wallet files are encrypted. An optional PIN, shared among all wallets, can be added to get prompted for spends. * The API of several CLI/RPC commands have changed, and several new commands have been introduced (mainly for LN). * Distributables: - The .tar.gz source dist is now built reproducibly. Relatedly, we no longer distribute a .zip sdist. - The MacOS binary now conforms to macOS 10.15; it is notarized by Apple. This required bumping the min macOS version to 10.13. Startup times should now be faster on 10.15. (#6128, #6225) * Transactions: - we now grind low R for ECDSA signatures to match bitcoind (#5820) * Lots and lots of other minor bugfixes and improvements. # Release 3.3.8 - (July 11, 2019) * fix some bugs with recent bump fee (RBF) improvements (#5483, #5502) * fix #5491: watch-only wallets could not bump fee in some cases * appimage: URLs could not be opened on some desktop environments (#5425) * faster tx signing for segwit inputs for really large txns (#5494) * A few other minor bugfixes and usability improvements. # Release 3.3.7 - (July 3, 2019) * The AppImage Linux x86_64 binary and the Windows setup.exe (so now all Windows binaries) are now built reproducibly. * Bump fee (RBF) improvements: Implemented a new fee-bump strategy that can add new inputs, so now any tx can be fee-bumped (d0a4366). The old strategy was to decrease the value of outputs (starting with change). We will now try the new strategy first, and only use the old as a fallback (needed e.g. when spending "Max"). * CoinChooser improvements: - more likely to construct txs without change (when possible) - less likely to construct txs with really small change (e864fa5) - will now only spend negative effective value coins when beneficial for privacy (cb69aa8) * fix long-standing bug that broke wallets with >65k addresses (#5366) * Windows binaries: we now build the PyInstaller boot loader ourselves, as this seems to reduce anti-virus false positives (1d0f679) * Android: (fix) BIP70 payment requests could not be paid (#5376) * Android: allow copy-pasting partial transactions from/to clipboard * Fix a performance regression for large wallets (c6a54f0) * Qt: fix some high DPI issues related to text fields (37809be) * Trezor: - allow bypassing "too old firmware" error (#5391) - use only the Bridge to scan devices if it is available (#5420) * hw wallets: (known issue) on Win10-1903, some hw devices (that also have U2F functionality) can only be detected with Administrator privileges. (see #5420 and #5437) A workaround is to run as Admin, or for Trezor to install the Bridge. * Several other minor bugfixes and usability improvements. # Release 3.3.6 - (May 16, 2019) * qt: fix crash during 2FA wallet creation (#5334) * fix synchronizer not to keep resubscribing to addresses of already closed wallets (e415c0d9) * fix removing addresses/keys from imported wallets (#4481) * kivy: fix crash when aborting 2FA wallet creation (#5333) * kivy: fix rare crash when changing exchange rate settings (#5329) * A few other minor bugfixes and usability improvements. # Release 3.3.5 - (May 9, 2019) * The logging system has been overhauled (#5296). Logs can now also optionally be written to disk, disabled by default. * Fix a bug in synchronizer (#5122) where client could get stuck. Also, show the progress of history sync in the GUI. (#5319) * fix Revealer in Windows and MacOS binaries (#5027) * fiat rate providers: - added CoinGecko.com and CoinCap.io - BitcoinAverage now only provides historical exchange rates for paying customers. Changed default provider to CoinGecko.com (#5188) * hardware wallets: - Ledger: Nano X is now recognized (#5140) - KeepKey: - device was not getting detected using Windows binary (#5165) - support firmware 6.0.0+ (#5205) - Trezor: implemented "seedless" mode (#5118) * Coin Control in Qt: implemented freezing individual UTXOs in addition to freezing addresses (#5152) * TrustedCoin (2FA wallets): - better error messages (#5184) - longer signing timeout (#5221) * Kivy: - fix bug with local transactions (#5156) - allow selecting fiat rate providers without historical data (#5162) * fix CPFP: the fees already paid by the parent were not included in the calculation, so it always overestimated (#5244) * Testnet: there is now a warning when the client is started in testnet mode as there were a number of reports of users getting scammed through social engineering (#5295) * CoinChooser: performance of creating transactions has been improved significantly for large wallets. (d56917f4) * Importing/sweeping WIF keys: stricter checks (#4638, #5290) * Electrum protocol: the client's "user agent" has been changed from "3.3.5" to "electrum/3.3.5". Other libraries connecting to servers can consider not "spoofing" to be Electrum. (#5246) * Several other minor bugfixes and usability improvements. # Release 3.3.4 - (February 13, 2019) * AppImage: we now also distribute self-contained binaries for x86_64 Linux in the form of an AppImage (#5042). The Python interpreter, PyQt5, libsecp256k1, PyCryptodomex, zbar, hidapi/libusb (including hardware wallet libraries) are all bundled. Note that users of hw wallets still need to set udev rules themselves. * hw wallets: fix a regression during transaction signing that prompts the user too many times for confirmations (commit 2729909) * transactions now set nVersion to 2, to mimic Bitcoin Core * fix Qt bug that made all hw wallets unusable on Windows 8.1 (#4960) * fix bugs in wallet creation wizard that resulted in corrupted wallets being created in rare cases (#5082, #5057) * fix compatibility with Qt 5.12 (#5109) # Release 3.3.3 - (January 25, 2019) * Do not expose users to server error messages (#4968) * Notify users of new releases. Release announcements must be signed, and they are verified byElectrum using a hardcoded Bitcoin address. * Hardware wallet fixes (#4991, #4993, #5006) * Display only QR code in QRcode Window * Fixed code signing on MacOS * Randomise locktime of transactions # Release 3.3.2 - (December 21, 2018) * Fix Qt history export bug * Improve network timeouts * Prepend server transaction_broadcast error messages with explanatory message. Render error messages as plain text. # Release 3.3.1 - (December 20, 2018) * Qt: Fix invoices tab crash (#4941) * Android: Minor GUI improvements # Release 3.3.0 - Hodler's Edition (December 19, 2018) * The network layer has been rewritten using asyncio and aiorpcx. In addition to easier maintenance, this makes the client more robust against misbehaving servers. * The minimum python version was increased to 3.6 * The blockchain headers and fork handling logic has been generalized. Clients by default now follow chain based on most work, not length. * New wallet creation defaults to native segwit (bech32). * Segwit 2FA: TrustedCoin now supports native segwit p2wsh two-factor wallets. * RBF batching (opt-in): If the wallet has an unconfirmed RBF transaction, new payments will be added to that transaction, instead of creating new transactions. * MacOS: support QR code scanner in binaries. * Android APK: - build using Google NDK instead of Crystax NDK - target API 28 - do not use external storage (previously for block headers) * hardware wallets: - Coldcard now supports spending from p2wpkh-p2sh, fixed p2pkh signing for fw 1.1.0 - Archos Safe-T mini: fix #4726 signing issue - KeepKey: full segwit support - Trezor: refactoring and compat with python-trezor 0.11 - Digital BitBox: support firmware v5.0.0 * fix bitcoin URI handling when app already running (#4796) * Qt listings rewritten: the History tab now uses QAbstractItemModel, the other tabs use QStandardItemModel. Performance should be better for large wallets. * Several other minor bugfixes and usability improvements. # Release 3.2.4 - (December 30, 2018) * backport anti-phishing measures from master # Release 3.2.3 - (September 3, 2018) * hardware wallet: the Safe-T mini from Archos is now supported. * hardware wallet: the Coldcard from Coinkite is now supported. * BIP39 seeds: if a seed extension (aka passphrase) contained multiple consecutive whitespaces or leading/trailing whitespaces then the derived addresses were not following spec. This has been fixed, and affected should move their coins. The wizard will show a warning in this case. (#4566) * Revealer: the PRNG used has been changed (#4649) * fix Linux distributables: 'typing' was not bundled, needed for python 3.4 * fix #4626: fix spending from segwit multisig wallets involving a Trezor cosigner when using a custom derivation path * fix #4491: on Android, if user had set "uBTC" as base unit, app crashed * fix #4497: on Android, paying bip70 invoices from cold start did not work * Several other minor bugfixes and usability improvements. # Release 3.2.2 - (July 2nd, 2018) * Fix DNS resolution on Windows * Fix websocket bug in daemon # Release 3.2.1 - (July 1st, 2018) * fix Windows binaries: due to build process changes, the locale files were not included; the language could not be changed from English * fix Linux distributables: wordlists were not included (#4475) # Release 3.2.0 - Satoshi's Vision (June 30, 2018) * If present, libsecp256k1 is used to speed up elliptic curve operations. The library is bundled in the Windows, MacOS, and Android binaries. On Linux, it needs to be installed separately. * Two-factor authentication is available on Android. Note that this will only provide additional security if one time passwords are generated on a separate device. * Semi-automated crash reporting is implemented for Android. * Transactions that are dropped from the mempool are kept in the wallet as 'local', and can be rebroadcast. Previously these transactions were deleted from the wallet. * The scriptSig and witness part of transaction inputs are no longer parsed, unless actually needed. The wallet will no longer display 'from' addresses corresponding to transaction inputs, except for its own inputs. * The partial transaction format has been incompatibly changed. This was needed as for partial transactions the scriptSig/witness has to be parsed, but for signed transactions we did not want to do the parsing. Users should make sure that all instances of Electrum they use to co-sign or offline sign, are updated together. * Signing of partial transactions created with online imported addresses wallets now supports significantly more setups. Previously only online p2pkh address + offline WIF was supported. Now the following setups are all supported: - online {p2pkh, p2wpkh-p2sh, p2wpkh} address + offline WIF, - online {p2pkh, p2wpkh-p2sh, p2wpkh} address + offline seed/xprv, - online {p2sh, p2wsh-p2sh, p2wsh}-multisig address + offline seeds/xprvs (potentially distributed among several different machines) Note that for the online address + offline HD secret case, you need the offline wallet to recognize the address (i.e. within gap limit). Having an xpub on the online machine is still the recommended setup, as this allows the online machine to generate new addresses on demand. * Segwit multisig for bip39 and hardware wallets is now enabled. (both p2wsh-p2sh and native p2wsh) * Ledger: offline signing for segwit inputs (#3302) This has already worked for Trezor and Digital Bitbox. Offline segwit signing can be combined with online imported addresses wallets. * Added Revealer plugin. ( https://revealer.cc ) Revealer is a seed phrase back-up solution. It allows you to create a cold, analog, multi-factor backup of your wallet seeds, or of any arbitrary secret. The Revealer utilizes a transparent plastic visual one time pad. * Fractional fee rates: the Qt GUI now displays fee rates with 0.1 sat/byte precision, and also allows this same resolution in the Send tab. * Hardware wallets: a "show address" button is now displayed in the Receive tab of the Qt GUI. (#4316) * Trezor One: implemented advanced/matrix recovery (#4329) * Qt/Kivy: added "sat" as optional base unit. * Kivy GUI: significant performance improvements when displaying history and address list of large wallets; and transaction dialog of large transactions. * Windows: use dnspython to resolve dns instead of socket.getaddrinfo (#4422) * Importing minikeys: use uncompressed pubkey instead of compressed (#4384) * SPV proofs: check inner nodes not to be valid transactions (#4436) * Qt GUI: there is now an optional "dark" theme (#4461) * Several other minor bugfixes and usability improvements. # Release 3.1.3 - (April 16, 2018) * Qt GUI: seed word auto-complete during restore * Android: fix some crashes * performance improvements (wallet, and Qt GUI) * hardware wallets: show debug message during device scan * Digital Bitbox: enabled BIP84 (p2wpkh) wallet creation * add regtest support (via --regtest flag) * other minor bugfixes and usability improvements # Release 3.1.2 - (March 28, 2018) * Kivy/android: request PIN on startup * Improve OSX build process * Fix various bugs with hardware wallets * Other minor bugfixes # Release 3.1.1 - (March 12, 2018) * fix #4031: Trezor T support * partial fix #4060: proxy and hardware wallet can't be used together * fix #4039: can't set address labels * fix crash related to coinbase transactions * MacOS: use internal graphics card * fix openalias related crashes * speed-up capital gains calculations * hw wallet encryption: re-prompt for passphrase if incorrect * other minor fixes. # Release 3.1.0 - (March 5, 2018) * Memory-pool based fee estimation. Dynamic fees can target a desired depth in the memory pool. This feature is optional, and ETA-based estimates from Bitcoin Core are still available. Note that miners could exploit this feature, if they conspired and filled the memory pool with expensive transactions that never get mined. However, since the Electrum client already trusts an Electrum server with fee estimates, activating this feature does not introduce any new vulnerability. In addition, the client uses a hard threshold to protect itself from servers sending excessive fee estimates. In practice, ETA-based estimates have resulted in sticky fees, and caused many users to overpay for transactions. Advanced users tend to visit (and trust) websites that display memory-pool data in order to set their fees. * Capital gains: For each outgoing transaction, the difference between the acquisition and liquidation prices of outgoing coins is displayed in the wallet history. By default, historical exchange rates are used to compute acquisition and liquidation prices. These values can also be entered manually, in order to match the actual price realized by the user. The order of liquidation of coins is the natural order defined by the blockchain; this results in capital gain values that are invariant to changes in the set of addresses that are in the wallet. Any other ordering strategy (such as FIFO, LIFO) would result in capital gain values that depend on the presence of other addresses in the wallet. * Local transactions: Transactions can be saved in the wallet without being broadcast. The inputs of local transactions are considered as spent, and their change outputs can be re-used in subsequent transactions. This can be combined with cold storage, in order to create several transactions before broadcasting them. Outgoing transactions that have been removed from the memory pool are also saved in the wallet, and can be broadcast again. * Checkpoints: The initial download of a headers file was replaced with hardcoded checkpoints. The wallet uses one checkpoint per retargeting period. The headers for a retargeting period are downloaded only if transactions need to be verified in this period. * The 'privacy' and 'priority' coin selection policies have been merged into one. Previously, the 'privacy' policy has been unusable because it was was not prioritizing confirmed coins. The new policy is similar to 'privacy', except that it de-prioritizes addresses that have unconfirmed coins. * The 'Send' tab of the Qt GUI displays how transaction fees are computed from transaction size. * The wallet history can be filtered by time interval. * Replace-by-fee is enabled by default. Note that this might cause some issues with wallets that do not display RBF transactions until they are confirmed. * Watching-only wallets and hardware wallets can be encrypted. * Semi-automated crash reporting * The SSL checkbox option was removed from the GUI. * The Trezor T hardware wallet is now supported. * BIP84: native segwit p2wpkh scripts for bip39 seeds and hardware wallets can now be created when specifying a BIP84 derivation path. This is usable with Trezor and Ledger. * Windows: the binaries now include ZBar, and QR code scanning should work. * The Wallet Import Format (WIF) for private keys that was extended in 3.0 is changed. Keys in the previous format can be imported, compatibility is maintained. Newly exported keys will be serialized as "script_type:original_wif_format_key". * BIP32 master keys for testnet once again have different version bytes than on mainnet. For the mainnet prefixes {x,y,Y,z,Z}|{pub,prv}, the corresponding testnet prefixes are {t,u,U,v,V}|{pub,prv}. More details and exact version bytes are specified at: https://github.com/spesmilo/electrum-docs/blob/master/xpub_version_bytes.rst Note that due to this change, testnet wallet files created with previous versions of Electrum must be considered broken, and they need to be recreated from seed words. * A new version of the Electrum protocol is required by the client (version 1.2). Servers using older versions of the protocol will not be displayed in the GUI. # Release 3.0.6 : * Fix transaction parsing bug #3788 # Release 3.0.5 : (Security update) This is a follow-up to the 3.0.4 release, which did not completely fix issue #3374. Users should upgrade to 3.0.5. * The JSONRPC interface is password protected * JSONRPC commands are disabled if the GUI is running, except 'ping', which is used to determine if a GUI is already running # Release 3.0.4 : (Security update) * Fix a vulnerability caused by Cross-Origin Resource Sharing (CORS) in the JSONRPC interface. Previous versions of Electrum are vulnerable to port scanning and deanonimization attacks from malicious websites. Wallets that are not password-protected are vulnerable to theft. * Bundle QR scanner with Android app * Minor bug fixes # Release 3.0.3 * Qt GUI: sweeping now uses the Send tab, allowing fees to be set * Windows: if using the installer binary, there is now a separate shortcut for "Electrum Testnet" * Digital Bitbox: added support for p2sh-segwit * OS notifications for incoming transactions * better transaction size estimation: - fees for segwit txns were somewhat underestimated (#3347) - some multisig txns were underestimated - handle uncompressed pubkeys * fix #3321: testnet for Windows binaries * fix #3264: Ledger/dbb signing on some platforms * fix #3407: KeepKey sending to p2sh output * other minor fixes and usability improvements # Release 3.0.2 * Android: replace requests tab with address tab, with access to private keys * sweeping minikeys: search for both compressed and uncompressed pubkeys * fix wizard crash when attempting to reset Google Authenticator * fix #3248: fix Ledger+segwit signing * fix #3262: fix SSL payment request signing * other minor fixes. # Release 3.0.1 * minor bug and usability fixes # Release 3.0 - Uncanny Valley (November 1st, 2017) * The project was migrated to Python3 and Qt5. Python2 is no longer supported. If you cloned the source repository, you will need to run "python3 setup.py install" in order to install the new dependencies. * Segwit support: - Native segwit scripts are supported using a new type of seed. The version number for segwit seeds is 0x100. The install wizard will not create segwit seeds by default; users must opt-in with the segwit option. - Native segwit scripts are represented using bech32 addresses, following BIP173. Please note that BIP173 is still in draft status, and that other wallets/websites may not support it. Thus, you should keep a non-segwit wallet in order to be able to receive bitcoins during the transition period. If BIP173 ends up being rejected or substantially modified, your wallet may have to be restored from seed. This will not affect funds sent to bech32 addresses, and it will not affect the capacity of Electrum to spend these funds. - Segwit scripts embedded in p2sh are supported with hardware wallets or bip39 seeds. To create a segwit-in-p2sh wallet, trezor/ledger users will need to enter a BIP49 derivation path. - The BIP32 master keys of segwit wallets are serialized using new version numbers. The new version numbers encode the script type, and they result in the following prefixes: * xpub/xprv : p2pkh or p2sh * ypub/yprv : p2wpkh-in-p2sh * Ypub/Yprv : p2wsh-in-p2sh * zpub/zprv : p2wpkh * Zpub/Zprv : p2wsh These values are identical for mainnet and testnet; tpub/tprv prefixes are no longer used in testnet wallets. - The Wallet Import Format (WIF) is similarly extended for segwit scripts. After a base58-encoded key is decoded to binary, its first byte encodes the script type: * 128 + 0: p2pkh * 128 + 1: p2wpkh * 128 + 2: p2wpkh-in-p2sh * 128 + 5: p2sh * 128 + 6: p2wsh * 128 + 7: p2wsh-in-p2sh The distinction between p2sh and p2pkh in private key means that it is not possible to import a p2sh private key and associate it to a p2pkh address. * A new version of the Electrum protocol is required by the client (version 1.1). Servers using older versions of the protocol will not be displayed in the GUI. * By default, transactions are time-locked to the height of the current block. Other values of locktime may be passed using the command line. # Release 2.9.4 (security update) * Backport security fixes from 3.0.5 after vulnerability was discovered in JSONRPC interface. # Release 2.9.3 * fix configuration file issue #2719 * fix ledger signing of non-RBF transactions * disable 'spend confirmed only' option by default # Release 2.9.2 * force headers download if headers file is corrupted * add websocket to windows builds # Release 2.9.1 * fix initial headers download * validate contacts on import * command-line option for locktime # Release 2.9 - Independence (July 27th, 2017) * Multiple Chain Validation: Electrum will download and validate block headers sent by servers that may follow different branches of a fork in the Bitcoin blockchain. Instead of a linear sequence, block headers are organized in a tree structure. Branching points are located efficiently using binary search. The purpose of MCV is to detect and handle blockchain forks that are invisible to the classical SPV model. * The desired branch of a blockchain fork can be selected using the network dialog. Branches are identified by the hash and height of the diverging block. Coin splitting is possible using RBF transaction (a tutorial will be added). * Multibit support: If the user enters a BIP39 seed (or uses a hardware wallet), the full derivation path is configurable in the install wizard. * Option to send only confirmed coins * Qt GUI: - Network dialog uses tabs and gets updated by network events. - The gui tabs use icons * Kivy GUI: - separation between network dialog and wallet settings dialog. - option for manual server entry - proxy configuration * Daemon: The wallet password can be passed as parameter to the JSONRPC API. * Various other bugfixes and improvements. # Release 2.8.3 * Fix crash on reading older wallet formats. * TrustedCoin: remove pay-per-tx option # Release 2.8.2 * show paid invoices in history tab * improve CPFP dialog * fixes for trezor, keepkey * other minor bugfixes # Release 2.8.1 * fix Digital Bitbox plugin * fix daemon jsonrpc * fix trustedcoin wallet creation * other minor bugfixes # Release 2.8.0 (March 9, 2017) * Wallet file encryption using ECIES: A keypair is derived from the wallet password. Once the wallet is decrypted, only the public key is retained in memory, in order to save the encrypted file. * The daemon requires wallets to be explicitly loaded before commands can use them. Wallets can be loaded using: 'electrum daemon load_wallet [-w path]'. This command will require a password if the wallet is encrypted. * Invoices and contacts are stored in the wallet file and are no longer shared between wallets. Previously created invoices and contacts files may be imported from the menu. * Fees improvements: - Dynamic fees are enabled by default. - Child Pays For Parent (CPFP) dialog in the GUI. - RBF is automatically proposed for low fee transactions. * Support for Segregated Witness (testnet only). * Support for Digital Bitbox hardware wallet. * The GUI shows a blue icon when connected using a proxy. # Release 2.7.18 * enforce https on exchange rate APIs * use hardcoded list of exchanges * move 'Freeze' menu to Coins (utxo) tab * various bugfixes # Release 2.7.17 * fix a few minor regressions in the Qt GUI # Release 2.7.16 * add Testnet support (fix #541) * allow daemon to be launched in the foreground (fix #1873) * Qt: use separate tabs for addresses and UTXOs * Qt: update fee slider with a network callback * Ledger: new ui and mobile 2fa validation (neocogent) # Release 2.7.15 * Use fee slider for both static and dynamic fees. * Add fee slider to RBF dialog (fix #2083). * Simplify fee preferences. * Critical: Fix password update issue (#2097). This bug prevents password updates in multisig and 2FA wallets. It may also cause wallet corruption if the wallet contains several master private keys (such as 2FA wallets that have been restored from seed). Affected wallets will need to be restored again. # Release 2.7.14 * Merge exchange_rate plugin with main code * Faster synchronization and transaction creation * Fix bugs #2096, #2016 # Release 2.7.13 * fix message signing with imported keys * add size to transaction details window * move plot plugin to main code * minor bugfixes # Release 2.7.12 various bugfixes # Release 2.7.11 * fix offline signing (issue #195) * fix android crashes caused by threads # Release 2.7.10 * various fixes for hardware wallets * improve fee bumping * separate sign and broadcast buttons in Qt tx dialog * allow spaces in private keys # Release 2.7.9 * Fix a bug with the ordering of pubkeys in recent multisig wallets. Affected wallets will regenerate their public keys when opened for the first time. This bug does not affect address generation. * Fix hardware wallet issues #1975, #1976 # Release 2.7.8 * Fix a bug with fee bumping * Fix crash when parsing request (issue #1969) # Release 2.7.7 * Fix utf8 encoding bug with old wallet seeds (issue #1967) * Fix delete request from menu (issue #1968) # Release 2.7.6 * Fixes a critical bug with imported private keys (issue #1966). Keys imported in Electrum 2.7.x were not encrypted, even if the wallet had a password. If you imported private keys using Electrum 2.7.x, you will need to import those keys again. If you imported keys in 2.6 and converted with 2.7.x, you don't need to do anything, but you still need to upgrade in order to be able to spend. * Wizard: Hide seed options in a popup dialog. # Release 2.7.5 * Add number of confirmations to request status. (issue #1757) * In the GUI, refer to passphrase as 'seed extension'. * Fix bug with utf8 encoded passphrases. * Kivy wizard: add a dialog for seed options. * Kivy wizard: add current word to suggestions, because some users don't see the space key. # Release 2.7.4 * Fix private key import in wizard * Fix Ledger display (issue #1961) * Fix old watching-only wallets (issue #1959) * Fix Android compatibility (issue #1947) # Release 2.7.3 * fix Trezor and Keepkey support in Windows builds * fix sweep private key dialog * minor fixes: #1958, #1959 # Release 2.7.2 * fix bug in password update (issue #1954) * fix fee slider (issue #1953) # Release 2.7.1 * fix wizard crash with old seeds * fix issue #1948: fee slider # Release 2.7.0 (Oct 2 2016) * The wallet file format has been upgraded. This upgrade is not backward compatible, which means that a wallet upgraded to the 2.7 format will not be readable by earlier versions of Electrum. Multiple accounts inside the same wallet are not supported in the new format; the Qt GUI will propose to split any wallet that has several accounts. Make sure that you have saved your seed phrase before you upgrade Electrum. * This version introduces a separation between wallets types and keystores types. 'Wallet type' defines the type of Bitcoin contract used in the wallet, while 'keystore type' refers to the method used to store private keys. Therefore, so-called 'hardware wallets' will be referred to as 'hardware keystores'. * Hardware keystores: - The Ledger Nano S is supported. - Hardware keystores can be used as cosigners in multi-signature wallets. - Multiple hardware cosigners can be used in the same multisig wallet. One icon per keystore is displayed in the satus bar. Each connected device will co-sign the transaction. * Replace-By-Fee: RBF transactions are supported in both Qt and Android. A warning is displayed in the history for transactions that are replaceable, have unconfirmed parents, or that have very low fees. * Dynamic fees: Dynamic fees are enabled by default. A slider allows the user to select the expected confirmation time of their transaction. The expected confirmation times of incoming transactions is also displayed in the history. * The install wizards of Qt and Kivy have been unified. * Qt GUI (Desktop): - A fee slider is visible in the in send tab - The Address tab is hidden by default, can be shown with Ctrl-A - UTXOs are displayed in the Address tab * Kivy GUI (Android): - The GUI displays the complete transaction history. - Multisig wallets are supported. - Wallets can be created and deleted in the GUI. * Seed phrases can be extended with a user-chosen passphrase. The length of seed phrases is standardized to 12 words, using 132 bits of entropy (including 2FA seeds). In the wizard, the type of the seed is displayed in the seed input dialog. * TrustedCoin users can request a reset of their Google Authenticator account, if they still have their seed. # Release 2.6.4 (bugfixes) * fix coinchooser bug (#1703) * fix daemon JSONRPC (#1731) * fix command-line broadcast (#1728) * QT: add colors to labels # Release 2.6.3 (bugfixes) * fix command line parsing of transactions * fix signtransaction --privkey (#1715) # Release 2.6.2 (bugfixes) * fix Trustedcoin restore from seed (bug #1704) * small improvements to kivy GUI # Release 2.6.1 (bugfixes) * fix broadcast command (bug #1688) * fix tx dialog (bug #1690) * kivy: support old-type seed phrases in wizard # Release 2.6 * The source code is relicensed under the MIT Licence * First official release of the Kivy GUI, with android APK * The old 'android' and 'gtk' GUIs are deprecated * Separation between plugins and GUIs * The command line uses jsonrpc to communicate with the daemon * New command: 'notify
' * Alternative coin selection policy, designed to help preserve user privacy. Enable it by setting the Coin Selection preference to Privacy. * The install wizard has been rewritten and improved * Support minikeys as used in Casascius coins for private key import and sweeping * Much improved support for TREZOR and KeepKey devices: - full device information display - initialize a new or wiped device in 4 ways: 1) device generates a new wallet 2) you enter a seed 3) you enter a BIP39 mnemonic to generate the seed 4) you enter a master private key - KeepKey secure seed recovery (KeepKey only) - change / set / disable PIN - set homescreen (TREZOR only) - set a session timeout. Once a session has timed out, further use of the device requires your PIN and passhphrase to be re-entered - enable / disable passphrases - device wipe - multiple device support # Release 2.5.4 * increase MIN_RELAY_TX_FEE to avoid dust transactions # Release 2.5.3 (bugfixes) * installwizard: do not allow direct copy-paste of the seed * installwizard: fix bug #1531 (starting offline) # Release 2.5.2 (bugfixes) * fix bug #1513 (client tries to broadcast transaction while not connected) * fix synchronization bug (#1520) * fix command line bug (#1494) * fixes for exchange rate plugin # Release 2.5.1 (bugfixes) * signatures in transactions were still using the old class * make sure that setup.py uses python2 * fix wizard crash with trustedcoin plugin * fix socket infinite loop * fix history bug #1479 # Release 2.5 * Low-S values are used in signatures (BIP 62). * The Kivy GUI has been merged into master. * The Qt GUI supports multiple windows in the same process. When a new Electrum instance is started, it checks for an already running Electrum process, and connects to it. * The network layer uses select(), so all server communication is handled by a single thread. Moreover, the synchronizer, verifier, and exchange rate plugin now run as separate jobs within the networking thread instead of as their own threads. * Plugins are revamped, particularly the exchange rate plugin. # Release 2.4.4 * Fix bug with TrustedCoin plugin # Release 2.4.3 * Support for KeepKey hardware wallet * Simplified Chinese wordlist * Minor bugfixes and GUI tweaks # Release 2.4.2 * Command line can read arguments from stdin (pipe) * Speedup fee computation for large transactions * Various bugfixes # Release 2.4.1 * Use ssl.PROTOCOL_TLSv1 * Fix DNSSEC issues with ECDSA signatures * Replace TLSLite dependency with minimal RSA implementation * Dynamic Fees: using estimatefee value returned by server * Various GUI improvements # Release 2.4 * Payment to DNS names storing a Bitcoin addresses (OpenAlias) is supported directly, without activating a plugin. The verification uses DNSSEC. * The DNSSEC verification code was rewritten. The previous code, which was part of the OpenAlias plugin, is vulnerable and should not be trusted (Electrum 2.0 to 2.3). * Payment requests can be signed using Bitcoin addresses stored in DNS (OpenAlias). The identity of the requestor is verified using DNSSEC. * Payment requests signed with OpenAlias keys can be shared as bitcoin: URIs, if they are simple (a single address-type output). The BIP21 URI scheme is extended with 'name', 'sig', 'time', 'exp'. * Arbitrary m-of-n multisig wallets are supported (n<=15). * Multisig transactions can be signed with TREZOR. When you create the multisig wallet, just enter the xpub of your existing TREZOR wallet. * Transaction fees set manually in the GUI are retained, including when the user uses the '!' shortcut. * New 'email' plugin, that enables sending and receiving payment requests by email. * The daemon supports Websocket notifications of payments. # Release 2.3.3 * fix proxy settings (issue #1309) * improvements to the transaction dialog: - request password after showing transaction - show change addresses in yellow color # Release 2.3.2 * minor bugfixes * updated ledger plugin * sort inputs/outputs lexicographically (BIP-LI01) # Release 2.3.1 * patch a bug with payment requests # Release 2.3 * Improved logic for the network layer. * More efficient coin selection. Spend oldest coins first, and minimize the number of transaction inputs. * Plugins are loaded independently of the GUI. As a result, Openalias, TrustedCoin and TREZOR wallets can be used with the command line. Example: 'electrum payto ' * The command line has been refactored: - Arguments are parsed with argparse. - The inline help includes a description of options. - Some commands have been renamed. Notably, 'mktx' and 'payto' have been merged into a single command, with a --broadcast option. Type 'electrum --help' for a complete overview. * The command line accepts the '!' syntax to send the maximum amount available. It can be combined with the '--from' option. Example: 'payto ! --from ' * The command line also accepts a '?' shortcut for private keys arguments, that triggers a prompt. * Payment requests can be managed with the command line, using the following commands: 'addrequest', 'rmrequest', 'listrequests'. Payment requests can be signed with a SSL certificate, and published as bip70 files in a public web directory. To see the relevant configuration variables, type 'electrum addrequest --help' * Commands can be called with jsonrpc, using the 'jsonrpc' gui. The jsonrpc interface may be called by php. # Release 2.2 * Show amounts (thousands separators and decimal point) according to locale in GUI * Show unmatured coins in balance * Fix exchange rates plugin * Network layer: refactoring and fixes # Release 2.1.1 * patch a bug that prevents new wallet creation. * fix connection issue on osx binaries # Release 2.1 * Faster startup, thanks to the following optimizations: 1. Transaction input/outputs are cached in the wallet file 2. Fast X509 certificate parser, not using pyasn1 anymore. 3. The Label Sync plugin only requests modified labels. * The 'Invoices' and 'Send' tabs have been merged. * Contacts are stored in a separate file, shared between wallets. * A Search Box is available in the GUI (Ctrl-S) * Payment requests have an expiration date and can be exported to BIP70 files. * file: scheme support in BIP72 URIs: "bitcoin:?r=file:///..." * Own addresses are shown in green in the Transaction dialog. * Address History dialog. * The OpenAlias plugin was improved. * Various bug fixes and GUI improvements. * A new LabelSync backend is being used an import of the old database was made but since the release came later it's recommended that you do a full push when you upgrade. # Release 2.0.4 - Minor GUI improvements * The password dialog will ask for password again if the user enters a wrong password * The Master Public Key dialog displays which keys belong to the wallet, and which are cosigners * The transaction dialog will ask to save unsaved transaction received from cosigner pool, when user clicks on 'Close' * The multisig restore dialog accepts xprv keys. * The network daemon must be started explicitly before using commands that require a connection Example: electrum daemon start electrum getaddressunspent electrum daemon status electrum daemon stop If a daemon is running, the GUI will use it. # Release 2.0.3 - bugfixes and minor GUI improvements * Do not use daemon threads (fix #960) * Add a zoom button to receive tab * Add exchange rate conversion to receive tab * Use Tor's default port number in default proxy config # Release 2.0.2 - bugfixes * Fix transaction sweep (#1066) * Fix thread timing bug (#1054) # Release 2.0.1 - bugfixes * Fix critical bug in TREZOR address derivation: passphrases were not NFKD normalized. TREZOR users who created a wallet protected by a passphrase containing utf-8 characters with diacritics are affected. These users will have to open their wallet with version 2.0 and to move their funds to a new wallet. * Use a file socket for the daemon (fixes network dialog issues) * Fix crash caused by QR scanner icon when zbar not installed. * Fix CosignerPool plugin * Label Sync plugin: Fix label sharing between multisig wallets # Release 2.0 * Before you upgrade, make sure you have saved your wallet seed on paper. * Documentation is now hosted on a wiki: http://electrum.orain.org * New seed derivation method (not compatible with BIP39). The seed phrase includes a version number, that refers to the wallet structure. The version number also serves as a checksum, and it will prevent the import of seeds from incompatible wallets. Old Electrum seeds are still supported. * New address derivation (BIP32). Standard wallets are single account and use a gap limit of 20. * Support for Multisig wallets using parallel BIP32 derivations and P2SH addresses ("2 of 2", "2 of 3"). * Compact serialization format for unsigned or partially signed transactions, that includes the BIP32 master public key and derivation needed to sign inputs. Serialized transactions can be sent to cosigners or to cold storage using QR codes (using Andreas Schildbach's base 43 idea). * Support for BIP70 payment requests: - Verification of the chain of signatures uses tlslite. - In the GUI, payment requests are shown in the 'Invoices' tab. * Support for hardware wallets: TREZOR (SatoshiLabs) and Btchip (Ledger). * Two-factor authentication service by TrustedCoin. This service uses "2 of 3" multisig wallets and Google Authenticator. Note that wallets protected by this service can be deterministically restored from seed, without Trustedcoin's server. * Cosigner Pool plugin: encrypted communication channel for multisig wallets, to send and receive partially signed transactions. * Audio Modem plugin: send and receive transactions by sound. * OpenAlias plugin: send bitcoins to aliases verified using DNSSEC. * New 'Receive' tab in the GUI: - create and manage payment requests, with QR Codes - the former 'Receive' tab was renamed to 'Addresses' - the former Point of Sale plugin is replaced by a resizable window that pops up if you click on the QR code * The 'Send' tab in the Qt GUI supports transactions with multiple outputs, and raw hexadecimal scripts. * The GUI can connect to the Electrum daemon: "electrum -d" will start the daemon if it is not already running, and the GUI will connect to it. The daemon can serve several clients. It times out if no client uses if for more than 5 minutes. * The install wizard can be used to import addresses or private keys. A watching-only wallet is created by entering a list of addresses in the wizard dialog. * New file format: Wallets files are saved as JSON. Note that new wallet files cannot be read by older versions of Electrum. Old wallet files will be converted to the new format; this operation may take some time, because public keys will be derived for each address of your wallet. * The client accepts servers with a CA-signed SSL certificate. * ECIES encrypt/decrypt methods, available in the GUI and using the command line: encrypt decrypt * The Android GUI has received various updates and it is much more stable. Another script was added to Android, called Authenticator, that works completely offline: it reads an unsigned transaction shown as QR code, signs it and shows the result as a QR code. # Release 1.9.8 * Electrum servers were upgraded to version 0.9. The new server stores a Patrica tree of all UTXOs, an idea proposed by Alan Reiner in the bitcointalk forum. This property allows the client to directly request the balance of any address. The new commands are: 1. getaddressbalance
2. getaddressunspent
3. getutxoaddress * Command-line commands that require a connection to the network spawn a daemon, that remains connected and handles subsequent commands. The daemon terminates itself if it remains unused for more than one minute. The purpose of this is to make scripting more efficient. For example, a bash script using many electrum commands will open only one connection. # Release 1.9.7 * Fix for offline signing * Various bugfixes * GUI usability improvements * Coinbase Buyback plugin # Release 1.9.6 * During wallet creation, do not write seed to disk until it is encrypted. * Confirmation dialog if the transaction fee is higher than 1mBTC. * bugfixes # Release 1.9.5 * Coin control: select addresses to send from * Put addresses that have been used in a minimized section (Qt GUI) * Allow non ascii chars in passwords # Release 1.9.4 bugfixes: offline transactions # Release 1.9.3 bugfixes: connection problems, transactions staying unverified # Release 1.9.2 * fix a syntax error # Release 1.9.1 * fix regression with --offline mode * fix regression with --portable mode: use a dedicated directory # Release 1.9 * The client connects to multiple servers in order to retrieve block headers and find the longest chain * SSL certificate validation (to prevent MITM) * Deterministic signatures (RFC 6979) * Menu to create/restore/open wallets * Create transactions with multiple outputs from CSV (comma separated values) * New text gui: stdio * Plugins are no longer tied to the qt GUI, they can reach all GUIs * Proxy bugs have been fixed # Release 1.8.1 * Notification option when receiving new transactions * Confirm dialogue before sending large amounts * Alternative datafile location for non-windows systems * Fix offline wallet creation * Remove enforced tx fee * Tray icon improvements * Various bugfixes # Release 1.8 * Menubar in classic gui * Updated the QR Code plugin to enable offline/online wallets to transmit unsigned/signed transactions via QR code. * Fixed bug where never-confirmed transactions prevented further spending # Release 1.7.4 * Increase default fee * fix create and restore in command line * fix verify message in the gui # Release 1.7.3: * Classic GUI can display amounts in mBTC * Account selector in the classic GUI * Changed the way the portable flag uses without supplying a -w argument * Classic GUI asks users to enter their seed on wallet creation # Release 1.7.2: * Transactions that are in the same block are displayed in chronological order in the history. * The client computes transaction priority and rejects zero-fee transactions that need a fee. * The default fee was lowered to 200 uBTC per kb. * Due to an internal format change, your history may be pruned when you open your wallet for the first time after upgrading to 1.7.2. If this is the case, please visit a full server to restore your full history. You will only need to do that once. # Release 1.7.1: bugfixes. # Release 1.7 * The Classic GUI can be extended with plugins. Developers who want to add new features or third-party services to Electrum are invited to write plugins. Some previously existing and non-essential features of Electrum (point-of-sale mode, qrcode scanner) were removed from the core and are now available as plugins. * The wallet waits for 2 confirmations before creating new addresses. This makes recovery from seed more robust. Note that it might create unwanted gaps if you use Electrum 1.7 together with older versions of Electrum. * An interactive Python console replaces the 'Wall' tab. The provided python environment gives users access to the wallet and gui. Most electrum commands are available as python function in the console. Custom scripts an be loaded with a "run(filename)" command. Tab-completions are available. * The location of the Electrum folder in Windows changed from LOCALAPPDATA to APPDATA. Discussion on this topic can be found here: https://bitcointalk.org/index.php?topic=144575.0 * Private keys can be exported from within the classic GUI: For a single address, use the address menu (right-click). To export the keys of your entire wallet, use the settings dialog (import/export tab). * It is possible to create, sign and redeem multisig transaction using the command line interface. This is made possible by the following new commands: dumpprivkey, listunspent, createmultisig, createrawtransaction, decoderawtransaction, signrawtransaction The syntax of these commands is similar to their bitcoind counterpart. For an example, see Gavin's tutorial: https://gist.github.com/gavinandresen/3966071 * Offline wallets now work in a way similar to Armory: 1. user creates an unsigned transaction using the online (watching-only) wallet. 2. unsigned transaction is copied to the offline computer, and signed by the offline wallet. 3. signed transaction is copied to the online computer, broadcasted by the online client. 4. All these steps can be done via the command line interface or the classic GUI. * Many command line commands have been renamed in order to make the syntax consistent with bitcoind. # Release 1.6.2 == Classic GUI * Added new version notification # Release 1.6.1 (11-01-2013) == Core * It is now possible to restore a wallet from MPK (this will create a watching-only wallet) * A switch button allows to easily switch between Lite and Classic GUI. == Classic GUI * Seed and MPK help dialogs were rewritten * Point of Sale: requested amounts can be expressed in other currencies and are converted to bitcoin. == Lite GUI * The receiving button was removed in favor of a menu item to keep it consistent with the history toggle. # Release 1.6.0 (07-01-2013) == Core * (Feature) Add support for importing, signing and verifiying compressed keys * (Feature) Auto reconnect to random server on disconnect * (Feature) Ultimate fallback to HTTP port 80 if TCP doesn't work on any server * (Bug) Under rare circumstances changing password with incorrect password could damage wallet == Lite GUI * (Chore) Use blockchain.info for exchange rate data * (Feature) added currency conversion for BRL, CNY, RUB * (Feature) Saraha theme * (Feature) csv import/export for transactions including labels == Classic GUI * (Chore) pruning servers now called "p", full servers "f" to avoid confusion with terms * (Feature) Debits in history shown in red * (Feature) csv import/export for transactions including labels # Release 1.5.8 (02-01-2013) == Core * (Bug) Fix pending address balance on received coins for pruning servers * (Bug) Fix history command line option to show output again (regression by SPV) * (Chore) Add timeout to blockchain headers file download by HTTP * (Feature) new option: -L, --language: default language used in GUI. == Lite GUI * (Bug) Sending to auto-completed contacts works again * (Chore) Added version number to title bar == Classic GUI * (Feature) Language selector in options. # Release 1.5.7 (18-12-2012) == Core * The blockchain headers file is no longer included in the packages, it is downloaded on startup. * New command line option: -P or --portable, for portable wallets. With this flag, all preferences are saved to the wallet file, and the blockchain headers file is in the same directory as the wallet == Lite GUI * (Feature) Added the ability to export your transactions to a CSV file. * (Feature) Added a label dialog after sending a transaction. * (Feature) Reworked receiving addresses; instead of a random selection from one of your receiving addresses a new widget will show listing unused addresses. * (Chore) Removed server selection. With all the new server options a simple menu item does not suffice anymore. ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Reporting a Vulnerability To report security issues, send an email to the addresses listed below. (Not for support. Support requests will be *ignored*.) Please send any report to *all* emails listed here. The following GPG keys may be used to communicate sensitive information. | Name | Email | GPG fingerprint | |-------------|----------------------------------------|---------------------------------------------------| | ThomasV | thomasv [AT] electrum [DOT] org | 6694 D8DE 7BE8 EE56 31BE D950 2BD5 824B 7F94 70E6 | | SomberNight | somber.night [AT] protonmail [DOT] com | 4AD6 4339 DFA0 5E20 B3F6 AD51 E7B7 48CD AF5E 5ED9 | #### Where to find GPG keys You can import a key by running the following command with that individual’s fingerprint: `gpg --recv-keys ""` These public keys can also be found in the Electrum git repository, in the top-level `pubkeys` folder. ================================================ FILE: contrib/add_cosigner ================================================ #!/usr/bin/python3 # # This script is part of the workflow for BUILDERs to reproduce and sign the # release binaries. (for builders who do not have sftp access to "electrum-downloads-airlock") # # env vars: # - SSHUSER # # # - BUILDER builds all binaries and checks they match the official releases # (using release.sh, and perhaps some manual steps) # - BUILDER creates a PR against https://github.com/spesmilo/electrum-signatures/ # to add their sigs for a given release, which then gets merged # - SFTPUSER runs `$ SSHUSER=$SFTPUSER electrum/contrib/add_cosigner $BUILDER` # - SFTPUSER runs `$ electrum/contrib/make_download $WWW_DIR` # - $ (cd $WWW_DIR; git commit -a -m "add_cosigner"; git push) # - SFTPUSER runs `$ electrum-web/publish.sh $SFTPUSER` # - (for the website to be updated, both ThomasV and SomberNight needs to run publish.sh) import re import os import sys import importlib import importlib.util import subprocess if len(sys.argv) < 2: print(f"usage: {os.path.basename(__file__)} ", file=sys.stderr) sys.exit(1) # cd to project root os.chdir(os.path.dirname(os.path.dirname(__file__))) # load version.py; needlessly complicated alternative to "imp.load_source": version_spec = importlib.util.spec_from_file_location('version', 'electrum/version.py') version_module = importlib.util.module_from_spec(version_spec) version_spec.loader.exec_module(version_module) ELECTRUM_VERSION = version_module.ELECTRUM_VERSION print("version", ELECTRUM_VERSION) # GPG name of cosigner cosigner = sys.argv[1] version = version_win = version_mac = version_android = ELECTRUM_VERSION files = { "tgz": f"Electrum-{version}.tar.gz", "tgz_srconly": f"Electrum-sourceonly-{version}.tar.gz", "appimage": f"electrum-{version}-x86_64.AppImage", "mac": f"electrum-{version_mac}.dmg", "win": f"electrum-{version_win}.exe", "win_setup": f"electrum-{version_win}-setup.exe", "win_portable": f"electrum-{version_win}-portable.exe", "apk_arm64": f"Electrum-{version_android}-arm64-v8a-release.apk", "apk_armeabi": f"Electrum-{version_android}-armeabi-v7a-release.apk", "apk_x86_64": f"Electrum-{version_android}-x86_64-release.apk", } for shortname, filename in files.items(): path = f"dist/{filename}" link = f"https://download.electrum.org/{version}/{filename}" if not os.path.exists(path): os.system(f"wget -q {link} -O {path}") if not os.path.getsize(path): raise Exception(path) sig_name = f"{filename}.{cosigner}.asc" sig_url = f"https://raw.githubusercontent.com/spesmilo/electrum-signatures/master/{version}/{filename}/{sig_name}" sig_path = f"dist/{sig_name}" os.system(f"wget -nc {sig_url} -O {sig_path}") if os.system(f"gpg --verify {sig_path} {path}") != 0: raise Exception(sig_name) print("Calling upload.sh now... This might take some time.") subprocess.check_output(["./contrib/upload.sh", ]) ================================================ FILE: contrib/android/Dockerfile ================================================ # based on https://github.com/kivy/python-for-android/blob/master/Dockerfile FROM debian:trixie@sha256:a3b5f4f0286249a124bfe9845b3aec0f88de32ff31dd8d7e1b945f9f98d116b0 ENV DEBIAN_FRONTEND=noninteractive ENV ANDROID_HOME="/opt/android" # need ca-certificates before using snapshot packages RUN apt update -qq > /dev/null && apt install -qq --yes --no-install-recommends \ ca-certificates # pin the distro packages. COPY contrib/android/apt.sources.list /etc/apt/sources.list COPY contrib/android/apt.preferences /etc/apt/preferences.d/snapshot # configure locale RUN apt update -qq > /dev/null && apt install -qq --yes --no-install-recommends --allow-downgrades \ locales && \ locale-gen en_US.UTF-8 ENV LANG="en_US.UTF-8" \ LANGUAGE="en_US.UTF-8" \ LC_ALL="en_US.UTF-8" RUN apt -y update -qq \ && apt -y install -qq --no-install-recommends --allow-downgrades \ curl \ wget \ unzip \ ca-certificates \ python3 \ && apt -y autoremove ENV ANDROID_NDK_HOME="${ANDROID_HOME}/android-ndk" #ENV ANDROID_NDK_VERSION="23b" #ENV ANDROID_NDK_HASH="c6e97f9c8cfe5b7be0a9e6c15af8e7a179475b7ded23e2d1c1fa0945d6fb4382" #ENV ANDROID_NDK_VERSION="27d" #ENV ANDROID_NDK_HASH="601246087a682d1944e1e16dd85bc6e49560fe8b6d61255be2829178c8ed15d9" ENV ANDROID_NDK_VERSION="23d-canary" ENV ANDROID_NDK_HASH="6944ffc20ab018ff4ef6a403048d0a99d50a0630c3eae690c8f803c452f46f3e" ENV ANDROID_NDK_HOME_V="${ANDROID_NDK_HOME}-r${ANDROID_NDK_VERSION}" # get the latest version from https://developer.android.com/ndk/downloads/index.html ENV ANDROID_NDK_ARCHIVE="android-ndk-r${ANDROID_NDK_VERSION}-linux.zip" ENV ANDROID_NDK_DL_URL="https://dl.google.com/android/repository/${ANDROID_NDK_ARCHIVE}" # below disabled in favor of CI build download # download and install Android NDK #RUN curl --location --progress-bar \ # "${ANDROID_NDK_DL_URL}" \ # --output "${ANDROID_NDK_ARCHIVE}" \ # && echo "${ANDROID_NDK_HASH} ${ANDROID_NDK_ARCHIVE}" | sha256sum -c - \ # && mkdir --parents "${ANDROID_NDK_HOME_V}" \ # && unzip -q "${ANDROID_NDK_ARCHIVE}" -d "${ANDROID_HOME}" \ # && ln -sfn "${ANDROID_NDK_HOME_V}" "${ANDROID_NDK_HOME}" \ # && rm -rf "${ANDROID_NDK_ARCHIVE}" # temporary build using NDK from CI ENV CI_REV="12186248" ENV CI_NDK_FILE="android-ndk-${CI_REV}-linux-x86_64.zip" COPY contrib/android/dl-ndk-ci.sh /tmp/ RUN /tmp/dl-ndk-ci.sh https://ci.android.com/builds/submitted/${CI_REV}/linux/latest/${CI_NDK_FILE} \ && echo "${ANDROID_NDK_HASH} android-ndk-ci-linux-x86_64.zip" | sha256sum -c - \ && mkdir --parents "${ANDROID_NDK_HOME_V}" \ && unzip -q "android-ndk-ci-linux-x86_64.zip" -d "${ANDROID_HOME}" \ && ln -sfn "${ANDROID_NDK_HOME_V}" "${ANDROID_NDK_HOME}" \ && rm -rf "android-ndk-ci-linux-x86_64.zip" ENV ANDROID_SDK_HOME="${ANDROID_HOME}/android-sdk" # get the latest version from https://developer.android.com/studio/index.html ENV ANDROID_SDK_TOOLS_VERSION="9477386" ENV ANDROID_SDK_HASH="bd1aa17c7ef10066949c88dc6c9c8d536be27f992a1f3b5a584f9bd2ba5646a0" ENV ANDROID_SDK_TOOLS_ARCHIVE="commandlinetools-linux-${ANDROID_SDK_TOOLS_VERSION}_latest.zip" ENV ANDROID_SDK_TOOLS_DL_URL="https://dl.google.com/android/repository/${ANDROID_SDK_TOOLS_ARCHIVE}" ENV ANDROID_SDK_MANAGER="${ANDROID_SDK_HOME}/cmdline-tools/bin/sdkmanager --sdk_root=${ANDROID_SDK_HOME}" # download and install Android SDK RUN curl --location --progress-bar \ "${ANDROID_SDK_TOOLS_DL_URL}" \ --output "${ANDROID_SDK_TOOLS_ARCHIVE}" \ && echo "${ANDROID_SDK_HASH} ${ANDROID_SDK_TOOLS_ARCHIVE}" | sha256sum -c - \ && mkdir --parents "${ANDROID_SDK_HOME}" \ && unzip -q "${ANDROID_SDK_TOOLS_ARCHIVE}" -d "${ANDROID_SDK_HOME}" \ && rm -rf "${ANDROID_SDK_TOOLS_ARCHIVE}" # update Android SDK, install Android API, Build Tools... RUN mkdir --parents "${ANDROID_SDK_HOME}/.android/" \ && echo '### User Sources for Android SDK Manager' \ > "${ANDROID_SDK_HOME}/.android/repositories.cfg" # download Java-17 (debian 13 only packages Java-21 and Java-25) # - we download the amd64 binaries from debian 12 repos # - we should try to upgrade to Java-21... # - the main blocker seems to be having to update Gradle (to a version compatible with Java-21) # - make_barcode_scanner.sh: markusfisch/{zxing-cpp, ...} pins old Gradle ENV JAVA_JRE_DL_URL="https://snapshot.debian.org/archive/debian/20260130T143028Z/pool/main/o/openjdk-17/openjdk-17-jre-headless_17.0.18+8-1~deb12u1_amd64.deb" ENV JAVA_JRE_ARCHIVE="openjdk-17-jre-headless.deb" ENV JAVA_JRE_HASH="5bc36cbb4e383dbea4168d57b5fd9b42375ec8837dd62a1d56677632c3c960e0" ENV JAVA_JDK_DL_URL="https://snapshot.debian.org/archive/debian/20260130T143028Z/pool/main/o/openjdk-17/openjdk-17-jdk-headless_17.0.18+8-1~deb12u1_amd64.deb" ENV JAVA_JDK_ARCHIVE="openjdk-17-jdk-headless.deb" ENV JAVA_JDK_HASH="8841044caa66860a71039342fe3c02b7853b61c518e05970e501faa215b1788a" RUN apt -y update -qq \ && apt -y install -qq --no-install-recommends \ ca-certificates-java \ java-common \ libcups2 \ libfontconfig1 \ liblcms2-2 \ libjpeg62-turbo \ libnss3 \ libasound2 \ libfreetype6 \ libharfbuzz0b \ libpcsclite1 \ && apt -y autoremove \ && cd /opt \ && curl --location --progress-bar "${JAVA_JRE_DL_URL}" --output "${JAVA_JRE_ARCHIVE}" \ && echo "${JAVA_JRE_HASH} ${JAVA_JRE_ARCHIVE}" | sha256sum -c - \ && dpkg -i "${JAVA_JRE_ARCHIVE}" \ && rm "${JAVA_JRE_ARCHIVE}" \ && curl --location --progress-bar "${JAVA_JDK_DL_URL}" --output "${JAVA_JDK_ARCHIVE}" \ && echo "${JAVA_JDK_HASH} ${JAVA_JDK_ARCHIVE}" | sha256sum -c - \ && dpkg -i "${JAVA_JDK_ARCHIVE}" \ && rm "${JAVA_JDK_ARCHIVE}" # accept Android licenses (JDK necessary!) RUN yes | ${ANDROID_SDK_MANAGER} --licenses > /dev/null ENV ANDROID_SDK_BUILD_TOOLS_MAJOR_V="31" ENV ANDROID_SDK_BUILD_TOOLS_VERSION="31.0.0" # download platforms, API, build tools RUN ${ANDROID_SDK_MANAGER} "platforms;android-${ANDROID_SDK_BUILD_TOOLS_MAJOR_V}" > /dev/null && \ ${ANDROID_SDK_MANAGER} "build-tools;${ANDROID_SDK_BUILD_TOOLS_VERSION}" > /dev/null && \ ${ANDROID_SDK_MANAGER} "extras;android;m2repository" > /dev/null && \ chmod +x "${ANDROID_SDK_HOME}/cmdline-tools/bin/avdmanager" # download ANT ENV APACHE_ANT_VERSION="1.10.13" ENV APACHE_ANT_HASH="776be4a5704158f00ef3f23c0327546e38159389bc8f39abbfe114913f88bab1" ENV APACHE_ANT_ARCHIVE="apache-ant-${APACHE_ANT_VERSION}-bin.tar.gz" ENV APACHE_ANT_DL_URL="https://archive.apache.org/dist/ant/binaries/${APACHE_ANT_ARCHIVE}" ENV APACHE_ANT_HOME="${ANDROID_HOME}/apache-ant" ENV APACHE_ANT_HOME_V="${APACHE_ANT_HOME}-${APACHE_ANT_VERSION}" RUN curl --location --progress-bar \ "${APACHE_ANT_DL_URL}" \ --output "${APACHE_ANT_ARCHIVE}" \ && echo "${APACHE_ANT_HASH} ${APACHE_ANT_ARCHIVE}" | sha256sum -c - \ && tar -xf "${APACHE_ANT_ARCHIVE}" -C "${ANDROID_HOME}" \ && ln -sfn "${APACHE_ANT_HOME_V}" "${APACHE_ANT_HOME}" \ && rm -rf "${APACHE_ANT_ARCHIVE}" # install system/build dependencies # https://github.com/kivy/buildozer/blob/master/docs/source/installation.rst#android-on-ubuntu-2004-64bit RUN apt -y update -q \ && apt -y install -q --no-install-recommends --allow-downgrades \ wget \ lbzip2 \ patch \ sudo \ git \ zip \ unzip \ rsync \ build-essential \ ccache \ autoconf \ autopoint \ libtool \ pkg-config \ zlib1g-dev \ libncurses-dev \ cmake \ libffi-dev \ libssl-dev \ automake \ gettext \ libltdl-dev \ && apt -y autoremove \ && apt -y clean # cross compile deps for Qt6 RUN apt -y update -qq \ && apt -y install -qq --no-install-recommends --allow-downgrades \ libopengl-dev \ libegl-dev \ dos2unix \ && apt -y autoremove \ && apt -y clean # create new user to avoid using root; but with sudo access and no password for convenience. ARG UID=1000 RUN if [ "$UID" != "0" ] ; then useradd --uid $UID --create-home --shell /bin/bash "user" ; fi RUN usermod -append --groups sudo $(id -nu $UID || echo "user") RUN echo "%sudo ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers RUN HOME_DIR=$(getent passwd $UID | cut -d: -f6) ENV WORK_DIR="${HOME_DIR}/wspace" \ PATH="${HOME_DIR}/.local/bin:${PATH}" WORKDIR ${WORK_DIR} RUN chown --recursive ${UID} ${WORK_DIR} ${ANDROID_SDK_HOME} RUN chown ${UID} /opt USER ${UID} # build cpython. FIXME we can't use the python3 from apt, as it is too new o.O # - p4a and buildozer require cython<3 (see https://github.com/kivy/python-for-android/issues/2919) # but the last such version, cython 0.29.37, can only be built by up to python 3.12 ENV VENV_PYTHON_VERSION="3.12.12" ENV VENV_PY_VER_MAJOR="3.12" ENV VENV_PYTHON_HASH="487c908ddf4097a1b9ba859f25fe46d22ccaabfb335880faac305ac62bffb79b" RUN mkdir --parents "/opt/cpython/download" && cd "/opt/cpython/download" \ && wget "https://www.python.org/ftp/python/${VENV_PYTHON_VERSION}/Python-${VENV_PYTHON_VERSION}.tgz" \ && echo "${VENV_PYTHON_HASH} Python-${VENV_PYTHON_VERSION}.tgz" | sha256sum -c - \ && tar xf "Python-${VENV_PYTHON_VERSION}.tgz" -C "/opt/cpython/download" \ && cd "Python-${VENV_PYTHON_VERSION}" \ && mkdir "/opt/cpython/install" \ && ./configure \ --prefix="/opt/cpython/install" \ -q \ && make "-j$(nproc)" -s \ && make -s altinstall \ && ln -s "/opt/cpython/install/bin/python${VENV_PY_VER_MAJOR}" "/opt/cpython/install/bin/python3" RUN "/opt/cpython/install/bin/python3" -m ensurepip # venv, VIRTUAL_ENV is used by buildozer to indicate a venv environment ENV VIRTUAL_ENV=/opt/venv RUN "/opt/cpython/install/bin/python3" -m venv ${VIRTUAL_ENV} ENV PATH="${VIRTUAL_ENV}/bin:${PATH}" COPY contrib/deterministic-build/requirements-build-base.txt /opt/deterministic-build/ COPY contrib/deterministic-build/requirements-build-android.txt /opt/deterministic-build/ RUN /opt/venv/bin/python3 -m pip install --no-build-isolation --no-dependencies \ -r /opt/deterministic-build/requirements-build-base.txt RUN /opt/venv/bin/python3 -m pip install --no-build-isolation --no-dependencies --no-binary :all: \ -r /opt/deterministic-build/requirements-build-android.txt # install buildozer ENV BUILDOZER_CHECKOUT_COMMIT="4403ecf445f10b5fbf7c74f4621bf2b922ad35b5" # ^ from branch electrum_20240930 (note: careful with force-pushing! see #8162) RUN cd /opt \ && git clone https://github.com/spesmilo/buildozer \ && cd buildozer \ && git checkout "${BUILDOZER_CHECKOUT_COMMIT}^{commit}" \ && /opt/venv/bin/python3 -m pip install --no-build-isolation --no-dependencies -e . # install python-for-android ENV P4A_CHECKOUT_COMMIT="a01269f7799587ad74ee40e0b642d917b8db7d4e" # ^ from branch electrum_20251211 (note: careful with force-pushing! see #8162) RUN cd /opt \ && git clone https://github.com/spesmilo/python-for-android \ && cd python-for-android \ && git checkout "${P4A_CHECKOUT_COMMIT}^{commit}" \ && /opt/venv/bin/python3 -m pip install --no-build-isolation --no-dependencies -e . # build env vars ENV USE_SDK_WRAPPER=1 ENV GRADLE_OPTS="-Xmx1536M -Dorg.gradle.jvmargs='-Xmx1536M'" #ENV P4A_FULL_DEBUG=1 ================================================ FILE: contrib/android/Makefile ================================================ SHELL := /bin/bash PYTHON = python3 # for reproducible builds export LC_ALL := C export TZ := UTC ifndef ELEC_APK_USE_CURRENT_TIME export SOURCE_DATE_EPOCH := $(shell git log -1 --pretty=%ct) else # p4a sets "private_version" based on SOURCE_DATE_EPOCH. "private_version" gets compiled into the apk, # and is used at runtime to decide whether the already extracted project files in the app's datadir need updating. # So, "private_version" needs to be reproducible, but it would be useful during development if it changed # between subsequent builds (otherwise the new code won't be unpacked and used at runtime!). # For this reason, for development purposes, we set SOURCE_DATE_EPOCH here to the current time. # see https://github.com/kivy/python-for-android/blob/e8686e2104a553f05959cdaf7dd26867671fc8e6/pythonforandroid/bootstraps/common/build/build.py#L575-L587 export SOURCE_DATE_EPOCH := $(shell date +%s) endif export PYTHONHASHSEED := $(SOURCE_DATE_EPOCH) export BUILD_DATE := $(shell LC_ALL=C TZ=UTC date +'%b %e %Y' -d @$(SOURCE_DATE_EPOCH)) export BUILD_TIME := $(shell LC_ALL=C TZ=UTC date +'%H:%M:%S' -d @$(SOURCE_DATE_EPOCH)) .PHONY: apk clean prepare: # running pre build setup # copy electrum to main.py @cp buildozer_$(ELEC_APK_GUI).spec ../../buildozer.spec @cp ../../run_electrum ../../main.py apk: @make prepare @-cd ../..; buildozer android debug @make clean release: @make prepare @-cd ../..; buildozer android release @make clean clean: # Cleaning up # rename main.py to electrum @-rm ../../main.py # remove buildozer.spec @-rm ../../buildozer.spec ================================================ FILE: contrib/android/Readme.md ================================================ # Qml GUI The Qml GUI is used with Electrum on Android devices, since Electrum 4.4. To generate an APK file, follow these instructions. (note: older versions of Electrum for Android used the "kivy" GUI) ## Android binary with Docker ✓ _These binaries should be reproducible, meaning you should be able to generate binaries that match the official releases._ - _Minimum supported target system (i.e. what end-users need): Android 6.0 (API 23)_ This assumes an Ubuntu (x86_64) host, but it should not be too hard to adapt to another similar system. 1. Install Docker See [`contrib/docker_notes.md`](../docker_notes.md). (worth reading even if you already have docker) 2. Build binaries The build script takes a few arguments. To see syntax, run it without providing any: ``` $ ./build.sh ``` For development, consider e.g. `$ ./build.sh qml arm64-v8a debug` If you want reproducibility, try instead e.g.: ``` $ ELECBUILD_COMMIT=HEAD ./build.sh qml all release-unsigned ``` 3. The generated binary is in `./dist`. ## Verifying reproducibility and comparing against official binary Every user can verify that the official binary was created from the source code in this repository. 1. Build your own binary as described above. Make sure you don't build in `debug` mode, instead use either of `release` or `release-unsigned`. If you build in `release` mode, the apk will be signed, which requires a keystore that you need to create manually (see source of `make_apk.sh` for an example). 2. Note that the binaries are not going to be byte-for-byte identical, as the official release is signed by a keystore that only the project maintainers have. You can use the `apkdiff.py` python script (written by the Signal developers) to compare the two binaries. ``` $ python3 contrib/android/apkdiff.py Electrum_apk_that_you_built.apk Electrum_apk_official_release.apk ``` This should output `APKs match!`. ## FAQ ### I changed something but I don't see any differences on the phone. What did I do wrong? You probably need to clear the cache: `rm -rf .buildozer/android/platform/build-*/{build,dists}` ### How do I deploy on connected phone for quick testing? Assuming `adb` is installed: ``` $ adb -d install -r dist/Electrum-*-arm64-v8a-debug.apk $ adb shell monkey -p org.electrum.electrum 1 ``` Note `adb install` can take a `--user {userId}` option to install the app for a specific profile. Without that, the default is to install to *all* profiles. ### How do I get an interactive shell inside docker? ``` $ docker run -it --rm \ -v $PWD:/home/user/wspace/electrum \ -v $PWD/.buildozer/.gradle:/home/user/.gradle \ --workdir /home/user/wspace/electrum \ electrum-android-builder-img ``` ### How do I get more verbose logs for the build? See `log_level` in `buildozer.spec` ### How can I see logs at runtime? This should work OK for most scenarios: ``` adb logcat | grep python ``` Better `grep` but fragile because of `cut`: ``` adb logcat | grep -F "`adb shell ps | grep org.electrum.electrum | cut -c14-19`" ``` ### The Qml GUI can be run directly on Linux Desktop. How? Install requirements: ``` python3 -m pip install ".[qml_gui]" ``` Run electrum with the `-g` switch: `electrum -g qml` Notes: - pyqt ~6.4 would work best, as the gui has not yet been adapted to styling changes in 6.5 - However, pyqt6 as distributed on PyPI does not include a required module (PyQt6.QtQml) until 6.5 - Installing these deps from your OS package manager should also work, except many don't distribute pyqt6 yet. For pyqt5 on debian-based distros, this used to look like this: ``` sudo apt-get install python3-pyqt5 python3-pyqt5.qtquick python3-pyqt5.qtmultimedia sudo apt-get install python3-pil sudo apt-get install qml-module-qtquick-controls2 qml-module-qtquick-layouts \ qml-module-qtquick-window2 qml-module-qtmultimedia \ libqt5multimedia5-plugins qml-module-qt-labs-folderlistmodel ``` ### debug vs release build If you just follow the instructions above, you will build the apk in debug mode. The most notable difference is that the apk will be signed using a debug keystore. If you are planning to upload what you build to e.g. the Play Store, you should create your own keystore, back it up safely, and run `./build.sh` in `release` mode. See e.g. [kivy wiki](https://github.com/kivy/kivy/wiki/Creating-a-Release-APK) and [android dev docs](https://developer.android.com/studio/build/building-cmdline#sign_cmdline). ### Access datadir on Android from desktop (e.g. to copy wallet file) Note that this only works for debug builds! Otherwise the security model of Android does not let you access the internal storage of an app without root. (See [this](https://stackoverflow.com/q/9017073)) To pull a file: ``` $ adb shell adb$ run-as org.electrum.electrum ls /data/data/org.electrum.electrum/files/data adb$ exit $ adb exec-out run-as org.electrum.electrum cat /data/data/org.electrum.electrum/files/data/wallets/my_wallet > my_wallet ``` To push a file: ``` $ adb push ~/wspace/tmp/my_wallet /data/local/tmp $ adb shell adb$ ls -la /data/local/tmp adb$ run-as org.electrum.testnet.electrum cp /data/local/tmp/my_wallet /data/data/org.electrum.testnet.electrum/files/data/testnet/wallets/ adb$ run-as org.electrum.testnet.electrum chmod -R 700 /data/data/org.electrum.testnet.electrum/files/data/testnet/wallets adb$ run-as org.electrum.testnet.electrum chmod -R u-x,u+X /data/data/org.electrum.testnet.electrum/files/data/testnet/wallets adb$ rm /data/local/tmp/my_wallet ``` Or use Android Studio: "Device File Explorer", which can download/upload data directly from device (via adb). #### Device with multiple user profiles There are further complications if using an Android device [with multiple user profiles](https://source.android.com/docs/devices/admin/multi-user-testing) (typical for GrapheneOS/etc). Run `$ adb shell pm list users` to get a list of all existing users, and take note of the user ids. Instead of `/data/data/{app.path}`, private app data is stored at `/data/user/{userId}/{app.path}`. Further, instead of `adb$ run-as org.electrum.electrum`, you need `adb$ run-as org.electrum.electrum --user {userId}`. ### How to investigate diff between binaries if reproducibility fails? ``` cd dist/ unzip Electrum-*.apk1 -d apk1 mkdir apk1/assets/private_mp3/ tar -xzvf apk1/assets/private.tar --directory apk1/assets/private_mp3/ mkdir apk1/lib/_libpybundle/ tar -xzvf apk1/lib/*/libpybundle.so --directory apk1/lib/_libpybundle/ unzip Electrum-*.apk2 -d apk2 mkdir apk2/assets/private_mp3/ tar -xzvf apk2/assets/private.tar --directory apk2/assets/private_mp3/ mkdir apk2/lib/_libpybundle/ tar -xzvf apk2/lib/*/libpybundle.so --directory apk2/lib/_libpybundle/ sudo chown --recursive "$(id -u -n)":"$(id -u -n)" apk1/ apk2/ chmod -R +Xr apk1/ apk2/ unzip apk1/lib/_libpybundle/_python_bundle/stdlib.zip -d apk1/lib/_libpybundle/_python_bundle/stdlib unzip apk2/lib/_libpybundle/_python_bundle/stdlib.zip -d apk2/lib/_libpybundle/_python_bundle/stdlib sudo chown --recursive "$(id -u -n)":"$(id -u -n)" apk1/ apk2/ chmod -R +Xr apk1/ apk2/ $(cd apk1; find -type f -exec sha256sum '{}' \; > ./../sha256sum1) $(cd apk2; find -type f -exec sha256sum '{}' \; > ./../sha256sum2) diff sha256sum1 sha256sum2 > d cat d ``` ### How to install apks built by the CI on my phone? The CI (Cirrus) builds apks on most git commits. See e.g. [here](https://github.com/spesmilo/electrum/runs/9272252577). The task name should start with "Android build". Click "View more details on Cirrus CI" to get to cirrus' website, and search for "Artifacts". The apk is built in `debug` mode, and is signed using an ephemeral RSA key. For tech demo purposes, you can directly install this apk on your phone. However, if you already have electrum installed on your phone, Android's TOFU signing model will not let you upgrade that to the CI apk due to mismatching signing keys. As the CI key is ephemeral, it is not even possible to upgrade from an older CI apk to a newer CI apk. However, it is possible to resign the apk manually with one's own key, using e.g. [`apksigner`](https://developer.android.com/studio/command-line/apksigner), mutating the apk in place, after which it should be possible to upgrade: ``` apksigner sign --ks ~/wspace/electrum/contrib/android/android_debug.keystore Electrum-*-arm64-v8a-debug.apk ``` ================================================ FILE: contrib/android/apkdiff.py ================================================ #! /usr/bin/env python3 # from https://github.com/signalapp/Signal-Android/blob/2029ea378f249a70983c1fc3d55b9a63588bc06c/reproducible-builds/apkdiff/apkdiff.py import sys from zipfile import ZipFile # FIXME it is possible to hide data in the apk signing block - and then the application # can introspect itself at runtime and access that, even execute it as code... :/ # see https://source.android.com/docs/security/features/apksigning/v2#apk-signing-block # https://android.izzysoft.de/articles/named/iod-scan-apkchecks # https://github.com/obfusk/sigblock-code-poc # I think if the app did this kind of introspection, that should be caught by code review, # but still, note that with this current diff script it is possible to smuggle data in the apk. class ApkDiff: IGNORE_FILES = ["META-INF/MANIFEST.MF", "META-INF/CERT.RSA", "META-INF/CERT.SF"] def compare(self, sourceApk, destinationApk) -> bool: sourceZip = ZipFile(sourceApk, 'r') destinationZip = ZipFile(destinationApk, 'r') if self.compareManifests(sourceZip, destinationZip) and self.compareEntries(sourceZip, destinationZip): print("APKs match!") return True else: print("APKs don't match!") return False def compareManifests(self, sourceZip, destinationZip): sourceEntrySortedList = sorted(sourceZip.namelist()) destinationEntrySortedList = sorted(destinationZip.namelist()) for ignoreFile in self.IGNORE_FILES: while ignoreFile in sourceEntrySortedList: sourceEntrySortedList.remove(ignoreFile) while ignoreFile in destinationEntrySortedList: destinationEntrySortedList.remove(ignoreFile) if len(sourceEntrySortedList) != len(destinationEntrySortedList): print("Manifest lengths differ!") for (sourceEntryName, destinationEntryName) in zip(sourceEntrySortedList, destinationEntrySortedList): if sourceEntryName != destinationEntryName: print("Sorted manifests don't match, %s vs %s" % (sourceEntryName, destinationEntryName)) return False return True def compareEntries(self, sourceZip, destinationZip): sourceInfoList = list(filter(lambda sourceInfo: sourceInfo.filename not in self.IGNORE_FILES, sourceZip.infolist())) destinationInfoList = list(filter(lambda destinationInfo: destinationInfo.filename not in self.IGNORE_FILES, destinationZip.infolist())) if len(sourceInfoList) != len(destinationInfoList): print("APK info lists of different length!") return False for sourceEntryInfo in sourceInfoList: for destinationEntryInfo in list(destinationInfoList): if sourceEntryInfo.filename == destinationEntryInfo.filename: sourceEntry = sourceZip.open(sourceEntryInfo, 'r') destinationEntry = destinationZip.open(destinationEntryInfo, 'r') if not self.compareFiles(sourceEntry, destinationEntry): print("APK entry %s does not match %s!" % (sourceEntryInfo.filename, destinationEntryInfo.filename)) return False destinationInfoList.remove(destinationEntryInfo) break return True def compareFiles(self, sourceFile, destinationFile): sourceChunk = sourceFile.read(1024) destinationChunk = destinationFile.read(1024) while sourceChunk != b"" or destinationChunk != b"": if sourceChunk != destinationChunk: return False sourceChunk = sourceFile.read(1024) destinationChunk = destinationFile.read(1024) return True if __name__ == '__main__': if len(sys.argv) != 3: print("Usage: apkdiff ") sys.exit(1) match = ApkDiff().compare(sys.argv[1], sys.argv[2]) if match: sys.exit(0) else: sys.exit(1) ================================================ FILE: contrib/android/apt.preferences ================================================ Package: * Pin: origin "snapshot.debian.org" Pin-Priority: 1001 ================================================ FILE: contrib/android/apt.sources.list ================================================ deb https://snapshot.debian.org/archive/debian/20260129T082333Z/ trixie main deb-src https://snapshot.debian.org/archive/debian/20260129T082333Z/ trixie main ================================================ FILE: contrib/android/bitcoin_intent.xml ================================================ ================================================ FILE: contrib/android/build.sh ================================================ #!/bin/bash # # env vars: # - ELECBUILD_NOCACHE: if set, forces rebuild of docker image # - ELECBUILD_COMMIT: if set, do a fresh clone and git checkout set -e PROJECT_ROOT="$(dirname "$(readlink -e "$0")")/../.." PROJECT_ROOT_OR_FRESHCLONE_ROOT="$PROJECT_ROOT" CONTRIB="$PROJECT_ROOT/contrib" CONTRIB_ANDROID="$CONTRIB/android" DISTDIR="$PROJECT_ROOT/dist" BUILD_UID=$(/usr/bin/stat -c %u "$PROJECT_ROOT") . "$CONTRIB"/build_tools_util.sh # check arguments if [[ -n "$3" \ && ( "$1" == "qml" ) \ && ( "$2" == "all" || "$2" == "armeabi-v7a" || "$2" == "arm64-v8a" || "$2" == "x86" || "$2" == "x86_64" ) \ && ( "$3" == "debug" || "$3" == "release" || "$3" == "release-unsigned" ) ]] ; then info "arguments $1 $2 $3" else fail "usage: build.sh " exit 1 fi # create symlink rm -f ${PROJECT_ROOT}/.buildozer mkdir -p "${PROJECT_ROOT}/.buildozer_$1" ln -s ".buildozer_$1" ${PROJECT_ROOT}/.buildozer DOCKER_BUILD_FLAGS="" if [ ! -z "$ELECBUILD_NOCACHE" ] ; then info "ELECBUILD_NOCACHE is set. forcing rebuild of docker image." DOCKER_BUILD_FLAGS="--pull --no-cache" fi if [ -z "$ELECBUILD_COMMIT" ] ; then # local dev build DOCKER_BUILD_FLAGS="$DOCKER_BUILD_FLAGS --build-arg UID=$BUILD_UID" fi info "building docker image." docker build \ $DOCKER_BUILD_FLAGS \ -t electrum-android-builder-img \ --file "$CONTRIB_ANDROID/Dockerfile" \ "$PROJECT_ROOT" # maybe do fresh clone if [ ! -z "$ELECBUILD_COMMIT" ] ; then info "ELECBUILD_COMMIT=$ELECBUILD_COMMIT. doing fresh clone and git checkout." FRESH_CLONE=${FRESH_CLONE:-"/tmp/electrum_build/android/fresh_clone/electrum"} rm -rf "$FRESH_CLONE" 2>/dev/null || ( info "we need sudo to rm prev FRESH_CLONE." && sudo rm -rf "$FRESH_CLONE" ) umask 0022 git clone "$PROJECT_ROOT" "$FRESH_CLONE" cd "$FRESH_CLONE" git checkout "$ELECBUILD_COMMIT" PROJECT_ROOT_OR_FRESHCLONE_ROOT="$FRESH_CLONE" else info "not doing fresh clone." fi DOCKER_RUN_FLAGS="" if [[ "$3" == "release" ]] ; then info "'release' mode selected. mounting ~/.keystore inside container." DOCKER_RUN_FLAGS="-v $HOME/.keystore:/home/user/.keystore" fi if sh -c ": >/dev/tty" >/dev/null 2>/dev/null; then info "/dev/tty is available and usable" DOCKER_RUN_FLAGS="$DOCKER_RUN_FLAGS -it" fi info "building binary..." mkdir --parents "$PROJECT_ROOT_OR_FRESHCLONE_ROOT"/.buildozer/.gradle # check uid and maybe chown. see #8261 if [ ! -z "$ELECBUILD_COMMIT" ] ; then # fresh clone (reproducible build) if [ $(id -u) != "1000" ] || [ $(id -g) != "1000" ] ; then info "need to chown -R FRESH_CLONE dir. prompting for sudo." sudo chown -R 1000:1000 "$FRESH_CLONE" fi fi docker run --rm \ --name electrum-android-builder-cont \ -v "$PROJECT_ROOT_OR_FRESHCLONE_ROOT":/home/user/wspace/electrum \ -v "$PROJECT_ROOT_OR_FRESHCLONE_ROOT"/.buildozer/.gradle:/home/user/.gradle \ $DOCKER_RUN_FLAGS \ --workdir /home/user/wspace/electrum \ electrum-android-builder-img \ ./contrib/android/make_apk.sh "$@" # make sure resulting binary location is independent of fresh_clone if [ ! -z "$ELECBUILD_COMMIT" ] ; then mkdir --parents "$DISTDIR/" cp -f "$FRESH_CLONE/dist"/* "$DISTDIR/" fi ================================================ FILE: contrib/android/buildozer_qml.spec ================================================ [app] # (str) Title of your application title = Electrum # (str) Package name package.name = Electrum # (str) Package domain (needed for android/ios packaging) package.domain = org.electrum # (str) Source code where the main.py live source.dir = . # (list) Source files to include (let empty to include all the files) source.include_exts = py,png,jpg,qml,qmltypes,ttf,txt,gif,pem,mo,json,csv,so,svg # (list) Source files to exclude (let empty to not exclude anything) source.exclude_exts = spec # (list) List of directory to exclude (let empty to not exclude anything) source.exclude_dirs = bin, build, dist, contrib, env, tests, fastlane, electrum/www, electrum/scripts, electrum/utils, electrum/gui/qt, electrum/plugins/audio_modem, electrum/plugins/bitbox02, electrum/plugins/coldcard, electrum/plugins/digitalbitbox, electrum/plugins/jade, electrum/plugins/keepkey, electrum/plugins/ledger, electrum/plugins/nwc, electrum/plugins/payserver, electrum/plugins/revealer, electrum/plugins/safe_t, electrum/plugins/swapserver, electrum/plugins/timelock_recovery, electrum/plugins/trezor, electrum/plugins/watchtower, packages/qdarkstyle, packages/qtpy, packages/bin, packages/share, packages/pkg_resources, packages/setuptools # (list) List of exclusions using pattern matching source.exclude_patterns = Makefile,setup*, # not reproducible: packages/aiohttp-*.dist-info/*, packages/frozenlist-*.dist-info/* # (str) Application versioning (method 1) version.regex = ELECTRUM_VERSION = '(.*)' version.filename = %(source.dir)s/electrum/version.py # (str) Application versioning (method 2) #version = 1.9.8 # (list) Application requirements # note: versions and hashes are pinned in ./p4a_recipes/* requirements = hostpython3, python3, android, openssl, plyer, libffi, libsecp256k1, pycryptodomex, pyqt6sip, pyqt6, libzbar # (str) Presplash of the application presplash.filename = %(source.dir)s/electrum/gui/icons/electrum_presplash.png # (str) Icon of the application icon.filename = %(source.dir)s/electrum/gui/icons/android_electrum_icon_legacy.png icon.adaptive_foreground.filename = %(source.dir)s/electrum/gui/icons/android_electrum_icon_foreground.png icon.adaptive_background.filename = %(source.dir)s/electrum/gui/icons/android_electrum_icon_background.png # (str) Supported orientation (one of landscape, portrait or all) orientation = portrait # (bool) Indicate if the application should be fullscreen or not fullscreen = False # # Android specific # # (list) Permissions android.permissions = INTERNET, CAMERA, WRITE_EXTERNAL_STORAGE, POST_NOTIFICATIONS, USE_BIOMETRIC # (int) Android API to use (compileSdkVersion) # note: when changing, Dockerfile also needs to be changed to install corresponding build tools android.api = 31 # (int) Android targetSdkVersion android.target_sdk_version = 35 # (int) Minimum API required. You will need to set the android.ndk_api to be as low as this value. android.minapi = 23 # (str) Android NDK version to use android.ndk = 23b # (int) Android NDK API to use (optional). This is the minimum API your app will support. android.ndk_api = 23 # (bool) Use --private data storage (True) or --dir public storage (False) #android.private_storage = True # (str) Android NDK directory (if empty, it will be automatically downloaded.) android.ndk_path = /opt/android/android-ndk # (str) Android SDK directory (if empty, it will be automatically downloaded.) android.sdk_path = /opt/android/android-sdk # (str) ANT directory (if empty, it will be automatically downloaded.) android.ant_path = /opt/android/apache-ant # (bool) If True, then skip trying to update the Android sdk # This can be useful to avoid excess Internet downloads or save time # when an update is due and you just want to test/build your package # note(ghost43): probably needed for reproducibility. versions pinned in Dockerfile. android.skip_update = True # (bool) If True, then automatically accept SDK license # agreements. This is intended for automation only. If set to False, # the default, you will be shown the license when first running # buildozer. android.accept_sdk_license = True # (str) Android entry point, default is ok for Kivy-based app #android.entrypoint = org.renpy.android.PythonActivity # (list) List of Java .jar files to add to the libs so that pyjnius can access # their classes. Don't add jars that you do not need, since extra jars can slow # down the build process. Allows wildcards matching, for example: # OUYA-ODK/libs/*.jar #android.add_jars = foo.jar,bar.jar,path/to/more/*.jar #android.add_jars = lib/android/zbar.jar android.add_jars = .buildozer/android/platform/*/build/libs_collections/Electrum/jar/*.jar android.add_aars = contrib/android/.cache/aars/BarcodeScannerView.aar, contrib/android/.cache/aars/CameraView.aar, contrib/android/.cache/aars/zxing-cpp.aar # (list) List of Java files to add to the android project (can be java or a # directory containing the files) android.add_src = electrum/gui/qml/java_classes/ # kotlin-stdlib is required for zxing-cpp (BarcodeScannerView) android.gradle_dependencies = com.android.support:support-compat:28.0.0, org.jetbrains.kotlin:kotlin-stdlib:1.8.22 android.add_activities = org.electrum.qr.SimpleScannerActivity, org.electrum.biometry.BiometricActivity # (list) Put these files or directories in the apk res directory. # The option may be used in three ways, the value may contain one or zero ':' # Some examples: # 1) A file to add to resources, legal resource names contain ['a-z','0-9','_'] # android.add_resources = my_icons/all-inclusive.png:drawable/all_inclusive.png # 2) A directory, here 'legal_icons' must contain resources of one kind # android.add_resources = legal_icons:drawable # 3) A directory, here 'legal_resources' must contain one or more directories, # each of a resource kind: drawable, xml, etc... # android.add_resources = legal_resources android.add_resources = electrum/gui/qml/android_res/layout:layout # (str) python-for-android branch to use, if not master, useful to try # not yet merged features. #android.branch = master # (str) OUYA Console category. Should be one of GAME or APP # If you leave this blank, OUYA support will not be enabled #android.ouya.category = GAME # (str) Filename of OUYA Console icon. It must be a 732x412 png image. #android.ouya.icon.filename = %(source.dir)s/data/ouya_icon.png # (str) XML file to include as an intent filters in tag android.manifest.intent_filters = contrib/android/bitcoin_intent.xml # (str) launchMode to set for the main activity android.manifest.launch_mode = singleTask # (list) Android additional libraries to copy into libs/armeabi #android.add_libs_armeabi = lib/android/*.so # (bool) Indicate whether the screen should stay on # Don't forget to add the WAKE_LOCK permission if you set this to True #android.wakelock = False # (str) The Android arch to build for, choices: armeabi-v7a, arm64-v8a, x86, x86_64 # note: can be overwritten by APP_ANDROID_ARCH env var #android.arch = armeabi-v7a # (int) overrides automatic versionCode computation (used in build.gradle) # this is not the same as app version and should only be edited if you know what you're doing # android.numeric_version = 1 # (list) Android application meta-data to set (key=value format) #android.meta_data = # (list) Android library project to add (will be added in the # project.properties automatically.) #android.library_references = android.whitelist = lib-dynload/_csv.so # (bool) enables Android auto backup feature (Android API >=23) android.allow_backup = False # (str) The format used to package the app for release mode (aab or apk or aar). android.release_artifact = apk # (str) The format used to package the app for debug mode (apk or aar). android.debug_artifact = apk # # Python for android (p4a) specific # # (str) python-for-android git clone directory (if empty, it will be automatically cloned from github) p4a.source_dir = /opt/python-for-android # (str) The directory in which python-for-android should look for your own build recipes (if any) p4a.local_recipes = %(source.dir)s/contrib/android/p4a_recipes/ # (str) Filename to the hook for p4a #p4a.hook = # (str) Bootstrap to use for android builds p4a.bootstrap = qt6 # (int) port number to specify an explicit --port= p4a argument (eg for bootstrap flask) #p4a.port = # # iOS specific # # (str) Name of the certificate to use for signing the debug version # Get a list of available identities: buildozer ios list_identities #ios.codesign.debug = "iPhone Developer: ()" # (str) Name of the certificate to use for signing the release version #ios.codesign.release = %(ios.codesign.debug)s [buildozer] # (int) Log level (0 = error only, 1 = info, 2 = debug (with command output)) log_level = 2 # (str) Path to build output (i.e. .apk, .ipa) storage bin_dir = ./dist # ----------------------------------------------------------------------------- # List as sections # # You can define all the "list" as [section:key]. # Each line will be considered as a option to the list. # Let's take [app] / source.exclude_patterns. # Instead of doing: # # [app] # source.exclude_patterns = license,data/audio/*.wav,data/images/original/* # # This can be translated into: # # [app:source.exclude_patterns] # license # data/audio/*.wav # data/images/original/* # # ----------------------------------------------------------------------------- # Profiles # # You can extend section / key with a profile # For example, you want to deploy a demo version of your application without # HD content. You could first change the title to add "(demo)" in the name # and extend the excluded directories to remove the HD content. # # [app@demo] # title = My Application (demo) # # [app:source.exclude_patterns@demo] # images/hd/* # # Then, invoke the command line with the "demo" profile: # # buildozer --profile demo android debug ================================================ FILE: contrib/android/dl-ndk-ci.sh ================================================ #!/bin/sh if [ -z "$1" ]; then echo "missing url" exit 1 fi echo $1 curl $1 | grep "var JSVariables" | python3 -c "import sys; line=sys.stdin.read(); line=line[line.find('{'):-2]; import json; j=json.loads(line); print(j['artifactUrl'])" | wget -i - -O android-ndk-ci-linux-x86_64.zip ================================================ FILE: contrib/android/get_apk_versioncode.py ================================================ #!/usr/bin/python3 import importlib.util import os import sys ARCH_DICT = { "x86_64": "4", "arm64-v8a": "3", "armeabi-v7a": "2", "x86": "1", "null": "0", } def get_electrum_version() -> str: project_root = os.path.abspath(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) version_file_path = os.path.join(project_root, "electrum", "version.py") # load version.py; needlessly complicated alternative to "imp.load_source": version_spec = importlib.util.spec_from_file_location('version', version_file_path) version_module = version = importlib.util.module_from_spec(version_spec) version_spec.loader.exec_module(version_module) return version.ELECTRUM_VERSION def get_android_versioncode(*, arch_name: str) -> int: version_code = 0 # add ELECTRUM_VERSION app_version = get_electrum_version() # if alpha/beta, and not stable: strip out alpha/beta part from last component. # NOTE: we REUSE the version_code int between alphas/betas and the final stable. # This is not allowed on Google Play or F-Droid. # This means we MUST NOT upload alphas/betas there. if any(c in app_version for c in ("a", "b")): c_pos = app_version.find("a") if c_pos == -1: c_pos = app_version.find("b") app_version = app_version[:c_pos] # now the app_version str must contain exactly three dot-delimited components app_version_components = app_version.split('.') assert len(app_version_components) == 3, f"version str expected to have 3 components, but got {app_version!r}" # convert to int for i in app_version_components: version_code *= 100 version_code += int(i) # add arch arch_code = ARCH_DICT[arch_name] assert len(arch_code) == 1 version_code *= 10 version_code += int(arch_code) # compensate for legacy scheme # note: up until version 4.5.5, we used a different scheme for version_code. # 4_______________4_05_05_00 # ^ android arch, ^ app_version (4.5.5.0) # This offset ensures that all new-scheme version codes are larger than the old-scheme version codes. offset_due_to_legacy_scheme = 45_000_000 version_code += offset_due_to_legacy_scheme return version_code if __name__ == '__main__': try: android_arch = sys.argv[1] except Exception: print(f"usage: {os.path.basename(__file__)} ", file=sys.stderr) sys.exit(1) if android_arch not in ARCH_DICT: print(f"usage: {os.path.basename(__file__)} ", file=sys.stderr) print(f"error: unknown {android_arch=}", file=sys.stderr) print(f" should be one of: {list(ARCH_DICT.keys())}", file=sys.stderr) sys.exit(1) version_code = get_android_versioncode(arch_name=android_arch) assert isinstance(version_code, int), f"{version_code=!r} must be an int." print(version_code, file=sys.stdout) ================================================ FILE: contrib/android/make_apk.sh ================================================ #!/bin/bash set -e CONTRIB_ANDROID="$(dirname "$(readlink -e "$0")")" CONTRIB="$CONTRIB_ANDROID"/.. PROJECT_ROOT="$CONTRIB"/.. PACKAGES="$PROJECT_ROOT"/packages/ . "$CONTRIB"/build_tools_util.sh git -C "$PROJECT_ROOT" rev-parse 2>/dev/null || fail "Building outside a git clone is not supported." # arguments have been checked in build.sh export ELEC_APK_GUI=$1 if [ ! -d "$PACKAGES" ]; then "$CONTRIB"/make_packages.sh || fail "make_packages failed" fi # update locale info "preparing electrum-locale." ( "$CONTRIB/locale/build_cleanlocale.sh" # we want the binary to have only compiled (.mo) locale files; not source (.po) files rm -r "$PROJECT_ROOT/electrum/locale/locale"/*/electrum.po ) pushd "$CONTRIB_ANDROID" info "apk building phase starts." # Uncomment and change below to set a custom android package id, # e.g. to allow simultaneous mainnet and testnet installs of the apk. # defaults: # export APP_PACKAGE_NAME=Electrum # export APP_PACKAGE_DOMAIN=org.electrum # FIXME: changing "APP_PACKAGE_NAME" seems to require a clean rebuild of ".buildozer/", # to avoid that, maybe change "APP_PACKAGE_DOMAIN" instead. # So, in particular, to build a testnet apk, simply uncomment: #export APP_PACKAGE_DOMAIN=org.electrum.testnet if [ $CI ]; then # override log level specified in buildozer.spec to "debug": export BUILDOZER_LOG_LEVEL=2 fi if [[ "$3" == "release" ]] ; then # do release build, and sign the APKs. TARGET="release" export P4A_RELEASE_KEYSTORE_PASSWD="$4" export P4A_RELEASE_KEYALIAS_PASSWD="$4" export P4A_RELEASE_KEYSTORE=~/.keystore export P4A_RELEASE_KEYALIAS=electrum if [ -z "$P4A_RELEASE_KEYSTORE_PASSWD" ] || [ -z "$P4A_RELEASE_KEYALIAS_PASSWD" ]; then echo "p4a password not defined" exit 1 fi elif [[ "$3" == "release-unsigned" ]] ; then # do release build, but do not sign the APKs. TARGET="release" elif [[ "$3" == "debug" ]] ; then # do debug build. TARGET="apk" export P4A_DEBUG_KEYSTORE="$CONTRIB_ANDROID"/android_debug.keystore export P4A_DEBUG_KEYSTORE_PASSWD=unsafepassword export P4A_DEBUG_KEYALIAS_PASSWD=unsafepassword export P4A_DEBUG_KEYALIAS=electrum # create keystore if needed if [ ! -f "$P4A_DEBUG_KEYSTORE" ]; then keytool -genkey -v -keystore "$CONTRIB_ANDROID"/android_debug.keystore \ -alias "$P4A_DEBUG_KEYALIAS" -keyalg RSA -keysize 2048 -validity 10000 \ -dname "CN=mqttserver.ibm.com, OU=ID, O=IBM, L=Hursley, S=Hants, C=GB" \ -storepass "$P4A_DEBUG_KEYSTORE_PASSWD" \ -keypass "$P4A_DEBUG_KEYALIAS_PASSWD" fi export ELEC_APK_USE_CURRENT_TIME=1 else fail "unknown build type" fi if [[ "$2" == "all" ]] ; then # build all apks # FIXME failures are not propagated out: we should fail the script if any arch build fails export APP_ANDROID_ARCHS=armeabi-v7a export APP_ANDROID_NUMERIC_VERSION=$("$CONTRIB_ANDROID"/get_apk_versioncode.py "$APP_ANDROID_ARCHS") "$CONTRIB_ANDROID"/make_barcode_scanner.sh "$APP_ANDROID_ARCHS" || fail "make_barcode_scanner.sh failed" make $TARGET export APP_ANDROID_ARCHS=arm64-v8a export APP_ANDROID_NUMERIC_VERSION=$("$CONTRIB_ANDROID"/get_apk_versioncode.py "$APP_ANDROID_ARCHS") "$CONTRIB_ANDROID"/make_barcode_scanner.sh "$APP_ANDROID_ARCHS" || fail "make_barcode_scanner.sh failed" make $TARGET export APP_ANDROID_ARCHS=x86_64 export APP_ANDROID_NUMERIC_VERSION=$("$CONTRIB_ANDROID"/get_apk_versioncode.py "$APP_ANDROID_ARCHS") "$CONTRIB_ANDROID"/make_barcode_scanner.sh "$APP_ANDROID_ARCHS" || fail "make_barcode_scanner.sh failed" make $TARGET else export APP_ANDROID_ARCHS=$2 export APP_ANDROID_NUMERIC_VERSION=$("$CONTRIB_ANDROID"/get_apk_versioncode.py "$APP_ANDROID_ARCHS") "$CONTRIB_ANDROID"/make_barcode_scanner.sh "$APP_ANDROID_ARCHS" || fail "make_barcode_scanner.sh failed" make $TARGET fi popd info "done." ls -la "$PROJECT_ROOT/dist" sha256sum "$PROJECT_ROOT/dist"/* ================================================ FILE: contrib/android/make_barcode_scanner.sh ================================================ #!/bin/bash # script to clone and build https://github.com/markusfisch/BarcodeScannerView and its dependencies, # https://github.com/markusfisch/CameraView/ and https://github.com/markusfisch/zxing-cpp # which are being used as barcode scanner in the Android app. # To bump the version of BarcodeScannerView, get the newest version tag from the github repo, # then get the required dependencies from # https://github.com/markusfisch/BarcodeScannerView/blob/**VERSION_TAG**/barcodescannerview/build.gradle # then update the commit hashes below. Also update kotlin-stdlib in buildozer_qml.spec to the # "kotlin-version" specified in the used zxing-cpp commit: # https://github.com/markusfisch/zxing-cpp/blob/master/wrappers/aar/build.gradle BARCODE_SCANNER_VIEW_COMMIT_HASH="0bdb69269c252bb6daef2f871b76403c8b051945" # 1.6.5 BARCODE_SCANNER_VIEW_REPO="https://github.com/markusfisch/BarcodeScannerView.git" CAMERA_VIEW_COMMIT_HASH="745597d05bc6abfdb3637a09a8ecaf30fdce7b6e" # 1.10.0 CAMERA_VIEW_REPO="https://github.com/markusfisch/CameraView.git" ZXING_CPP_COMMIT_HASH="79f5adc6250e90de0bd635eb9181c5f8a18affda" # v2.3.0.4 using kotlin-stdlib 1.8.22 ZXING_CPP_REPO="https://github.com/markusfisch/zxing-cpp.git" ######################################################################################################## set -e CONTRIB_ANDROID="$(dirname "$(readlink -e "$0")")" CONTRIB="$CONTRIB_ANDROID"/.. CACHEDIR="$CONTRIB_ANDROID/.cache" BUILDDIR="$CACHEDIR/builds" . "$CONTRIB"/build_tools_util.sh # target architecture passed as argument by`make_apk.sh` TARGET_ARCH="$1" # check if TARGET_ARCH is set and supported if [[ "$TARGET_ARCH" != "armeabi-v7a" \ && "$TARGET_ARCH" != "arm64-v8a" \ && "$TARGET_ARCH" != "x86_64" ]]; then fail "make_barcode_scanner.sh invalid target architecture argument: $TARGET_ARCH" fi info "Building BarcodeScannerView and deps for architecture: $TARGET_ARCH" # check if directories exist, create them if not if [ ! -d "$CACHEDIR/aars" ]; then mkdir -p "$CACHEDIR/aars" fi if [ ! -d "$BUILDDIR" ]; then mkdir -p "$BUILDDIR" fi ####### zxing-cpp ######## # check if zxing-cpp aar is already in cachedir, else build it ZXING_CPP_BUILD_ID="$TARGET_ARCH-$ZXING_CPP_COMMIT_HASH" if [ -f "$CACHEDIR/aars/zxing-cpp-$ZXING_CPP_BUILD_ID.aar" ]; then info "zxing-cpp for $ZXING_CPP_BUILD_ID already exists in cache, skipping build." cp "$CACHEDIR/aars/zxing-cpp-$ZXING_CPP_BUILD_ID.aar" "$CACHEDIR/aars/zxing-cpp.aar" else info "Building zxing-cpp for $ZXING_CPP_BUILD_ID..." ZXING_CPP_DIR="$BUILDDIR/zxing-cpp" clone_or_update_repo "$ZXING_CPP_REPO" "$ZXING_CPP_COMMIT_HASH" "$ZXING_CPP_DIR" cd "$ZXING_CPP_DIR/wrappers/aar" chmod +x gradlew # Set local.properties to use SDK of docker container echo "sdk.dir=${ANDROID_SDK_HOME}" > local.properties # gradlew will install a specific NDK version required by zxing-cpp ./gradlew :zxingcpp:assembleRelease -Pandroid.injected.build.abi="$TARGET_ARCH" # Copy the built AAR to cache directory ZXING_AAR_SOURCE="$ZXING_CPP_DIR/wrappers/aar/zxingcpp/build/outputs/aar/zxingcpp-release.aar" ZXING_AAR_DEST_GENERIC="$CACHEDIR/aars/zxing-cpp.aar" ZXING_AAR_DEST_SPECIFIC="$CACHEDIR/aars/zxing-cpp-$ZXING_CPP_BUILD_ID.aar" if [ ! -f "$ZXING_AAR_SOURCE" ]; then fail "zxing-cpp AAR not found at $ZXING_AAR_SOURCE, build failed?" fi cp "$ZXING_AAR_SOURCE" "$ZXING_AAR_DEST_GENERIC" # keeping an arch specific copy allows to skip the build later if it already exists cp "$ZXING_AAR_SOURCE" "$ZXING_AAR_DEST_SPECIFIC" info "zxing-cpp AAR copied to $ZXING_AAR_DEST_GENERIC" fi ########### CameraView ########### CAMERA_VIEW_BUILD_ID="$CAMERA_VIEW_COMMIT_HASH" if [ -f "$CACHEDIR/aars/CameraView-$CAMERA_VIEW_BUILD_ID.aar" ]; then info "CameraView AAR already exists in cache, skipping build." cp "$CACHEDIR/aars/CameraView-$CAMERA_VIEW_BUILD_ID.aar" "$CACHEDIR/aars/CameraView.aar" else info "Building CameraView..." CAMERA_VIEW_DIR="$BUILDDIR/CameraView" clone_or_update_repo "$CAMERA_VIEW_REPO" "$CAMERA_VIEW_COMMIT_HASH" "$CAMERA_VIEW_DIR" cd "$CAMERA_VIEW_DIR" chmod +x gradlew echo "sdk.dir=${ANDROID_SDK_HOME}" > local.properties ./gradlew :cameraview:assembleRelease CAMERA_AAR_SOURCE="$CAMERA_VIEW_DIR/cameraview/build/outputs/aar/cameraview-release.aar" CAMERA_AAR_DEST_GENERIC="$CACHEDIR/aars/CameraView.aar" CAMERA_AAR_DEST_SPECIFIC="$CACHEDIR/aars/CameraView-$CAMERA_VIEW_BUILD_ID.aar" if [ ! -f "$CAMERA_AAR_SOURCE" ]; then fail "CameraView AAR not found at $CAMERA_AAR_SOURCE" fi cp "$CAMERA_AAR_SOURCE" "$CAMERA_AAR_DEST_GENERIC" cp "$CAMERA_AAR_SOURCE" "$CAMERA_AAR_DEST_SPECIFIC" info "CameraView AAR copied to $CAMERA_AAR_DEST_GENERIC" fi ########### BarcodeScannerView ########### BARCODE_SCANNER_VIEW_BUILD_ID="$BARCODE_SCANNER_VIEW_COMMIT_HASH" if [ -f "$CACHEDIR/aars/BarcodeScannerView-$BARCODE_SCANNER_VIEW_BUILD_ID.aar" ]; then info "BarcodeScannerView AAR already exists in cache, skipping build." cp "$CACHEDIR/aars/BarcodeScannerView-$BARCODE_SCANNER_VIEW_BUILD_ID.aar" "$CACHEDIR/aars/BarcodeScannerView.aar" else info "Building BarcodeScannerView..." BARCODE_SCANNER_VIEW_DIR="$BUILDDIR/BarcodeScannerView" clone_or_update_repo "$BARCODE_SCANNER_VIEW_REPO" "$BARCODE_SCANNER_VIEW_COMMIT_HASH" "$BARCODE_SCANNER_VIEW_DIR" cd "$BARCODE_SCANNER_VIEW_DIR" chmod +x gradlew echo "sdk.dir=${ANDROID_SDK_HOME}" > local.properties ./gradlew :barcodescannerview:assembleRelease BARCODE_AAR_SOURCE="$BARCODE_SCANNER_VIEW_DIR/barcodescannerview/build/outputs/aar/barcodescannerview-release.aar" BARCODE_AAR_DEST_GENERIC="$CACHEDIR/aars/BarcodeScannerView.aar" BARCODE_AAR_DEST_SPECIFIC="$CACHEDIR/aars/BarcodeScannerView-$BARCODE_SCANNER_VIEW_BUILD_ID.aar" if [ ! -f "$BARCODE_AAR_SOURCE" ]; then fail "BarcodeScannerView AAR not found at $BARCODE_AAR_SOURCE" fi cp "$BARCODE_AAR_SOURCE" "$BARCODE_AAR_DEST_GENERIC" cp "$BARCODE_AAR_SOURCE" "$BARCODE_AAR_DEST_SPECIFIC" info "BarcodeScannerView AAR copied to $BARCODE_AAR_DEST_GENERIC" fi info "All barcode scanner libraries built successfully for $TARGET_ARCH" ================================================ FILE: contrib/android/p4a_recipes/README.md ================================================ python-for-android local recipes -------------------------------- These folders are recipes (build scripts) for most of our direct and transitive dependencies for the Android app. python-for-android has recipes built-in for many packages but it also allows users to specify their "local" recipes. Local recipes have precedence over the built-in recipes. The local recipes we have here are mostly just used to pin down specific versions and hashes for reproducibility. The hashes are updated manually. ================================================ FILE: contrib/android/p4a_recipes/cffi/__init__.py ================================================ import os from pythonforandroid.recipes.cffi import CffiRecipe from pythonforandroid.util import load_source util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py')) assert CffiRecipe._version == "1.15.1" assert CffiRecipe.depends == ['setuptools', 'pycparser', 'libffi', 'python3'] assert CffiRecipe.python_depends == [] class CffiRecipePinned(util.InheritedRecipeMixin, CffiRecipe): version = "1.17.1" sha512sum = "907129891d56351ca5cb885aae62334ad432321826d6eddfaa32195b4c7b7689a80333e6d14d0aab479a646aba148b9852c0815b80344dfffa4f183a5e74372c" recipe = CffiRecipePinned() ================================================ FILE: contrib/android/p4a_recipes/cryptography/__init__.py ================================================ from pythonforandroid.recipes.cryptography import CryptographyRecipe assert CryptographyRecipe._version == "2.8" assert CryptographyRecipe.depends == ['openssl', 'six', 'setuptools', 'cffi', 'python3'] assert CryptographyRecipe.python_depends == [] class CryptographyRecipePinned(CryptographyRecipe): sha512sum = "000816a5513691bfbb01c5c65d96fb3567a5ff25300da4b485e716b6d4dc789aec05ed0fe65df9c5e3e60127aa9110f04e646407db5b512f88882b0659f7123f" recipe = CryptographyRecipePinned() ================================================ FILE: contrib/android/p4a_recipes/hostpython3/__init__.py ================================================ import os from pythonforandroid.recipes.hostpython3 import HostPython3Recipe from pythonforandroid.util import load_source util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py')) assert HostPython3Recipe.depends == [] assert HostPython3Recipe.python_depends == [] class HostPython3RecipePinned(util.InheritedRecipeMixin, HostPython3Recipe): # PYTHON_VERSION= # < line here so that I can grep the codebase and teleport here version = "3.11.14" sha512sum = "41fb3ae22ce4ac0e8bb6b9ae8db88a810af1001d944e3f1abc9e86824ae4be31347e3e3a70425ab12271c6b7eeef552f00164ef23cfffa2551c3c9d1fe5ab91f" recipe = HostPython3RecipePinned() ================================================ FILE: contrib/android/p4a_recipes/libffi/__init__.py ================================================ import os from pythonforandroid.recipes.libffi import LibffiRecipe from pythonforandroid.util import load_source util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py')) assert LibffiRecipe._version == "v3.4.2" assert LibffiRecipe.depends == [] assert LibffiRecipe.python_depends == [] class LibffiRecipePinned(util.InheritedRecipeMixin, LibffiRecipe): version = "v3.4.8" sha512sum = "064a43ddae005f3d0fa56db4da6071fae93aaae87a755b84888c0cb9c8fa2fe9bb452b3d9a382fab64c442c19d98a20ba15b8be92eba7bf3773815b31fb7824c" recipe = LibffiRecipePinned() ================================================ FILE: contrib/android/p4a_recipes/libiconv/__init__.py ================================================ import os from pythonforandroid.recipes.libiconv import LibIconvRecipe from pythonforandroid.util import load_source util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py')) assert LibIconvRecipe._version == "1.16" assert LibIconvRecipe.depends == [] assert LibIconvRecipe.python_depends == [] class LibIconvRecipePinned(util.InheritedRecipeMixin, LibIconvRecipe): version = "1.18" sha512sum = "a55eb3b7b785a78ab8918db8af541c9e11deb5ff4f89d54483287711ed797d87848ce0eafffa7ce26d9a7adb4b5a9891cb484f94bd4f51d3ce97a6a47b4c719a" recipe = LibIconvRecipePinned() ================================================ FILE: contrib/android/p4a_recipes/libsecp256k1/__init__.py ================================================ from pythonforandroid.recipes.libsecp256k1 import LibSecp256k1Recipe assert LibSecp256k1Recipe.depends == [] assert LibSecp256k1Recipe.python_depends == [] class LibSecp256k1RecipePinned(LibSecp256k1Recipe): version = "1a53f4961f337b4d166c25fce72ef0dc88806618" url = "https://github.com/bitcoin-core/secp256k1/archive/{version}.zip" sha512sum = "4072e45517bc1bb416250bc8e4fa4ed94f83b4eebbe25a70925fd7cc9759df3edbce64ab0116519c335f82353f6a029cde92018ed7116f2f85c8092a9adeb532" recipe = LibSecp256k1RecipePinned() ================================================ FILE: contrib/android/p4a_recipes/libzbar/__init__.py ================================================ import os from pythonforandroid.recipes.libzbar import LibZBarRecipe from pythonforandroid.util import load_source util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py')) assert LibZBarRecipe.depends == ['libiconv'] assert LibZBarRecipe.python_depends == [] class LibZBarRecipePinned(util.InheritedRecipeMixin, LibZBarRecipe): version = "bb05ec54eec57f8397cb13fb9161372a281a1219" url = "https://github.com/mchehab/zbar/archive/{version}.zip" sha512sum = "186312ef0a50404efef79a5fbed34534569fab2873a6bb6d2e3d8ea64fa461c5537ca4fb0e659670d72b021e514f8fd4651b1e85954bf987015d8eb2e6f68375" patches = [] # werror.patch not needed for modern zbar recipe = LibZBarRecipePinned() ================================================ FILE: contrib/android/p4a_recipes/openssl/__init__.py ================================================ import os from pythonforandroid.recipes.openssl import OpenSSLRecipe from pythonforandroid.util import load_source util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py')) assert OpenSSLRecipe._version == "3.0.18" assert OpenSSLRecipe.depends == [] assert OpenSSLRecipe.python_depends == [] class OpenSSLRecipePinned(util.InheritedRecipeMixin, OpenSSLRecipe): version = "3.0.18" sha512sum = "6bdd16f33b83ae2a12777230c4ff00d0595bbc00253ac8c3ac31e1375e818fc74d7f491bd2e507ff33cab9f0498cfb28fa8690f75a98663568d40901523cdf3c" recipe = OpenSSLRecipePinned() ================================================ FILE: contrib/android/p4a_recipes/packaging/__init__.py ================================================ from pythonforandroid.recipes.packaging import PackagingRecipe assert PackagingRecipe._version == "21.3" assert PackagingRecipe.depends == ["setuptools", "pyparsing", "python3"] assert PackagingRecipe.python_depends == [] class PackagingRecipePinned(PackagingRecipe): #version = "21.3" # note: 21.3 is the last version to use setup.py, so newer versions don't work. see comment for PyparsingRecipePinned sha512sum = "2e3aa276a4229ac7dc0654d586799473ced9761a83aa4159660d37ae1a2a8f30e987248dd0e260e2834106b589f259a57ce9936eef0dcc3c430a99ac6b663e05" recipe = PackagingRecipePinned() ================================================ FILE: contrib/android/p4a_recipes/ply/__init__.py ================================================ import os from pythonforandroid.recipes.ply import PlyRecipe from pythonforandroid.util import load_source util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py')) assert PlyRecipe._version == "3.11" assert PlyRecipe.depends == ['packaging', 'python3'] assert PlyRecipe.python_depends == [] class PlyRecipePinned(util.InheritedRecipeMixin, PlyRecipe): sha512sum = "37e39a4f930874933223be58a3da7f259e155b75135f1edd47069b3b40e5e96af883ebf1c8a1bbd32f914a9e92cfc12e29fec05cf61b518f46c1d37421b20008" recipe = PlyRecipePinned() ================================================ FILE: contrib/android/p4a_recipes/plyer/__init__.py ================================================ from pythonforandroid.recipe import PythonRecipe assert PythonRecipe.depends == ['python3'] assert PythonRecipe.python_depends == [] class PlyerRecipePinned(PythonRecipe): version = "5262087c85b2c82c69e702fe944069f1d8465fdf" url = "git+https://github.com/SomberNight/plyer" depends = ["setuptools"] recipe = PlyerRecipePinned() ================================================ FILE: contrib/android/p4a_recipes/pycparser/__init__.py ================================================ from pythonforandroid.recipes.pycparser import PycparserRecipe assert PycparserRecipe._version == "2.14" assert PycparserRecipe.depends == ['setuptools', 'python3'] assert PycparserRecipe.python_depends == [] class PycparserRecipePinned(PycparserRecipe): version = "2.22" sha512sum = "c9a81c78d87162f71281a32a076b279f4f7f2e17253fe14c89c6db5f9b3554a6563ff700c385549a8b51ef8832f99f7bb4ac07f22754c7c475dd91feeb0cf87f" recipe = PycparserRecipePinned() ================================================ FILE: contrib/android/p4a_recipes/pycryptodomex/__init__.py ================================================ from pythonforandroid.recipe import PythonRecipe assert PythonRecipe.depends == ['python3'] assert PythonRecipe.python_depends == [] class PycryptodomexRecipe(PythonRecipe): version = "3.23.0" sha512sum = "951cebaad2e19b9f9d04fe85c73ab1ff8b515069c1e0e8e3cd6845ec9ccd5ef3e5737259e0934ed4a6536e289dee6aabac58e1c822a5a6393e86b482c60afc89" url = "https://github.com/Legrandin/pycryptodome/archive/v{version}x.tar.gz" depends = ["setuptools", "cffi"] recipe = PycryptodomexRecipe() ================================================ FILE: contrib/android/p4a_recipes/pyjnius/__init__.py ================================================ import os from pythonforandroid.recipes.pyjnius import PyjniusRecipe from pythonforandroid.util import load_source util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py')) assert PyjniusRecipe._version == "1.5.0" assert PyjniusRecipe.depends == [('genericndkbuild', 'sdl2', 'qt6'), 'six', 'python3'] assert PyjniusRecipe.python_depends == [] class PyjniusRecipePinned(util.InheritedRecipeMixin, PyjniusRecipe): version = "1.6.1" sha512sum = "deb5ac566479111c6f4c6adb895821b263d72bf88414fb093bdfd5ad5d0b7aea56b53d5ef0967e28db360f4fb6fb1c2264123f15c747884799df55848191c424" recipe = PyjniusRecipePinned() ================================================ FILE: contrib/android/p4a_recipes/pyparsing/__init__.py ================================================ from pythonforandroid.recipes.pyparsing import PyparsingRecipe assert PyparsingRecipe._version == "3.0.7" assert PyparsingRecipe.depends == ["setuptools", "python3"] assert PyparsingRecipe.python_depends == [] class PyparsingRecipePinned(PyparsingRecipe): #version = "3.0.7" # note: 3.0.7 is the last version to use setup.py, so newer versions don't work, # as p4a runs "$ python3 setup.py install". This is only going become a larger problem, needs fix upstream. # see https://github.com/kivy/python-for-android/blob/be3de2e28e5a52d5f8949f3969f8a3b7f9eb3cba/pythonforandroid/recipe.py#L983 # - but maybe upstream p4a already has a workaround? # see "PyProjectRecipe" from https://github.com/kivy/python-for-android/pull/3007 sha512sum = "1e692f4cdaa6b6e8ca2729d0a3e2ba16d978f1957c538b6de3a4220ec7d996bdbe87c41c43abab851fffa3b0498a05841373e435602917b8c095042e273badb5" recipe = PyparsingRecipePinned() ================================================ FILE: contrib/android/p4a_recipes/pyqt6/__init__.py ================================================ import os from pythonforandroid.recipes.pyqt6 import PyQt6Recipe from pythonforandroid.util import load_source util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py')) assert PyQt6Recipe._version == "6.4.2" assert PyQt6Recipe.depends == ['qt6', 'pyjnius', 'setuptools', 'pyqt6sip', 'hostpython3', 'pyqt_builder'] assert PyQt6Recipe.python_depends == [] class PyQt6RecipePinned(util.InheritedRecipeMixin, PyQt6Recipe): sha512sum = "51e5f0d028ee7984876da1653cb135d61e2c402f18b939a92477888cc7c86d3bc2889477403dee6b3d9f66519ee3236d344323493b4c2c2e658e1637b10e53bf" recipe = PyQt6RecipePinned() ================================================ FILE: contrib/android/p4a_recipes/pyqt6sip/__init__.py ================================================ import os from pythonforandroid.recipes.pyqt6sip import PyQt6SipRecipe from pythonforandroid.util import load_source util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py')) assert PyQt6SipRecipe._version == "13.5.1" assert PyQt6SipRecipe.depends == ['setuptools', 'python3'] assert PyQt6SipRecipe.python_depends == [] class PyQt6SipRecipePinned(util.InheritedRecipeMixin, PyQt6SipRecipe): sha512sum = "1e4170d167a326afe6df86e4a35e209299548054981cb2e5d56da234ef9db4d8594bcb05b6be363c3bc6252776ae9de63d589a3d9f33fba8250d39cdb5e9061a" recipe = PyQt6SipRecipePinned() ================================================ FILE: contrib/android/p4a_recipes/pyqt_builder/__init__.py ================================================ from pythonforandroid.recipes.pyqt_builder import PyQtBuilderRecipe assert PyQtBuilderRecipe._version == "1.15.1" assert PyQtBuilderRecipe.depends == ["sip", "packaging", "python3"] assert PyQtBuilderRecipe.python_depends == [] class PyQtBuilderRecipePinned(PyQtBuilderRecipe): sha512sum = "61ee73b6bb922c04739da60025ab50d35d345d2e298943305fcbd3926cda31d732cc5e5b0dbfc39f5eb85c0f0b091b8c3f5fee00dcc240d7849c5c4191c1368a" recipe = PyQtBuilderRecipePinned() ================================================ FILE: contrib/android/p4a_recipes/python3/__init__.py ================================================ import os from pythonforandroid.recipes.python3 import Python3Recipe from pythonforandroid.util import load_source util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py')) assert Python3Recipe.depends == ['hostpython3', 'sqlite3', 'openssl', 'libffi'] assert Python3Recipe.python_depends == [] class Python3RecipePinned(util.InheritedRecipeMixin, Python3Recipe): # PYTHON_VERSION= # < line here so that I can grep the codebase and teleport here version = "3.11.14" sha512sum = "41fb3ae22ce4ac0e8bb6b9ae8db88a810af1001d944e3f1abc9e86824ae4be31347e3e3a70425ab12271c6b7eeef552f00164ef23cfffa2551c3c9d1fe5ab91f" recipe = Python3RecipePinned() ================================================ FILE: contrib/android/p4a_recipes/qt6/__init__.py ================================================ import os from pythonforandroid.recipes.qt6 import Qt6Recipe from pythonforandroid.util import load_source util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py')) assert Qt6Recipe._version == "6.4.3" # assert Qt6Recipe._version == "6.5.3" assert Qt6Recipe.depends == ['python3', 'hostqt6'] assert Qt6Recipe.python_depends == [] class Qt6RecipePinned(util.InheritedRecipeMixin, Qt6Recipe): sha512sum = "0bdbe8b9a43390c98cf19e851ec5394bc78438d227cf9d0d7a3748aee9a32a7f14fc46f52d4fa283819f21413567080aee7225c566af5278557f5e1992674da3" # sha512sum = "ca8ea3b81c121886636988275f7fa8ae6d19f7be02669e63ab19b4285b611057a41279db9532c25ae87baa3904b010e1db68b899cd0eda17a5a8d3d87098b4d5" recipe = Qt6RecipePinned() ================================================ FILE: contrib/android/p4a_recipes/setuptools/__init__.py ================================================ from pythonforandroid.recipes.setuptools import SetuptoolsRecipe assert SetuptoolsRecipe._version == "51.3.3" assert SetuptoolsRecipe.depends == ['python3'] assert SetuptoolsRecipe.python_depends == [] class SetuptoolsRecipePinned(SetuptoolsRecipe): sha512sum = "5a3572466a68c6f650111448ce3343f64c62044650bb8635edbff97e2bc7b216b8bbe3b4e3bccf34e6887f3bedc911b27ca5f9a515201cae49cf44fbacf03345" recipe = SetuptoolsRecipePinned() ================================================ FILE: contrib/android/p4a_recipes/sip/__init__.py ================================================ from pythonforandroid.recipes.sip import SipRecipe assert SipRecipe._version == "6.7.9" assert SipRecipe.depends == ["setuptools", "packaging", "tomli", "ply", "python3"], SipRecipe.depends assert SipRecipe.python_depends == [] class SipRecipePinned(SipRecipe): sha512sum = "bb9d0d0d92002b6fd33f7e8ebe8cd62456dacc16b5734b73760b1ba14fb9b1f2b9b6640b40196c6cf5f345e1afde48bdef39675c4d3480041771325d4cf3c233" recipe = SipRecipePinned() ================================================ FILE: contrib/android/p4a_recipes/six/__init__.py ================================================ from pythonforandroid.recipes.six import SixRecipe assert SixRecipe._version == "1.15.0" assert SixRecipe.depends == ['setuptools', 'python3'] assert SixRecipe.python_depends == [] class SixRecipePinned(SixRecipe): version = "1.17.0" sha512sum = "fcfa58b03877ac3ac00a4f85b5fea4fecb2a010244451aa95013637a0aa21529f3dcfe25c0a07c72da46da1fa12bc0c16b6c641c40c6ab2133e5b5cbb5a71e4b" recipe = SixRecipePinned() ================================================ FILE: contrib/android/p4a_recipes/sqlite3/__init__.py ================================================ import os from pythonforandroid.recipes.sqlite3 import Sqlite3Recipe from pythonforandroid.util import load_source util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py')) assert Sqlite3Recipe._version == "3.35.5" assert Sqlite3Recipe.depends == [] assert Sqlite3Recipe.python_depends == [] class Sqlite3RecipePinned(util.InheritedRecipeMixin, Sqlite3Recipe): version = "3.50.0" url = 'https://www.sqlite.org/2025/sqlite-amalgamation-3500000.zip' sha512sum = "0fd87f2b8140300ce165600f6708aafef19041a181e9f00ed14f7aeaa3c06805c8c54c53751a9ce74d4d666f018ca6f48e3f5b5c874ccb9e1424a528c92326f0" recipe = Sqlite3RecipePinned() ================================================ FILE: contrib/android/p4a_recipes/toml/__init__.py ================================================ from pythonforandroid.recipes.toml import TomlRecipe assert TomlRecipe._version == "0.10.2" assert TomlRecipe.depends == ["setuptools", "python3"] assert TomlRecipe.python_depends == [] class TomlRecipePinned(TomlRecipe): sha512sum = "ede2c8fed610a3827dba828f6e7ab7a8dbd5745e8ef7c0cd955219afdc83b9caea714deee09e853627f05ad1c525dc60426a6e9e16f58758aa028cb4d3db4b39" recipe = TomlRecipePinned() ================================================ FILE: contrib/android/p4a_recipes/tomli/__init__.py ================================================ from pythonforandroid.recipes.tomli import TomliRecipe assert TomliRecipe._version == "2.0.1" assert TomliRecipe.depends == ["setuptools", "python3"] assert TomliRecipe.python_depends == [] class TomliRecipePinned(TomliRecipe): #version = "2.0.1" # note: can't be easily updated as base recipe has version number hardcoded in custom "patch"-like setup.py sha512sum = "fd410039e255e2b3359e999d69a5a2d38b9b89b77e8557f734f2621dfbd5e1207e13aecc11589197ec22594c022f07f41b4cfe486a3a719281a595c95fd19ecf" recipe = TomliRecipePinned() ================================================ FILE: contrib/android/p4a_recipes/util.py ================================================ import os class InheritedRecipeMixin: def get_recipe_dir(self): """This is used to replace pythonforandroid.recipe.Recipe.get_recipe_dir. If one of our local recipes inherits from a built-in p4a recipe, this override ensures that potential patches and other local files used by the recipe will be looked for in the built-in recipe's folder. """ return os.path.join(self.ctx.root_dir, 'recipes', self.name) ================================================ FILE: contrib/apparmor/README.md ================================================ # Electrum AppArmor Profiles AppArmor is a Mandatory Access Control (MAC) system which confines programs to a limited set of resources. AppArmor confinement is provided via profiles loaded into the kernel. ## Installation Copy the AppArmor profile from `contrib/apparmor/apparmor.d/` to `/etc/apparmor.d/`: ``` sudo cp -R -L contrib/apparmor/apparmor.d/* /etc/apparmor.d ``` Reload the AppArmor profiles to apply the changes: ``` sudo systemctl reload apparmor ``` Verify that the profile is loaded: ``` sudo apparmor_status ``` Look for the entry corresponding to `electrum` ## Usage After installing the AppArmor profile, electrum will be restricted to the permissions specified in the profile. ## Compatibility The help tab may not function as expected as browser permissions can be tricky (Tarball Binaries) These AppArmor profiles have been tested on the following operating systems: ``` Debian 12 Ubuntu 23.10 Kali Linux 6.6 ``` ================================================ FILE: contrib/apparmor/apparmor.d/abstractions/electrum ================================================ include include include include include include include include include include include include if exists include if exists include if exists include if exists owner @{PROC}/@{pid}/{mounts,fd/} r, /{usr/,}sbin/ldconfig ix, /{usr/,}bin/{file,dash,dirname,uname} rix, /{usr/,}bin/@{multiarch}-gcc-8 ix, /{usr/,}bin/@{multiarch}-ld.bfd ix, /etc/mime.types r, @{system_share_dirs}/{mime,icons}/{**,} r, /dev/bus/usb/ r, /dev/bus/usb/** rw, @{sys}/class/ r, @{sys}/bus/ r, /etc/udev/udev.conf r, /etc/magic r, @{sys}/devices/pci*/**/usb*/**{busnum,devnum,descriptors,speed,bConfigurationValue} r, /dev/ r, /{var/,}run/udev/data/* r, @{sys}/bus/usb/devices/ r, /{usr/,}/bin/uname rix, owner @{user_share_dirs}/mime/** r, /{,run/}user/**/dconf/* rw, /{var/,}lib/dbus/** r, /etc/apt/apt.conf.d/ r, /etc/machine-id r, /{usr/,}bin/xdg-open ix, /{usr/,}bin/evince ix, ================================================ FILE: contrib/apparmor/apparmor.d/electrum.appimage ================================================ # Credits : Mikhail Morfikov abi , include @{exec_path} = /{usr/,}bin/fusermount{,3} profile fusermount @{exec_path} { include include # To mount anything: # fusermount: mount failed: Operation not permitted capability sys_admin, # For jmtpfs capability dac_read_search, @{exec_path} mr, # Where to mount ISO files owner @{HOME}/*/ rw, owner @{HOME}/*/*/ rw, owner @{HOME}/.cache/**/ rw, # Be able to mount ISO images mount fstype={fuse,fuse.*}, unmount fstype={fuse,fuse.*}, /etc/fuse.conf r, /dev/fuse rw, @{PROC}/@{pid}/mounts r, include if exists } ================================================ FILE: contrib/apparmor/apparmor.d/usr.local.bin.electrum ================================================ #Credits: Anton Nesterov abi , include @{electrum_exec_path} = /{usr/,usr/local/,*/*/.local/,}bin/electrum profile electrum @{electrum_exec_path} { include @{electrum_exec_path} mr, owner @{HOME}/.electrum/{**,} rw, owner @{HOME}/.local/{**,} mrw, } ================================================ FILE: contrib/ban_unicode.py ================================================ #!/usr/bin/env python3 # # Copyright (C) 2025 The Electrum developers # Distributed under the MIT software license, see the accompanying # file LICENCE or http://www.opensource.org/licenses/mit-license.php # # This script scans the whole codebase for unicode characters and # errors if it finds any, unless the character is specifically whitelisted below. # The motivation is to protect against homoglyph attacks, invisible unicode characters, # bidirectional and other control characters, and other malicious unicode usage. # Given that we mostly expect to use ASCII characters in the source code, # the most robust and generic fix seems to be to just ban all unicode usage. import os.path import subprocess import sys project_root = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) os.chdir(project_root) EXCLUDE_PATH_PREFIX = { "electrum/wordlist/", "fastlane/", "tests/", } EXCLUDE_EXTENSIONS = { ".jpg", ".jpeg", ".png", ".ttf", ".otf", ".pdn", ".icns", ".ico", ".gif", } UNICODE_WHITELIST = { "💬", "🗯", "⚠", chr(0xfe0f), "✓", "▷", "▽", "…", "•", "█", "™", "≈", "á", "é", "’", "│", "─", "└", "├", "📋", } exit_code = 0 bfiles = subprocess.check_output(["git", "ls-files"]) bfiles = bfiles.decode("utf-8") for file_path in bfiles.splitlines(): if os.path.isdir(file_path): continue if any(file_path.startswith(pattern) for pattern in EXCLUDE_PATH_PREFIX): continue _fname, ext = os.path.splitext(file_path) if ext in EXCLUDE_EXTENSIONS: continue # open file try: with open(file_path, "r", encoding="utf-8") as f: for line_no, line in enumerate(f.read().splitlines()): for char in line: if ord(char)>0x7f and char not in UNICODE_WHITELIST: print(f"{file_path}:{line_no}. {line=}. hex={hex(ord(char))}. {char=}") exit_code = 1 except UnicodeDecodeError as e: raise Exception(f"cannot parse file {file_path=}") from e sys.exit(exit_code) ================================================ FILE: contrib/build-linux/appimage/.dockerignore ================================================ build/ .cache/ ================================================ FILE: contrib/build-linux/appimage/Dockerfile ================================================ # Note: we deliberately use an old Debian stable as base image. # from https://docs.appimage.org/introduction/concepts.html : # "[AppImages] should be built on the oldest possible system, allowing them to run on newer system[s]" FROM debian:bullseye@sha256:cf48c31af360e1c0a0aedd33aae4d928b68c2cdf093f1612650eb1ff434d1c34 ENV LC_ALL=C.UTF-8 LANG=C.UTF-8 ENV DEBIAN_FRONTEND=noninteractive # need ca-certificates before using snapshot packages RUN apt update -qq > /dev/null && apt install -qq --yes --no-install-recommends \ ca-certificates # pin the distro packages COPY apt.sources.list /etc/apt/sources.list COPY apt.preferences /etc/apt/preferences.d/snapshot RUN apt-get update -q && \ apt-get install -qy --allow-downgrades \ sudo \ git \ wget \ python3 \ make \ autotools-dev \ autoconf \ libtool \ autopoint \ pkg-config \ xz-utils \ libssl-dev \ libssl1.1 \ openssl \ zlib1g-dev \ libffi-dev \ libncurses5-dev \ libncurses5 \ libtinfo-dev \ libtinfo5 \ libsqlite3-dev \ libusb-1.0-0-dev \ libudev-dev \ libudev1 \ gettext \ libdbus-1-3 \ xutils-dev \ libxkbcommon0 \ libxkbcommon-x11-0 \ libxcb1-dev \ libxcb-xinerama0 \ libxcb-randr0 \ libxcb-render0 \ libxcb-shm0 \ libxcb-shape0 \ libxcb-sync1 \ libxcb-xfixes0 \ libxcb-xkb1 \ libxcb-icccm4 \ libxcb-image0 \ libxcb-keysyms1 \ libxcb-util1 \ libxcb-render-util0 \ libxcb-cursor0 \ libx11-xcb1 \ libc6-dev \ libc6 \ libc-dev-bin \ libv4l-dev \ libjpeg62-turbo-dev \ libx11-dev \ desktop-file-utils \ && \ rm -rf /var/lib/apt/lists/* && \ apt-get autoremove -y && \ apt-get clean # create new user to avoid using root; but with sudo access and no password for convenience. ARG UID=1000 RUN if [ "$UID" != "0" ] ; then useradd --uid $UID --create-home --shell /bin/bash "user" ; fi RUN usermod -append --groups sudo $(id -nu $UID || echo "user") RUN echo "%sudo ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers RUN HOME_DIR=$(getent passwd $UID | cut -d: -f6) ENV WORK_DIR="${HOME_DIR}/wspace" \ PATH="${HOME_DIR}/.local/bin:${PATH}" WORKDIR ${WORK_DIR} RUN chown --recursive ${UID} ${WORK_DIR} USER ${UID} ================================================ FILE: contrib/build-linux/appimage/README.md ================================================ AppImage binary for Electrum ============================ ✓ _This binary should be reproducible, meaning you should be able to generate binaries that match the official releases._ - _Minimum supported target system (i.e. what end-users need): x86_64, glibc 2.31_ This assumes an Ubuntu host, but it should not be too hard to adapt to another similar system. The host architecture should be x86_64 (amd64). We currently only build a single AppImage, for x86_64 architecture. Help to adapt these scripts to build for (some flavor of) ARM would be welcome, see [issue #5159](https://github.com/spesmilo/electrum/issues/5159). 1. Install Docker See [`contrib/docker_notes.md`](../../docker_notes.md). (worth reading even if you already have docker) 2. Build binary ``` $ ./build.sh ``` If you want reproducibility, try instead e.g.: ``` $ ELECBUILD_COMMIT=HEAD ./build.sh ``` 3. The generated binary is in `./dist`. ## FAQ ### How can I see what is included in the AppImage? Execute the binary as follows: `./electrum*.AppImage --appimage-extract` ### How to investigate diff between binaries if reproducibility fails? ``` cd dist/ ./electrum-*-x86_64.AppImage1 --appimage-extract mv squashfs-root/ squashfs-root1/ ./electrum-*-x86_64.AppImage2 --appimage-extract mv squashfs-root/ squashfs-root2/ $(cd squashfs-root1; find -type f -exec sha256sum '{}' \; > ./../sha256sum1) $(cd squashfs-root2; find -type f -exec sha256sum '{}' \; > ./../sha256sum2) diff sha256sum1 sha256sum2 > d cat d ``` For file metadata, e.g. timestamps: ``` rsync -n -a -i --delete squashfs-root1/ squashfs-root2/ ``` Useful binary comparison tools: - vbindiff - diffoscope ================================================ FILE: contrib/build-linux/appimage/apprun.sh ================================================ #!/bin/bash set -e APPDIR="$(dirname "$(readlink -e "$0")")" export LD_LIBRARY_PATH="${APPDIR}/usr/lib/:${APPDIR}/usr/lib/x86_64-linux-gnu${LD_LIBRARY_PATH+:$LD_LIBRARY_PATH}" export PATH="${APPDIR}/usr/bin:${PATH}" export LDFLAGS="-L${APPDIR}/usr/lib/x86_64-linux-gnu -L${APPDIR}/usr/lib" exec "${APPDIR}/usr/bin/python3" -s "${APPDIR}/usr/bin/electrum" "$@" ================================================ FILE: contrib/build-linux/appimage/apt.preferences ================================================ Package: * Pin: origin "snapshot.debian.org" Pin-Priority: 1001 ================================================ FILE: contrib/build-linux/appimage/apt.sources.list ================================================ deb https://snapshot.debian.org/archive/debian/20250530T143637Z/ bullseye main deb-src https://snapshot.debian.org/archive/debian/20250530T143637Z/ bullseye main ================================================ FILE: contrib/build-linux/appimage/build.sh ================================================ #!/bin/bash # # env vars: # - ELECBUILD_NOCACHE: if set, forces rebuild of docker image # - ELECBUILD_COMMIT: if set, do a fresh clone and git checkout set -e PROJECT_ROOT="$(dirname "$(readlink -e "$0")")/../../.." PROJECT_ROOT_OR_FRESHCLONE_ROOT="$PROJECT_ROOT" CONTRIB="$PROJECT_ROOT/contrib" CONTRIB_APPIMAGE="$CONTRIB/build-linux/appimage" DISTDIR="$PROJECT_ROOT/dist" BUILD_UID=$(/usr/bin/stat -c %u "$PROJECT_ROOT") . "$CONTRIB"/build_tools_util.sh DOCKER_BUILD_FLAGS="" if [ ! -z "$ELECBUILD_NOCACHE" ] ; then info "ELECBUILD_NOCACHE is set. forcing rebuild of docker image." DOCKER_BUILD_FLAGS="--pull --no-cache" fi if [ -z "$ELECBUILD_COMMIT" ] ; then # local dev build DOCKER_BUILD_FLAGS="$DOCKER_BUILD_FLAGS --build-arg UID=$BUILD_UID" fi info "building docker image." docker build \ $DOCKER_BUILD_FLAGS \ -t electrum-appimage-builder-img \ "$CONTRIB_APPIMAGE" # maybe do fresh clone if [ ! -z "$ELECBUILD_COMMIT" ] ; then info "ELECBUILD_COMMIT=$ELECBUILD_COMMIT. doing fresh clone and git checkout." FRESH_CLONE="/tmp/electrum_build/appimage/fresh_clone/electrum" rm -rf "$FRESH_CLONE" 2>/dev/null || ( info "we need sudo to rm prev FRESH_CLONE." && sudo rm -rf "$FRESH_CLONE" ) umask 0022 git clone "$PROJECT_ROOT" "$FRESH_CLONE" cd "$FRESH_CLONE" git checkout "$ELECBUILD_COMMIT" PROJECT_ROOT_OR_FRESHCLONE_ROOT="$FRESH_CLONE" else info "not doing fresh clone." fi # build the type2-runtime binary, this build step uses a separate docker container # defined in the type2-runtime repo (patched with type2-runtime-reproducible-build.patch) "$PROJECT_ROOT_OR_FRESHCLONE_ROOT/contrib/build-linux/appimage/make_type2_runtime.sh" || fail "Error building type2-runtime." DOCKER_RUN_FLAGS="" if sh -c ": >/dev/tty" >/dev/null 2>/dev/null; then info "/dev/tty is available and usable" DOCKER_RUN_FLAGS="-it" fi info "building binary..." # check uid and maybe chown. see #8261 if [ ! -z "$ELECBUILD_COMMIT" ] ; then # fresh clone (reproducible build) if [ $(id -u) != "1000" ] || [ $(id -g) != "1000" ] ; then info "need to chown -R FRESH_CLONE dir. prompting for sudo." sudo chown -R 1000:1000 "$FRESH_CLONE" fi fi docker run $DOCKER_RUN_FLAGS \ --name electrum-appimage-builder-cont \ -v "$PROJECT_ROOT_OR_FRESHCLONE_ROOT":/opt/electrum \ --rm \ --workdir /opt/electrum/contrib/build-linux/appimage \ electrum-appimage-builder-img \ ./make_appimage.sh # make sure resulting binary location is independent of fresh_clone if [ ! -z "$ELECBUILD_COMMIT" ] ; then mkdir --parents "$DISTDIR/" cp -f "$FRESH_CLONE/dist"/* "$DISTDIR/" fi ================================================ FILE: contrib/build-linux/appimage/make_appimage.sh ================================================ #!/bin/bash set -e PROJECT_ROOT="$(dirname "$(readlink -e "$0")")/../../.." CONTRIB="$PROJECT_ROOT/contrib" CONTRIB_APPIMAGE="$CONTRIB/build-linux/appimage" DISTDIR="$PROJECT_ROOT/dist" BUILDDIR="$CONTRIB_APPIMAGE/build/appimage" APPDIR="$BUILDDIR/electrum.AppDir" CACHEDIR="$CONTRIB_APPIMAGE/.cache/appimage" TYPE2_RUNTIME_REPO_DIR="$CACHEDIR/type2-runtime" export DLL_TARGET_DIR="$CACHEDIR/dlls" PIP_CACHE_DIR="$CONTRIB_APPIMAGE/.cache/pip_cache" . "$CONTRIB"/build_tools_util.sh git -C "$PROJECT_ROOT" rev-parse 2>/dev/null || fail "Building outside a git clone is not supported." export GCC_STRIP_BINARIES="1" # pinned versions PYTHON_VERSION=3.12.11 PY_VER_MAJOR="3.12" # as it appears in fs paths PKG2APPIMAGE_COMMIT="a9c85b7e61a3a883f4a35c41c5decb5af88b6b5d" VERSION=$(git describe --tags --dirty --always) APPIMAGE="$DISTDIR/electrum-$VERSION-x86_64.AppImage" rm -rf "$BUILDDIR" mkdir -p "$APPDIR" "$CACHEDIR" "$PIP_CACHE_DIR" "$DISTDIR" "$DLL_TARGET_DIR" # potential leftover from setuptools that might make pip put garbage in binary rm -rf "$PROJECT_ROOT/build" info "downloading some dependencies." download_if_not_exist "$CACHEDIR/functions.sh" "https://raw.githubusercontent.com/AppImage/pkg2appimage/$PKG2APPIMAGE_COMMIT/functions.sh" verify_hash "$CACHEDIR/functions.sh" "8f67711a28635b07ce539a9b083b8c12d5488c00003d6d726c7b134e553220ed" download_if_not_exist "$CACHEDIR/appimagetool" "https://github.com/AppImage/appimagetool/releases/download/1.9.0/appimagetool-x86_64.AppImage" verify_hash "$CACHEDIR/appimagetool" "46fdd785094c7f6e545b61afcfb0f3d98d8eab243f644b4b17698c01d06083d1" # note: desktop-file-utils in the docker image is needed to run desktop-file-validate for appimagetool <= 1.9.0, so it can be removed once # appimagetool tags a new release (see https://github.com/AppImage/appimagetool/pull/47) download_if_not_exist "$CACHEDIR/Python-$PYTHON_VERSION.tar.xz" "https://www.python.org/ftp/python/$PYTHON_VERSION/Python-$PYTHON_VERSION.tar.xz" verify_hash "$CACHEDIR/Python-$PYTHON_VERSION.tar.xz" "c30bb24b7f1e9a19b11b55a546434f74e739bb4c271a3e3a80ff4380d49f7adb" info "building python." tar xf "$CACHEDIR/Python-$PYTHON_VERSION.tar.xz" -C "$CACHEDIR" ( if [ -f "$CACHEDIR/Python-$PYTHON_VERSION/python" ]; then info "python already built, skipping" exit 0 fi cd "$CACHEDIR/Python-$PYTHON_VERSION" LC_ALL=C export BUILD_DATE=$(date -u -d "@$SOURCE_DATE_EPOCH" "+%b %d %Y") LC_ALL=C export BUILD_TIME=$(date -u -d "@$SOURCE_DATE_EPOCH" "+%H:%M:%S") # Patches taken from Ubuntu http://archive.ubuntu.com/ubuntu/pool/main/p/python3.11/python3.11_3.11.6-3.debian.tar.xz patch -p1 < "$CONTRIB_APPIMAGE/patches/python-3.11-reproducible-buildinfo.diff" ./configure \ --cache-file="$CACHEDIR/python.config.cache" \ --prefix="$APPDIR/usr" \ --enable-ipv6 \ --enable-shared \ -q make "-j$CPU_COUNT" -s || fail "Could not build Python" ) info "installing python." ( cd "$CACHEDIR/Python-$PYTHON_VERSION" make -s install > /dev/null || fail "Could not install Python" # When building in docker on macOS, python builds with .exe extension because the # case insensitive file system of macOS leaks into docker. This causes the build # to result in a different output on macOS compared to Linux. We simply patch # sysconfigdata to remove the extension. # Some more info: https://bugs.python.org/issue27631 sed -i -e 's/\.exe//g' "${APPDIR}/usr/lib/python${PY_VER_MAJOR}"/_sysconfigdata* ) if ls "$DLL_TARGET_DIR"/libsecp256k1.so.* 1> /dev/null 2>&1; then info "libsecp256k1 already built, skipping" else "$CONTRIB"/make_libsecp256k1.sh || fail "Could not build libsecp" fi cp -f "$DLL_TARGET_DIR"/libsecp256k1.so.* "$APPDIR/usr/lib/" || fail "Could not copy libsecp to its destination" if [ -f "$DLL_TARGET_DIR/libzbar.so.0" ]; then info "libzbar already built, skipping" else # note: could instead just use the libzbar0 pkg from debian/apt, but that is too old and missing fixes for CVE-2023-40889 "$CONTRIB"/make_zbar.sh || fail "Could not build zbar" fi cp -f "$DLL_TARGET_DIR/libzbar.so.0" "$APPDIR/usr/lib/" || fail "Could not copy libzbar to its destination" appdir_python() { env \ PYTHONNOUSERSITE=1 \ LD_LIBRARY_PATH="$APPDIR/usr/lib:$APPDIR/usr/lib/x86_64-linux-gnu${LD_LIBRARY_PATH+:$LD_LIBRARY_PATH}" \ "$APPDIR/usr/bin/python${PY_VER_MAJOR}" "$@" } python='appdir_python' info "installing pip." "$python" -m ensurepip break_legacy_easy_install info "preparing electrum-locale." ( "$CONTRIB/locale/build_cleanlocale.sh" # we want the binary to have only compiled (.mo) locale files; not source (.po) files rm -r "$PROJECT_ROOT/electrum/locale/locale"/*/electrum.po ) info "Installing build dependencies." # note: re pip installing from PyPI, # we prefer compiling C extensions ourselves, instead of using binary wheels, # hence "--no-binary :all:" flags. However, we specifically allow # - PyQt6, as it's harder to build from source # - cryptography, as it's harder to build from source # - the whole of "requirements-build-base.txt", which includes pip and friends, as it also includes "wheel", # and I am not quite sure how to break the circular dependence there (I guess we could introduce # "requirements-build-base-base.txt" with just wheel in it...) "$python" -m pip install --no-build-isolation --no-dependencies --no-warn-script-location \ --cache-dir "$PIP_CACHE_DIR" -r "$CONTRIB/deterministic-build/requirements-build-base.txt" "$python" -m pip install --no-build-isolation --no-dependencies --no-binary :all: --no-warn-script-location \ --cache-dir "$PIP_CACHE_DIR" -r "$CONTRIB/deterministic-build/requirements-build-appimage.txt" # opt out of compiling C extensions export YARL_NO_EXTENSIONS=1 export FROZENLIST_NO_EXTENSIONS=1 export ELECTRUM_ECC_DONT_COMPILE=1 info "installing electrum and its dependencies." "$python" -m pip install --no-build-isolation --no-dependencies --no-binary :all: --no-warn-script-location \ --cache-dir "$PIP_CACHE_DIR" -r "$CONTRIB/deterministic-build/requirements.txt" "$python" -m pip install --no-build-isolation --no-dependencies --no-binary :all: --only-binary PyQt6,PyQt6-Qt6,cryptography --no-warn-script-location \ --cache-dir "$PIP_CACHE_DIR" -r "$CONTRIB/deterministic-build/requirements-binaries.txt" "$python" -m pip install --no-build-isolation --no-dependencies --no-binary :all: --no-warn-script-location \ --cache-dir "$PIP_CACHE_DIR" -r "$CONTRIB/deterministic-build/requirements-hw.txt" "$python" -m pip install --no-build-isolation --no-dependencies --no-warn-script-location \ --cache-dir "$PIP_CACHE_DIR" "$PROJECT_ROOT" # was only needed during build time, not runtime "$python" -m pip uninstall -y Cython info "desktop integration." cp "$PROJECT_ROOT/electrum.desktop" "$APPDIR/electrum.desktop" cp "$PROJECT_ROOT/electrum/gui/icons/electrum.png" "$APPDIR/electrum.png" # add launcher cp "$CONTRIB_APPIMAGE/apprun.sh" "$APPDIR/AppRun" info "finalizing AppDir." ( export PKG2AICOMMIT="$PKG2APPIMAGE_COMMIT" . "$CACHEDIR/functions.sh" cd "$APPDIR" # copy system dependencies copy_deps; copy_deps; copy_deps move_lib # apply global appimage blacklist to exclude stuff # move usr/include out of the way to preserve usr/include/python${PY_VER_MAJOR}. mv usr/include usr/include.tmp delete_blacklisted mv usr/include.tmp usr/include ) info "Copying additional libraries" ( # On some systems it can cause problems to use the system libusb (on AppImage excludelist) cp -f /usr/lib/x86_64-linux-gnu/libusb-1.0.so "$APPDIR/usr/lib/libusb-1.0.so" || fail "Could not copy libusb" # some distros lack libxkbcommon-x11 cp -f /usr/lib/x86_64-linux-gnu/libxkbcommon-x11.so.0 "$APPDIR"/usr/lib/x86_64-linux-gnu || fail "Could not copy libxkbcommon-x11" # some distros lack some libxcb libraries (see https://github.com/Electron-Cash/Electron-Cash/issues/2196) cp -f /usr/lib/x86_64-linux-gnu/libxcb-* "$APPDIR"/usr/lib/x86_64-linux-gnu || fail "Could not copy libxcb" ) info "stripping binaries from debug symbols." # "-R .note.gnu.build-id" also strips the build id # "-R .comment" also strips the GCC version information strip_binaries() { chmod u+w -R "$APPDIR" { printf '%s\0' "$APPDIR/usr/bin/python${PY_VER_MAJOR}" find "$APPDIR" -type f -regex '.*\.so\(\.[0-9.]+\)?$' -print0 } | xargs -0 --no-run-if-empty --verbose strip -R .note.gnu.build-id -R .comment } strip_binaries remove_emptydirs() { find "$APPDIR" -type d -empty -print0 | xargs -0 --no-run-if-empty rmdir -vp --ignore-fail-on-non-empty } remove_emptydirs info "removing some unneeded stuff to decrease binary size." rm -rf "$APPDIR"/usr/{share,include} PYDIR="$APPDIR/usr/lib/python${PY_VER_MAJOR}" rm -rf "$PYDIR"/{test,ensurepip,lib2to3,idlelib,turtledemo} rm -rf "$PYDIR"/{ctypes,sqlite3,tkinter,unittest}/test rm -rf "$PYDIR"/distutils/{command,tests} rm -rf "$PYDIR"/config-3.*-x86_64-linux-gnu rm -rf "$PYDIR"/site-packages/{opt,pip,setuptools,wheel} rm -rf "$PYDIR"/site-packages/Cryptodome/SelfTest rm -rf "$PYDIR"/site-packages/{psutil,qrcode,websocket}/tests # rm lots of unused parts of Qt/PyQt. (assuming PyQt 6 layout) for component in connectivity declarative help location multimedia quickcontrols2 serialport webengine websockets xmlpatterns ; do rm -rf "$PYDIR"/site-packages/PyQt6/Qt6/translations/qt${component}_* rm -rf "$PYDIR"/site-packages/PyQt6/Qt6/resources/qt${component}_* done rm -rf "$PYDIR"/site-packages/PyQt6/Qt6/{qml,libexec} rm -rf "$PYDIR"/site-packages/PyQt6/{pyrcc*.so,pylupdate*.so,uic} rm -rf "$PYDIR"/site-packages/PyQt6/Qt6/plugins/{bearer,gamepads,geometryloaders,geoservices,playlistformats,position,renderplugins,sceneparsers,sensors,sqldrivers,texttospeech,webview} for component in Bluetooth Concurrent Designer Help Location NetworkAuth Nfc Positioning PositioningQuick Qml Quick Sensors SerialPort Sql Test Web Xml Labs ShaderTools SpatialAudio ; do rm -rf "$PYDIR"/site-packages/PyQt6/Qt6/lib/libQt6${component}* rm -rf "$PYDIR"/site-packages/PyQt6/Qt${component}* rm -rf "$PYDIR"/site-packages/PyQt6/bindings/Qt${component}* done for component in Qml Quick ; do rm -rf "$PYDIR"/site-packages/PyQt6/Qt6/lib/libQt6*${component}.so* done rm -rf "$PYDIR"/site-packages/PyQt6/Qt.so # these are deleted as they were not deterministic; and are not needed anyway find "$APPDIR" -path '*/__pycache__*' -delete # although note that *.dist-info might be needed by certain packages... # e.g. slip10 uses importlib that needs it for f in "$PYDIR"/site-packages/slip10-*.dist-info; do mv "$f" "$(echo "$f" | sed s/\.dist-info/\.dist-info2/)"; done rm -rf "$PYDIR"/site-packages/*.dist-info/ rm -rf "$PYDIR"/site-packages/*.egg-info/ for f in "$PYDIR"/site-packages/slip10-*.dist-info2; do mv "$f" "$(echo "$f" | sed s/\.dist-info2/\.dist-info/)"; done find -exec touch -h -d '2000-11-11T11:11:11+00:00' {} + info "creating the AppImage." ( cd "$BUILDDIR" cp "$CACHEDIR/appimagetool" "$CACHEDIR/appimagetool_copy" # zero out "appimage" magic bytes, as on some systems they confuse the linker sed -i 's|AI\x02|\x00\x00\x00|' "$CACHEDIR/appimagetool_copy" chmod +x "$CACHEDIR/appimagetool_copy" "$CACHEDIR/appimagetool_copy" --appimage-extract # We build a small wrapper for mksquashfs that removes the -mkfs-time option # as it conflicts with SOURCE_DATE_EPOCH. mv "$BUILDDIR/squashfs-root/usr/bin/mksquashfs" "$BUILDDIR/squashfs-root/usr/bin/mksquashfs_orig" cat > "$BUILDDIR/squashfs-root/usr/bin/mksquashfs" << EOF #!/bin/sh args=\$(echo "\$@" | sed -e 's/-mkfs-time 0//') "$BUILDDIR/squashfs-root/usr/bin/mksquashfs_orig" \$args EOF chmod +x "$BUILDDIR/squashfs-root/usr/bin/mksquashfs" env VERSION="$VERSION" ARCH=x86_64 ./squashfs-root/AppRun --runtime-file "$TYPE2_RUNTIME_REPO_DIR/runtime-x86_64" --no-appstream --verbose "$APPDIR" "$APPIMAGE" ) info "done." ls -la "$DISTDIR" sha256sum "$DISTDIR"/* ================================================ FILE: contrib/build-linux/appimage/make_type2_runtime.sh ================================================ #!/bin/bash set -e PROJECT_ROOT="$(dirname "$(readlink -e "$0")")/../../.." CONTRIB="$PROJECT_ROOT/contrib" CONTRIB_APPIMAGE="$CONTRIB/build-linux/appimage" # when bumping the runtime commit also check if the `type2-runtime-reproducible-build.patch` still works TYPE2_RUNTIME_COMMIT="5e7217b7cfeecee1491c2d251e355c3cf8ba6e4d" TYPE2_RUNTIME_REPO="https://github.com/AppImage/type2-runtime.git" . "$CONTRIB"/build_tools_util.sh TYPE2_RUNTIME_REPO_DIR="$PROJECT_ROOT/contrib/build-linux/appimage/.cache/appimage/type2-runtime" if [ -f "$TYPE2_RUNTIME_REPO_DIR/runtime-x86_64" ]; then info "type2-runtime already built, skipping" exit 0 fi clone_or_update_repo "$TYPE2_RUNTIME_REPO" "$TYPE2_RUNTIME_COMMIT" "$TYPE2_RUNTIME_REPO_DIR" # Apply patch to make runtime build reproducible info "Applying type2-runtime patch..." cd "$TYPE2_RUNTIME_REPO_DIR" git apply "$CONTRIB_APPIMAGE/patches/type2-runtime-reproducible-build.patch" || fail "Failed to apply runtime repo patch" info "building type2-runtime in build container..." cd "$TYPE2_RUNTIME_REPO_DIR/scripts/docker" env ARCH=x86_64 ./build-with-docker.sh mv "./runtime-x86_64" "$TYPE2_RUNTIME_REPO_DIR/" # clean up the empty created 'out' dir to prevent permission issues rm -rf "$TYPE2_RUNTIME_REPO_DIR/out" info "runtime build successful: $(sha256sum "$TYPE2_RUNTIME_REPO_DIR/runtime-x86_64")" ================================================ FILE: contrib/build-linux/appimage/patches/python-3.11-reproducible-buildinfo.diff ================================================ Description: Build reproduceable date and time into build info Build information is encoded into getbuildinfo.o at build time. Use the date and time from the debian changelog, to make this reproduceable. Forwarded: no --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -1248,6 +1248,8 @@ -DGITVERSION="\"`LC_ALL=C $(GITVERSION)`\"" \ -DGITTAG="\"`LC_ALL=C $(GITTAG)`\"" \ -DGITBRANCH="\"`LC_ALL=C $(GITBRANCH)`\"" \ + $(if $(BUILD_DATE),-DDATE='"$(BUILD_DATE)"') \ + $(if $(BUILD_TIME),-DTIME='"$(BUILD_TIME)"') \ -o $@ $(srcdir)/Modules/getbuildinfo.c Modules/getpath.o: $(srcdir)/Modules/getpath.c Python/frozen_modules/getpath.h Makefile $(PYTHON_HEADERS) ================================================ FILE: contrib/build-linux/appimage/patches/type2-runtime-reproducible-build.patch ================================================ From 0c54d91dd1d33235ae97566600e692edfb613642 Mon Sep 17 00:00:00 2001 From: f321x Date: Thu, 10 Jul 2025 17:45:20 +0200 Subject: [PATCH] make docker build reproducible attempts to make the docker build more reproducible by: * pinning the docker image (alpine:3.21) to a hash * version pinning the apk packages in the dockerfile * setting TZ, LC_ALL and SOURCE_DATE_EPOCH in the container * only building single threaded (make -j1) * use a fixed build directory in `build-runtime.sh` instead of mktemp * prevent linker from adding build id (-Wl,--build-id=none) * replace absolute build paths in debug info with relative paths (-fdebug-prefix-map=$(PWD)=.) * replace absolute paths in all compiler output with relative paths (-ffile-prefix-map=$(PWD)=.) * stop adding gnu-debuglink to runtime binary --- scripts/build-runtime.sh | 18 +++++++++++---- scripts/common/install-dependencies.sh | 2 +- scripts/docker/Dockerfile | 32 ++++++++++++++++++++++---- src/runtime/Makefile | 2 +- 4 files changed, 42 insertions(+), 12 deletions(-) diff --git a/scripts/build-runtime.sh b/scripts/build-runtime.sh index 3ce3b91..e11f082 100755 --- a/scripts/build-runtime.sh +++ b/scripts/build-runtime.sh @@ -8,8 +8,10 @@ set -euo pipefail out_dir="$(readlink -f "$(pwd)")"/out mkdir -p "$out_dir" -# we create a temporary build directory -build_dir="$(mktemp -d -t type2-runtime-build-XXXXXX)" +# we create a temporary build directory with a fixed name for reproducibility +build_dir="$(readlink -f "$(pwd)")"/build-runtime-temp +rm -rf "$build_dir" +mkdir -p "$build_dir" # since the plain ol' Makefile doesn't support out-of-source builds at all, we need to copy all the files cp -R src "$build_dir"/ @@ -17,13 +19,14 @@ cp -R src "$build_dir"/ pushd "$build_dir" pushd src/runtime/ -make -j"$(nproc)" runtime +make -j1 runtime file runtime objcopy --only-keep-debug runtime runtime.debug -strip --strip-debug --strip-unneeded runtime +# strip --strip-debug --strip-unneeded runtime +strip --strip-all runtime ls -lh runtime runtime.debug @@ -50,7 +53,7 @@ fi mv runtime runtime-"$architecture" mv runtime.debug runtime-"$architecture".debug -objcopy --add-gnu-debuglink runtime-"$architecture".debug runtime-"$architecture" +# objcopy --add-gnu-debuglink runtime-"$architecture".debug runtime-"$architecture" # "classic" magic bytes which cannot be embedded with compiler magic, always do AFTER strip # needs to be done after calls to objcopy, strip etc. @@ -61,3 +64,8 @@ cp runtime-"$architecture" "$out_dir"/ cp runtime-"$architecture".debug "$out_dir"/ ls -al "$out_dir" + +# cleanup +popd # return to build_dir +popd # return to original working directory +rm -rf "$build_dir" diff --git a/scripts/common/install-dependencies.sh b/scripts/common/install-dependencies.sh index 0e21cdb..5237079 100755 --- a/scripts/common/install-dependencies.sh +++ b/scripts/common/install-dependencies.sh @@ -39,7 +39,7 @@ tar xf 0.5.2.tar.gz pushd squashfuse-*/ ./autogen.sh ./configure LDFLAGS="-static" -make -j"$(nproc)" +make -j1 make install /usr/bin/install -c -m 644 ./*.h '/usr/local/include/squashfuse' popd diff --git a/scripts/docker/Dockerfile b/scripts/docker/Dockerfile index 07b6533..fba9c6e 100644 --- a/scripts/docker/Dockerfile +++ b/scripts/docker/Dockerfile @@ -1,13 +1,35 @@ -FROM alpine:3.21 +FROM alpine:3.21@sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c # includes dependencies from https://git.alpinelinux.org/aports/tree/main/fuse3/APKBUILD RUN apk add --no-cache \ - bash alpine-sdk util-linux strace file autoconf automake libtool xz \ - eudev-dev gettext-dev linux-headers meson \ - zstd-dev zstd-static zlib-dev zlib-static clang musl-dev mimalloc-dev + bash=5.2.37-r0 \ + alpine-sdk=1.1-r0 \ + util-linux=2.40.4-r1 \ + strace=6.12-r0 \ + file=5.46-r2 \ + autoconf=2.72-r0 \ + automake=1.17-r0 \ + libtool=2.4.7-r3 \ + xz=5.6.3-r1 \ + eudev-dev=3.2.14-r5 \ + gettext-dev=0.22.5-r0 \ + linux-headers=6.6-r1 \ + meson=1.6.1-r0 \ + zstd-dev=1.5.6-r2 \ + zstd-static=1.5.6-r2 \ + zlib-dev=1.3.1-r2 \ + zlib-static=1.3.1-r2 \ + clang19=19.1.4-r0 \ + musl-dev=1.2.5-r9 \ + mimalloc2-dev=2.1.7-r0 COPY scripts/common/install-dependencies.sh /tmp/scripts/common/install-dependencies.sh COPY patches/ /tmp/patches/ +# Set environment variables for reproducible build +ENV SOURCE_DATE_EPOCH=1640995200 +ENV TZ=UTC +ENV LC_ALL=C + WORKDIR /tmp -RUN bash scripts/common/install-dependencies.sh +RUN bash scripts/common/install-dependencies.sh \ No newline at end of file diff --git a/src/runtime/Makefile b/src/runtime/Makefile index 9fd4165..3a3cbaa 100644 --- a/src/runtime/Makefile +++ b/src/runtime/Makefile @@ -1,6 +1,6 @@ GIT_COMMIT := $(shell cat version) CC = clang -CFLAGS = -std=gnu99 -Os -D_FILE_OFFSET_BITS=64 -DGIT_COMMIT=\"$(GIT_COMMIT)\" -T data_sections.ld -ffunction-sections -fdata-sections -Wl,--gc-sections -static -Wall -Werror -static-pie +CFLAGS = -std=gnu99 -Os -D_FILE_OFFSET_BITS=64 -DGIT_COMMIT=\"$(GIT_COMMIT)\" -T data_sections.ld -ffunction-sections -fdata-sections -Wl,--gc-sections -Wl,--build-id=none -static -Wall -Werror -static-pie -fdebug-prefix-map=$(PWD)=. -ffile-prefix-map=$(PWD)=. LIBS = -lsquashfuse -lsquashfuse_ll -lzstd -lz -lfuse3 -lmimalloc all: runtime -- 2.50.0 ================================================ FILE: contrib/build-linux/sdist/.dockerignore ================================================ ================================================ FILE: contrib/build-linux/sdist/Dockerfile ================================================ FROM debian:bookworm@sha256:b877a1a3fdf02469440f1768cf69c9771338a875b7add5e80c45b756c92ac20a ENV LC_ALL=C.UTF-8 LANG=C.UTF-8 ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update -q && \ apt-get install -qy \ git \ gettext \ python3 \ python3-pip \ python3-setuptools \ python3-venv \ && \ rm -rf /var/lib/apt/lists/* && \ apt-get autoremove -y && \ apt-get clean # create new user to avoid using root; but with sudo access and no password for convenience. ARG UID=1000 RUN if [ "$UID" != "0" ] ; then useradd --uid $UID --create-home --shell /bin/bash "user" ; fi RUN usermod -append --groups sudo $(id -nu $UID || echo "user") RUN echo "%sudo ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers RUN HOME_DIR=$(getent passwd $UID | cut -d: -f6) ENV WORK_DIR="${HOME_DIR}/wspace" \ PATH="${HOME_DIR}/.local/bin:${PATH}" WORKDIR ${WORK_DIR} RUN chown --recursive ${UID} ${WORK_DIR} USER ${UID} ================================================ FILE: contrib/build-linux/sdist/README.md ================================================ # Source tarballs ✓ _These tarballs should be reproducible, meaning you should be able to generate distributables that match the official releases._ This assumes an Ubuntu (x86_64) host, but it should not be too hard to adapt to another similar system. We distribute two tarballs, a "normal" one (the default, recommended for users), and a strictly source-only one (for Linux distro packagers). The normal tarball, in addition to including everything from the source-only one, also includes: - compiled (`.mo`) locale files (in addition to source `.po` locale files) - compiled (`_pb2.py`) protobuf files (in addition to source `.proto` files) - the `packages/` folder containing source-only pure-python runtime dependencies ## Build steps 1. Install Docker See [`contrib/docker_notes.md`](../../docker_notes.md). (worth reading even if you already have docker) 2. Build tarball (set envvar `OMIT_UNCLEAN_FILES=1` to build the "source-only" tarball) ``` $ ./build.sh ``` If you want reproducibility, try instead e.g.: ``` $ ELECBUILD_COMMIT=HEAD ELECBUILD_NOCACHE=1 ./build.sh $ ELECBUILD_COMMIT=HEAD ELECBUILD_NOCACHE=1 OMIT_UNCLEAN_FILES=1 ./build.sh ``` 3. The generated distributables are in `./dist`. ## Differences between the `sourceonly` vs "normal" tar.gz These scripts can either build a source-only or a "normal" tarball. The official release process builds both. The source-only tarball is aimed at Linux distro packagers. Users wanting to run from source should typically use the normal tarball. The differences are as follows: - the normal tarball bundles all the pure-python dependencies of Electrum. These are placed into the `packages/` folder, and they are automatically found and used at runtime. - the normal tarball includes compiled (.mo) locale files, the source-only tarball does not. Both tarballs contain (.po) source locale files. If you are packaging for a Linux distro, you probably want to compile the .mo locale files yourself (see `contrib/locale/build_locale.sh`). - the normal tarball includes generated `*_pb2.py` files. These are created using `protobuf-compiler` from `.proto` files (see `contrib/generate_payreqpb2.sh`) ================================================ FILE: contrib/build-linux/sdist/build.sh ================================================ #!/bin/bash # # env vars: # - ELECBUILD_NOCACHE: if set, forces rebuild of docker image # - ELECBUILD_COMMIT: if set, do a fresh clone and git checkout set -e PROJECT_ROOT="$(dirname "$(readlink -e "$0")")/../../.." PROJECT_ROOT_OR_FRESHCLONE_ROOT="$PROJECT_ROOT" CONTRIB="$PROJECT_ROOT/contrib" CONTRIB_SDIST="$CONTRIB/build-linux/sdist" DISTDIR="$PROJECT_ROOT/dist" BUILD_UID=$(/usr/bin/stat -c %u "$PROJECT_ROOT") . "$CONTRIB"/build_tools_util.sh DOCKER_BUILD_FLAGS="" if [ ! -z "$ELECBUILD_NOCACHE" ] ; then info "ELECBUILD_NOCACHE is set. forcing rebuild of docker image." DOCKER_BUILD_FLAGS="--pull --no-cache" fi if [ -z "$ELECBUILD_COMMIT" ] ; then # local dev build DOCKER_BUILD_FLAGS="$DOCKER_BUILD_FLAGS --build-arg UID=$BUILD_UID" fi info "building docker image." docker build \ $DOCKER_BUILD_FLAGS \ -t electrum-sdist-builder-img \ "$CONTRIB_SDIST" # maybe do fresh clone if [ ! -z "$ELECBUILD_COMMIT" ] ; then info "ELECBUILD_COMMIT=$ELECBUILD_COMMIT. doing fresh clone and git checkout." FRESH_CLONE="/tmp/electrum_build/sdist/fresh_clone/electrum" rm -rf "$FRESH_CLONE" 2>/dev/null || ( info "we need sudo to rm prev FRESH_CLONE." && sudo rm -rf "$FRESH_CLONE" ) umask 0022 git clone "$PROJECT_ROOT" "$FRESH_CLONE" cd "$FRESH_CLONE" git checkout "$ELECBUILD_COMMIT" PROJECT_ROOT_OR_FRESHCLONE_ROOT="$FRESH_CLONE" else info "not doing fresh clone." fi DOCKER_RUN_FLAGS="" if sh -c ": >/dev/tty" >/dev/null 2>/dev/null; then info "/dev/tty is available and usable" DOCKER_RUN_FLAGS="-it" fi info "building binary..." # check uid and maybe chown. see #8261 if [ ! -z "$ELECBUILD_COMMIT" ] ; then # fresh clone (reproducible build) if [ $(id -u) != "1000" ] || [ $(id -g) != "1000" ] ; then info "need to chown -R FRESH_CLONE dir. prompting for sudo." sudo chown -R 1000:1000 "$FRESH_CLONE" fi fi docker run $DOCKER_RUN_FLAGS \ --name electrum-sdist-builder-cont \ -v "$PROJECT_ROOT_OR_FRESHCLONE_ROOT":/opt/electrum \ --rm \ --workdir /opt/electrum/contrib/build-linux/sdist \ --env OMIT_UNCLEAN_FILES \ electrum-sdist-builder-img \ ./make_sdist.sh # make sure resulting binary location is independent of fresh_clone if [ ! -z "$ELECBUILD_COMMIT" ] ; then mkdir --parents "$DISTDIR/" cp -f "$FRESH_CLONE/dist"/* "$DISTDIR/" fi ================================================ FILE: contrib/build-linux/sdist/make_sdist.sh ================================================ #!/bin/bash set -e PROJECT_ROOT="$(dirname "$(readlink -e "$0")")/../../.." CONTRIB="$PROJECT_ROOT/contrib" CONTRIB_SDIST="$CONTRIB/build-linux/sdist" DISTDIR="$PROJECT_ROOT/dist" BUILDDIR="$CONTRIB_SDIST/build" . "$CONTRIB"/build_tools_util.sh git -C "$PROJECT_ROOT" rev-parse 2>/dev/null || fail "Building outside a git clone is not supported." rm -rf "$BUILDDIR" mkdir -p "$BUILDDIR" "$DISTDIR" python3 --version || fail "python interpreter not found" break_legacy_easy_install rm -rf "$PROJECT_ROOT/packages/" if ([ "$OMIT_UNCLEAN_FILES" != 1 ]); then "$CONTRIB"/make_packages.sh || fail "make_packages failed" fi info "preparing electrum-locale." ( "$CONTRIB/locale/build_cleanlocale.sh" # By default, include both source (.po) and compiled (.mo) locale files in the source dist. # Set option OMIT_UNCLEAN_FILES=1 to exclude the compiled locale files # see https://askubuntu.com/a/144139 (also see MANIFEST.in) if ([ "$OMIT_UNCLEAN_FILES" = 1 ]); then rm -r "$PROJECT_ROOT/electrum/locale/locale"/*/LC_MESSAGES/electrum.mo fi ) if ([ "$OMIT_UNCLEAN_FILES" = 1 ]); then # FIXME side-effecting repo... though in practice, this script probably runs in fresh_clone rm -f "$PROJECT_ROOT/electrum/paymentrequest_pb2.py" fi ( cd "$PROJECT_ROOT" find -exec touch -h -d '2000-11-11T11:11:11+00:00' {} + # note: .zip sdists would not be reproducible due to https://bugs.python.org/issue40963 if ([ "$OMIT_UNCLEAN_FILES" = 1 ]); then PY_DISTDIR="$BUILDDIR/dist1/_sourceonly" # The DISTDIR variable of this script is only used to find where the output is *finally* placed. else PY_DISTDIR="$BUILDDIR/dist1" fi # build initial tar.gz python3 setup.py --quiet sdist --format=gztar --dist-dir="$PY_DISTDIR" VERSION=$("$CONTRIB"/print_electrum_version.py) if ([ "$OMIT_UNCLEAN_FILES" = 1 ]); then FINAL_DISTNAME="Electrum-sourceonly-$VERSION.tar.gz" else FINAL_DISTNAME="Electrum-$VERSION.tar.gz" fi if ([ "$OMIT_UNCLEAN_FILES" = 1 ]); then mv "$PY_DISTDIR/Electrum-$VERSION.tar.gz" "$PY_DISTDIR/../$FINAL_DISTNAME" rmdir "$PY_DISTDIR" fi # the initial tar.gz is not reproducible, see https://github.com/pypa/setuptools/issues/2133 # so we untar, fix timestamps, and then re-tar mkdir -p "$BUILDDIR/dist2" cd "$BUILDDIR/dist2" tar -xzf "$BUILDDIR/dist1/$FINAL_DISTNAME" find -exec touch -h -d '2000-11-11T11:11:11+00:00' {} + GZIP=-n tar --sort=name -czf "$FINAL_DISTNAME" "Electrum-$VERSION/" mv "$FINAL_DISTNAME" "$DISTDIR/$FINAL_DISTNAME" ) info "done." ls -la "$DISTDIR" sha256sum "$DISTDIR"/* ================================================ FILE: contrib/build-wine/.dockerignore ================================================ tmp/ build/ .cache/ dist/ signed/ ================================================ FILE: contrib/build-wine/Dockerfile ================================================ FROM debian:trixie@sha256:13f29b6806e531c3ff3b565bb6eed73f2132506c8c9d41bb996065ca20fb27f2 # need ca-certificates before using snapshot packages RUN apt update -qq > /dev/null && apt install -qq --yes --no-install-recommends \ ca-certificates # pin the distro packages. COPY apt.sources.list /etc/apt/sources.list COPY apt.preferences /etc/apt/preferences.d/snapshot ENV LC_ALL=C.UTF-8 LANG=C.UTF-8 ENV DEBIAN_FRONTEND=noninteractive RUN dpkg --add-architecture i386 && \ apt-get update -q && \ apt-get install -qy --allow-downgrades \ lsb-release \ wget \ gnupg2 \ dirmngr \ python3 \ git \ p7zip-full \ make \ cmake \ pkgconf \ mingw-w64 \ mingw-w64-tools \ autotools-dev \ autoconf \ autopoint \ libtool \ gettext \ sudo \ nsis \ && \ rm -rf /var/lib/apt/lists/* && \ apt-get autoremove -y && \ apt-get clean RUN DEBIAN_CODENAME=$(lsb_release --codename --short) && \ WINEVERSION="11.0.0.0~${DEBIAN_CODENAME}-1" && \ wget -nc https://dl.winehq.org/wine-builds/winehq.key && \ echo "d965d646defe94b3dfba6d5b4406900ac6c81065428bf9d9303ad7a72ee8d1b8 winehq.key" | sha256sum -c - && \ cat winehq.key | gpg --dearmor -o /etc/apt/keyrings/winehq.gpg && \ echo deb [signed-by=/etc/apt/keyrings/winehq.gpg] https://dl.winehq.org/wine-builds/debian/ ${DEBIAN_CODENAME} main >> /etc/apt/sources.list.d/winehq.list && \ rm winehq.key && \ apt-get update -q && \ apt-get install -qy --allow-downgrades \ wine-stable-amd64:amd64=${WINEVERSION} \ wine-stable-i386:i386=${WINEVERSION} \ wine-stable:amd64=${WINEVERSION} \ winehq-stable:amd64=${WINEVERSION} \ && \ rm -rf /var/lib/apt/lists/* && \ apt-get autoremove -y && \ apt-get clean # create new user to avoid using root; but with sudo access and no password for convenience. ARG UID=1000 RUN if [ "$UID" != "0" ] ; then useradd --uid $UID --create-home --shell /bin/bash "user" ; fi RUN usermod -append --groups sudo $(id -nu $UID || echo "user") RUN echo "%sudo ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers RUN HOME_DIR=$(getent passwd $UID | cut -d: -f6) ENV WORK_DIR="${HOME_DIR}/wspace" \ PATH="${HOME_DIR}/.local/bin:${PATH}" WORKDIR ${WORK_DIR} RUN chown --recursive ${UID} ${WORK_DIR} RUN chown ${UID} /opt USER ${UID} RUN mkdir --parents "/opt/wine64/drive_c/electrum" ================================================ FILE: contrib/build-wine/README.md ================================================ # Windows binaries ✓ _These binaries should be reproducible, meaning you should be able to generate binaries that match the official releases._ - _Minimum supported target system (i.e. what end-users need): x86_64, Windows 10 (1809)_ This assumes an Ubuntu (x86_64) host, but it should not be too hard to adapt to another similar system. 1. Install Docker See [`contrib/docker_notes.md`](../docker_notes.md). (worth reading even if you already have docker) Note: older versions of Docker might not work well (see [#6971](https://github.com/spesmilo/electrum/issues/6971)). If having problems, try to upgrade to at least `docker 20.10`. 2. Build Windows binaries ``` $ ./build.sh ``` If you want reproducibility, try instead e.g.: ``` $ ELECBUILD_COMMIT=HEAD ./build.sh ``` 3. The generated binaries are in `./contrib/build-wine/dist`. ## Code Signing Electrum Windows builds are signed with a Microsoft Authenticode™ code signing certificate in addition to the GPG-based signatures. The advantage of using Authenticode is that Electrum users won't receive a Windows SmartScreen warning when starting it. The release signing procedure involves a signer (the holder of the certificate/key) and one or multiple trusted verifiers: | Signer | Verifier | |-----------------------------------------------------------|--------------------------------------| | Build .exe files using `make_win.sh` | | | Sign .exe with `./sign.sh` | | | Upload signed files to download server | | | | Build .exe files using `make_win.sh` | | | Compare files using `unsign.sh` | | | Sign .exe file using `gpg -b` | | Signer and verifiers: | |--------------------------------------------------------------------------------------------------| | Upload signatures to 'electrum-signatures' repo, as `$version/$filename.$builder.asc` | ## Verify Integrity of signed binary Every user can verify that the official binary was created from the source code in this repository. To do so, the Authenticode signature needs to be stripped since the signature is not reproducible. This procedure removes the differences between the signed and unsigned binary: 1. Remove the signature from the signed binary using osslsigncode or signtool. 2. Set the COFF image checksum for the signed binary to 0x0. This is necessary because pyinstaller doesn't generate a checksum. 3. Append null bytes to the _unsigned_ binary until the byte count is a multiple of 8. The script `unsign.sh` performs these steps. ## FAQ ### How to investigate diff between binaries if reproducibility fails? `pyi-archive_viewer` is needed, for that run `$ pip install pyinstaller`. As a first pass overview, run: ``` pyi-archive_viewer -l electrum-*.exe1 > f1 pyi-archive_viewer -l electrum-*.exe2 > f2 diff f1 f2 > d cat d ``` Then investigate manually: ``` $ pyi-archive_viewer electrum-*.exe1 ? help ``` ================================================ FILE: contrib/build-wine/README_windows.md ================================================ # Running Electrum from source on Windows (development version) ## Prerequisites - [python3](https://www.python.org/) - [git](https://gitforwindows.org/) ## Main steps ### 1. Check out the code from GitHub: ``` > git clone https://github.com/spesmilo/electrum.git > cd electrum > git submodule update --init ``` Run install (this should install most dependencies): ``` > python3 -m pip install --user -e ".[gui,crypto]" ``` ### 2. Install `libsecp256k1` [comment]: # (technically the dll should be put into site-packages/electrum_ecc/, but putting it into electrum/ also works because of the `os.add_dll_directory` call in electrum/__init__.py) [libsecp256k1](https://github.com/bitcoin-core/secp256k1) is a required dependency. This is a C library, which you need to compile yourself. Electrum needs a dll, named `libsecp256k1-0.dll` (or newer `libsecp256k1-*.dll`), placed into the inner `electrum/` folder. For Unix-like systems, the (`contrib/make_libsecp256k1.sh`) script does this for you, however it does not work on Windows. If you have access to a Linux machine (e.g. VM) or perhaps even using WSL (Windows Subsystem for Linux), you can cross-compile from there to Windows, and build this dll: ``` $ GCC_TRIPLET_HOST="x86_64-w64-mingw32" ./contrib/make_libsecp256k1.sh ``` Alternatively, MSYS2 and MinGW-w64 can be used directly on Windows, as follows. - download and install [MSYS2](https://www.msys2.org/) - run MSYS2 - inside the MSYS2 shell: ``` $ pacman -Syu $ pacman -S --needed git base-devel mingw-w64-x86_64-toolchain mingw-w64-x86_64-autotools $ export PATH="$PATH:/mingw64/bin" ``` `cd` into the git clone, e.g. `C:\wspace\electrum` (auto-mounted at `/c/wspace/electrum`) ``` $ cd /c/wspace/electrum $ GCC_TRIPLET_HOST="x86_64-w64-mingw32" ./contrib/make_libsecp256k1.sh ``` (note: this is a bit cumbersome, see [issue #5976](https://github.com/spesmilo/electrum/issues/5976) for discussion) ### 3. Run electrum: ``` > python3 ./run_electrum ``` ================================================ FILE: contrib/build-wine/apt.preferences ================================================ Package: * Pin: origin "snapshot.debian.org" Pin-Priority: 1001 ================================================ FILE: contrib/build-wine/apt.sources.list ================================================ deb https://snapshot.debian.org/archive/debian/20260227T144551Z/ trixie main deb-src https://snapshot.debian.org/archive/debian/20260227T144551Z/ trixie main ================================================ FILE: contrib/build-wine/build-electrum-git.sh ================================================ #!/bin/bash NAME_ROOT=electrum PROJECT_ROOT="$WINEPREFIX/drive_c/electrum" export PYTHONDONTWRITEBYTECODE=1 # don't create __pycache__/ folders with .pyc files # Let's begin! set -e . "$CONTRIB"/build_tools_util.sh pushd "$PROJECT_ROOT" VERSION=$(git describe --tags --dirty --always) info "Last commit: $VERSION" info "preparing electrum-locale." ( "$CONTRIB/locale/build_cleanlocale.sh" # we want the binary to have only compiled (.mo) locale files; not source (.po) files rm -r "$PROJECT_ROOT/electrum/locale/locale"/*/electrum.po ) find -exec touch -h -d '2000-11-11T11:11:11+00:00' {} + popd # opt out of compiling C extensions export AIOHTTP_NO_EXTENSIONS=1 export YARL_NO_EXTENSIONS=1 export MULTIDICT_NO_EXTENSIONS=1 export FROZENLIST_NO_EXTENSIONS=1 export PROPCACHE_NO_EXTENSIONS=1 export ELECTRUM_ECC_DONT_COMPILE=1 info "Installing requirements..." $WINE_PYTHON -m pip install --no-build-isolation --no-dependencies --no-binary :all: --no-warn-script-location \ --cache-dir "$WINE_PIP_CACHE_DIR" -r "$CONTRIB"/deterministic-build/requirements.txt info "Installing dependencies specific to binaries..." # TODO tighten "--no-binary :all:" (but we don't have a C compiler...) $WINE_PYTHON -m pip install --no-build-isolation --no-dependencies --no-warn-script-location \ --no-binary :all: --only-binary cffi,cryptography,PyQt6,PyQt6-Qt6,PyQt6-sip \ --cache-dir "$WINE_PIP_CACHE_DIR" -r "$CONTRIB"/deterministic-build/requirements-binaries.txt info "Installing hardware wallet requirements..." $WINE_PYTHON -m pip install --no-build-isolation --no-dependencies --no-warn-script-location \ --no-binary :all: --only-binary cffi,cryptography,hidapi \ --cache-dir "$WINE_PIP_CACHE_DIR" -r "$CONTRIB"/deterministic-build/requirements-hw.txt pushd "$PROJECT_ROOT" # see https://github.com/pypa/pip/issues/2195 -- pip makes a copy of the entire directory info "Pip installing Electrum. This might take a long time if the project folder is large." $WINE_PYTHON -m pip install --no-build-isolation --no-dependencies --no-warn-script-location . # pyinstaller needs to be able to "import electrum_ecc", for which we need libsecp256k1: # (or could try "pip install -e" instead) cp electrum/libsecp256k1-*.dll "$WINEPREFIX/drive_c/python3/Lib/site-packages/electrum_ecc/" popd rm -rf dist/ # build standalone and portable versions info "Running pyinstaller..." ELECTRUM_CMDLINE_NAME="$NAME_ROOT-$VERSION" wine "$WINE_PYHOME/scripts/pyinstaller.exe" --noconfirm --clean pyinstaller.spec # set timestamps in dist, in order to make the installer reproducible pushd dist find -exec touch -h -d '2000-11-11T11:11:11+00:00' {} + popd info "building NSIS installer" # $VERSION could be passed to the electrum.nsi script, but this would require some rewriting in the script itself. makensis -DPRODUCT_VERSION=$VERSION electrum.nsi cd dist mv electrum-setup.exe $NAME_ROOT-$VERSION-setup.exe cd .. info "Padding binaries to 8-byte boundaries, and fixing COFF image checksum in PE header" # note: 8-byte boundary padding is what osslsigncode uses: # https://github.com/mtrojnar/osslsigncode/blob/6c8ec4427a0f27c145973450def818e35d4436f6/osslsigncode.c#L3047 ( cd dist for binary_file in ./*.exe; do info ">> fixing $binary_file..." # code based on https://github.com/erocarrera/pefile/blob/bbf28920a71248ed5c656c81e119779c131d9bd4/pefile.py#L5877 python3 <> 32) if checksum > 2 ** 32: checksum = (checksum & 0xffffffff) + (checksum >> 32) checksum = (checksum & 0xffff) + (checksum >> 16) checksum = (checksum) + (checksum >> 16) checksum = checksum & 0xffff checksum += len(binary) # Set the checksum binary[checksum_offset : checksum_offset + 4] = int.to_bytes(checksum, byteorder="little", length=4) with open(pe_file, "wb") as f: f.write(binary) EOF done ) sha256sum dist/electrum*.exe ================================================ FILE: contrib/build-wine/build.sh ================================================ #!/bin/bash # # env vars: # - ELECBUILD_NOCACHE: if set, forces rebuild of docker image # - ELECBUILD_COMMIT: if set, do a fresh clone and git checkout set -e PROJECT_ROOT="$(dirname "$(readlink -e "$0")")/../.." PROJECT_ROOT_OR_FRESHCLONE_ROOT="$PROJECT_ROOT" CONTRIB="$PROJECT_ROOT/contrib" CONTRIB_WINE="$CONTRIB/build-wine" BUILD_UID=$(/usr/bin/stat -c %u "$PROJECT_ROOT") . "$CONTRIB"/build_tools_util.sh info "Clearing $CONTRIB_WINE/dist..." rm -rf "$CONTRIB_WINE"/dist/* DOCKER_BUILD_FLAGS="" if [ ! -z "$ELECBUILD_NOCACHE" ] ; then info "ELECBUILD_NOCACHE is set. forcing rebuild of docker image." DOCKER_BUILD_FLAGS="--pull --no-cache" fi if [ -z "$ELECBUILD_COMMIT" ] ; then # local dev build DOCKER_BUILD_FLAGS="$DOCKER_BUILD_FLAGS --build-arg UID=$BUILD_UID" fi info "building docker image." docker build \ $DOCKER_BUILD_FLAGS \ -t electrum-wine-builder-img \ "$CONTRIB_WINE" # maybe do fresh clone if [ ! -z "$ELECBUILD_COMMIT" ] ; then info "ELECBUILD_COMMIT=$ELECBUILD_COMMIT. doing fresh clone and git checkout." FRESH_CLONE="/tmp/electrum_build/windows/fresh_clone/electrum" rm -rf "$FRESH_CLONE" 2>/dev/null || ( info "we need sudo to rm prev FRESH_CLONE." && sudo rm -rf "$FRESH_CLONE" ) umask 0022 git clone "$PROJECT_ROOT" "$FRESH_CLONE" cd "$FRESH_CLONE" git checkout "$ELECBUILD_COMMIT" PROJECT_ROOT_OR_FRESHCLONE_ROOT="$FRESH_CLONE" else info "not doing fresh clone." fi DOCKER_RUN_FLAGS="" if sh -c ": >/dev/tty" >/dev/null 2>/dev/null; then info "/dev/tty is available and usable" DOCKER_RUN_FLAGS="-it" fi info "building binary..." # check uid and maybe chown. see #8261 if [ ! -z "$ELECBUILD_COMMIT" ] ; then # fresh clone (reproducible build) if [ $(id -u) != "1000" ] || [ $(id -g) != "1000" ] ; then info "need to chown -R FRESH_CLONE dir. prompting for sudo." sudo chown -R 1000:1000 "$FRESH_CLONE" fi fi docker run $DOCKER_RUN_FLAGS \ --name electrum-wine-builder-cont \ -v "$PROJECT_ROOT_OR_FRESHCLONE_ROOT":/opt/wine64/drive_c/electrum \ --rm \ --workdir /opt/wine64/drive_c/electrum/contrib/build-wine \ electrum-wine-builder-img \ ./make_win.sh # make sure resulting binary location is independent of fresh_clone if [ ! -z "$ELECBUILD_COMMIT" ] ; then mkdir --parents "$PROJECT_ROOT/contrib/build-wine/dist/" cp -f "$FRESH_CLONE/contrib/build-wine/dist"/*.exe "$PROJECT_ROOT/contrib/build-wine/dist/" fi ================================================ FILE: contrib/build-wine/electrum.nsi ================================================ ;-------------------------------- ;Include Modern UI !include "TextFunc.nsh" ;Needed for the $GetSize function. I know, doesn't sound logical, it isn't. !include "MUI2.nsh" ;-------------------------------- ;Variables !define PRODUCT_NAME "Electrum" !define PRODUCT_WEB_SITE "https://github.com/spesmilo/electrum" !define PRODUCT_PUBLISHER "Electrum Technologies GmbH" !define PRODUCT_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" ;-------------------------------- ;General ;Name and file Name "${PRODUCT_NAME}" OutFile "dist/electrum-setup.exe" ;Default installation folder InstallDir "$PROGRAMFILES64\${PRODUCT_NAME}" ;Get installation folder from registry if available InstallDirRegKey HKCU "Software\${PRODUCT_NAME}" "" ;Request application privileges for Windows Vista RequestExecutionLevel admin ;Specifies whether or not the installer will perform a CRC on itself before allowing an install CRCCheck on ;Sets whether or not the details of the install are shown. Can be 'hide' (the default) to hide the details by default, allowing the user to view them, or 'show' to show them by default, or 'nevershow', to prevent the user from ever seeing them. ShowInstDetails show ;Sets whether or not the details of the uninstall are shown. Can be 'hide' (the default) to hide the details by default, allowing the user to view them, or 'show' to show them by default, or 'nevershow', to prevent the user from ever seeing them. ShowUninstDetails show ;Sets the colors to use for the install info screen (the default is 00FF00 000000. Use the form RRGGBB (in hexadecimal, as in HTML, only minus the leading '#', since # can be used for comments). Note that if "/windows" is specified as the only parameter, the default windows colors will be used. InstallColors /windows ;This command sets the compression algorithm used to compress files/data in the installer. (http://nsis.sourceforge.net/Reference/SetCompressor) SetCompressor /SOLID lzma ;Sets the dictionary size in megabytes (MB) used by the LZMA compressor (default is 8 MB). SetCompressorDictSize 64 ;Sets the text that is shown (by default it is 'Nullsoft Install System vX.XX') in the bottom of the install window. Setting this to an empty string ("") uses the default; to set the string to blank, use " " (a space). BrandingText "${PRODUCT_NAME} Installer v${PRODUCT_VERSION}" ;Sets what the titlebars of the installer will display. By default, it is 'Name Setup', where Name is specified with the Name command. You can, however, override it with 'MyApp Installer' or whatever. If you specify an empty string (""), the default will be used (you can however specify " " to achieve a blank string) Caption "${PRODUCT_NAME}" ;Adds the Product Version on top of the Version Tab in the Properties of the file. VIProductVersion 1.0.0.0 ;VIAddVersionKey - Adds a field in the Version Tab of the File Properties. This can either be a field provided by the system or a user defined field. VIAddVersionKey ProductName "${PRODUCT_NAME} Installer" VIAddVersionKey Comments "The installer for ${PRODUCT_NAME}" VIAddVersionKey CompanyName "${PRODUCT_NAME}" VIAddVersionKey LegalCopyright "2013-2018 ${PRODUCT_PUBLISHER}" VIAddVersionKey FileDescription "${PRODUCT_NAME} Installer" VIAddVersionKey FileVersion ${PRODUCT_VERSION} VIAddVersionKey ProductVersion ${PRODUCT_VERSION} VIAddVersionKey InternalName "${PRODUCT_NAME} Installer" VIAddVersionKey LegalTrademarks "${PRODUCT_NAME} is a trademark of ${PRODUCT_PUBLISHER}" VIAddVersionKey OriginalFilename "${PRODUCT_NAME}.exe" ;-------------------------------- ;Interface Settings !define MUI_ABORTWARNING !define MUI_ABORTWARNING_TEXT "Are you sure you wish to abort the installation of ${PRODUCT_NAME}?" !define MUI_ICON "..\..\electrum\gui\icons\electrum.ico" ;-------------------------------- ;Pages !insertmacro MUI_PAGE_DIRECTORY !insertmacro MUI_PAGE_INSTFILES !insertmacro MUI_UNPAGE_CONFIRM !insertmacro MUI_UNPAGE_INSTFILES ;-------------------------------- ;Languages !insertmacro MUI_LANGUAGE "English" ;-------------------------------- ;Functions !macro CreateEnsureNotRunning prefix operation Function ${prefix}EnsureNotRunning ; pop the directory to check from the stack into $R0 Pop $R0 ; if the dir at $R0 doesn't exist, jump to nodir IfFileExists "$R0" 0 nodir ; Find all .exe files in the directory, $1 is the handle, $2 is the filename FindFirst $1 $2 "$R0\*.exe" IfErrors noexe 0 checkloop: ; Skip checking the uninstaller if we are the uninstaller to avoid locking the uninstaller itself !if "${prefix}" == "un." StrCmp $2 "Uninstall.exe" skipfile 0 !endif ; Check if we can append to the .exe file. If we can't that means it is still running. retryopen: FileOpen $0 "$R0\$2" a IfErrors 0 closeexe MessageBox MB_RETRYCANCEL "Can not ${operation} because $2 is still running. Close it and retry." /SD IDCANCEL IDRETRY retryopen FindClose $1 Abort closeexe: FileClose $0 skipfile: ; Find next .exe file FindNext $1 $2 IfErrors done 0 Goto checkloop done: FindClose $1 noexe: nodir: FunctionEnd !macroend ; The function has to be created twice, once for the installer and once for the uninstaller !insertmacro CreateEnsureNotRunning "" "install" !insertmacro CreateEnsureNotRunning "un." "uninstall" ;-------------------------------- ;Installer Sections ;Check if we have Administrator rights Function .onInit UserInfo::GetAccountType pop $0 ${If} $0 != "admin" ;Require admin rights on NT4+ MessageBox mb_iconstop "Administrator rights required!" SetErrorLevel 740 ;ERROR_ELEVATION_REQUIRED Quit ${EndIf} ; Check if already installed and ensure the process is not running if it is ReadRegStr $R0 HKCU "Software\${PRODUCT_NAME}" "" IfErrors noinstdir 0 Push $R0 Call EnsureNotRunning noinstdir: ClearErrors FunctionEnd Section SetOutPath $INSTDIR ;Uninstall previous version files RMDir /r "$INSTDIR\*.*" Delete "$DESKTOP\${PRODUCT_NAME}.lnk" Delete "$SMPROGRAMS\${PRODUCT_NAME}\*.*" ;Files to pack into the installer File /r "dist\electrum\*.*" File "..\..\electrum\gui\icons\electrum.ico" ;Store installation folder WriteRegStr HKCU "Software\${PRODUCT_NAME}" "" $INSTDIR ;Create uninstaller DetailPrint "Creating uninstaller..." WriteUninstaller "$INSTDIR\Uninstall.exe" ;Create desktop shortcut DetailPrint "Creating desktop shortcut..." CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\electrum-${PRODUCT_VERSION}.exe" "" ;Create start-menu items DetailPrint "Creating start-menu items..." CreateDirectory "$SMPROGRAMS\${PRODUCT_NAME}" CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall.lnk" "$INSTDIR\Uninstall.exe" "" "$INSTDIR\Uninstall.exe" 0 CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME}.lnk" "$INSTDIR\electrum-${PRODUCT_VERSION}.exe" "" "$INSTDIR\electrum-${PRODUCT_VERSION}.exe" 0 CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME} Testnet.lnk" "$INSTDIR\electrum-${PRODUCT_VERSION}.exe" "--testnet" "$INSTDIR\electrum-${PRODUCT_VERSION}.exe" 0 ;Links bitcoin: and lightning: URIs to Electrum WriteRegStr HKCU "Software\Classes\bitcoin" "" "URL:bitcoin Protocol" WriteRegStr HKCU "Software\Classes\bitcoin" "URL Protocol" "" WriteRegStr HKCU "Software\Classes\bitcoin" "DefaultIcon" "$\"$INSTDIR\electrum.ico, 0$\"" WriteRegStr HKCU "Software\Classes\bitcoin\shell\open\command" "" "$\"$INSTDIR\electrum-${PRODUCT_VERSION}.exe$\" $\"%1$\"" WriteRegStr HKCU "Software\Classes\lightning" "" "URL:lightning Protocol" WriteRegStr HKCU "Software\Classes\lightning" "URL Protocol" "" WriteRegStr HKCU "Software\Classes\lightning" "DefaultIcon" "$\"$INSTDIR\electrum.ico, 0$\"" WriteRegStr HKCU "Software\Classes\lightning\shell\open\command" "" "$\"$INSTDIR\electrum-${PRODUCT_VERSION}.exe$\" $\"%1$\"" ;Adds an uninstaller possibility to Windows Uninstall or change a program section WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "DisplayName" "$(^Name)" WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "UninstallString" "$INSTDIR\Uninstall.exe" WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "DisplayVersion" "${PRODUCT_VERSION}" WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "URLInfoAbout" "${PRODUCT_WEB_SITE}" WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "Publisher" "${PRODUCT_PUBLISHER}" WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "DisplayIcon" "$INSTDIR\electrum.ico" ;Fixes Windows broken size estimates ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 IntFmt $0 "0x%08X" $0 WriteRegDWORD HKCU "${PRODUCT_UNINST_KEY}" "EstimatedSize" "$0" SectionEnd ;-------------------------------- ;Descriptions ;-------------------------------- ;Uninstaller Section Section "Uninstall" RMDir /r "$INSTDIR\*.*" RMDir "$INSTDIR" Delete "$DESKTOP\${PRODUCT_NAME}.lnk" Delete "$SMPROGRAMS\${PRODUCT_NAME}\*.*" RMDir "$SMPROGRAMS\${PRODUCT_NAME}" DeleteRegKey HKCU "Software\Classes\bitcoin" DeleteRegKey HKCU "Software\${PRODUCT_NAME}" DeleteRegKey HKCU "${PRODUCT_UNINST_KEY}" SectionEnd Function UN.onInit ; Ensure the process is not running in the uninstallation directory Push $INSTDIR Call un.EnsureNotRunning FunctionEnd ================================================ FILE: contrib/build-wine/gpg_keys/7ED10B6531D7C8E1BC296021FC624643487034E5.asc ================================================ -----BEGIN PGP PUBLIC KEY BLOCK----- Comment: User-ID: Steve Dower (Python Release Signing) Comment: Created: 2015-04-06 02:32 Comment: Type: 4096-bit RSA Comment: Usage: Signing, Encryption, Certifying User-IDs Comment: Fingerprint: 7ED10B6531D7C8E1BC296021FC624643487034E5 mQINBFUh1AUBEACdUPt6PwJVO23zGZqgtgBeA9JsO22dk3CMzrwPJdUmMd6mcRWa vl4BoAba66fuC17GvOgGXimKI+iaw5Vt9QI3uSjUjFSfc24J8T7NB/yAr/0zEcex raHD2dxT/JpE/iY0yWHxRlitvwGSw1Qlq3NnY8tDI1DJEJD+gBuCktvVvu1FfQTw 6bd+aEq0c4sWJHAOnKLuLH0pNFOznnynAFGPGBBsm/YwYc5BP2JVvka775LUjA+W 1h2Sgg3FAUPIm64pc4Pq6mUo6Tulw72xsWMpCL1/5atXNPXT6rJUOB8euTcNMr4l 1O6GKSsiLeLAuvq4bmhOKtLzjWzXnY1gDVoOfdgpD6o4ZHk4xiVsdVE8hCa/ylz8 1ZwRW2gGo2jP8t3hKciR2i+Qs+6lPNZpeFIxa6Uo9ER1IBgCHHapIR/UdcOFyoS0 MNn7Ui7DLQNM4gI/G17eG9tfvjW2dl4SgFSYWMq/OtXnPDUBGqFUWsn8adOL2PFL B7kM5ZRTPc5SnY9hoSGa5E20rJZIXcpy1aygRz/xUjoKwNzAySSEyyIorUxZ8KaH EEBQSsqwe04MXIENqnDozH0/cvP4JXEDSl8EkzMSCWSoavQSIYD5pQppyFQpGHqa 5CuOA25Ja+sgp2xqahtr3fEqZUknPQSoYlnJbaHnzsGSlRAVWMsklsZibQARAQAB tEBTdGV2ZSBEb3dlciAoUHl0aG9uIFJlbGVhc2UgU2lnbmluZykgPHN0ZXZlLmRv d2VyQG1pY3Jvc29mdC5jb20+iQEcBBABCAAGBQJYsBphAAoJEEhSKohZ29goZggI ALKlgyoecD5v3ulh1eoctRqtCOxkAoENEfPt3l5x6N8Wq89yHzf10T1rVioEXOHh Di1m37DDoQmRJD0sOYQymq10xDGRYAJjyOf3X0pvRkZ+F7T0U4dSV3DasLIHcN26 kRwv1yCYsf0QvhgT6EJZKyUNHtV9qrb9u3A1Zp6epC/EyT8zMZj+21GzTUrnbnug 3Ak9p7+APCZS4Ahh9ZHFuD38MZ7+OwrUd6ot+6cbb1nnQLSAGQOHSp6EP6ktrnsK zts0L+tzHurxtJgUkR01imJuSFfYpLoZa/L7qXNyEpEUTC/SWzRWD9y2QkM7DLzX caReVAyJr9rix1lDQbEFIquJAhwEEAEIAAYFAlW2TwMACgkQKeBHm5nIo5fahg/+ IQSSE/yH8Cf82PYI7IGqDVNwRw2o7dq8iscB+fhFHfFFhXANwUUFpzPeDMrMrdmq Bke7Vg1D3bIFocXYOiNwf2J7f4mBO6OL0VAvDX02Vyh/C2ZSc15uZyU6CWFQMCG8 JOSmgQFs3kMHkL4qtut1Y5reoYesmteIe06UVyRw8yT1R1BkxP2whZ97qwsvUUE9 cVD08wCvH486efw7EswIzYGa1KcZXji0MvjXfksVtkEQQbxMMI7SVXo0345ZReww buioGL5gvvAPObgU43skORanFHFxiHEKmqgHBHXK/LKqaFUFMKcb4iFTNs2XKrhE XsEi5EMI1AFsJzjcXRqT50Wi2cZhXeRc70uF6gzqrdWvowa2oOPiO6zGDiTqZCW1 AArk/QBzGtPjVh+nKEdHwnvpK9913UAkAN682h8QkoVPYXOvIKDYZRBr5EfpUyQt y2r9MYewz0YN4zlGP1PFS9FxncdSZiZJqQVif0CkOp1tdSxLynHcujQgATZNtgcu X9JwUwPp60MurgOcIZiW3nZw/z/5vzBBadSa9/TIFSJAFNBlqeKdIGQuik0UH+Cz RRtSFb38F7jMPwr0QUSktuntQ0HWuvNqj4N8DFm45/n5rN190eRotrVDXZmjGein qWPITuICslGIKAp+Q6y3t7JA71MIbeu/ZY6ZcftOka6JAhwEEAEIAAYFAlZRWicA CgkQxiNM8COVzQq5bRAAktnXceO3GCivMt9yR1Qr0Ov4A4Q+CJSIL45efLFmS30k cbkHHtaq+0FZNh2ZaMartC16MUja4a2OUejg53VBhaSVkQrVk/6M/HA6/o6CvIhb FW/5C+nRWBd5gfvwsWvjrtC3cKZco4wg+yYclkDbSH+2EPDZOKIHpBy46YTz9WQ1 8SJ51WVkNUNiZqRBA6Ny5GFoyd6EpWZYEPelmzNemv3zOrQdVzLV24/mLejcLL2t KmI6ngX4XViXUCRUU3MH8/V+V2YTQGcTM/6HGaHpN0LTqknf6zEto9q9FiRTaiU2 kzExhBq8Qf+cVqwm+1kMt0FGOgpT47VBWMeUWq62gQ3h5NfAs4DfriLgNURlTC1d JYAEquFhB/8oBQD1h/d9CjQyk88iib2pJInRBDsK2FcfQBap9iaeBFYoBWTzMQJx g+RuWK1wIm2n0oqa5urBYZtRHE5RIdDP8ZLogrBOFkfXGJxlRBQD1Gab77qohdp0 SnErGw4Ne3gJH/SNhK+zzHkHERIrRZCR95zdYkKfZ2jyOPzSuABVRigEQVQPCDn0 hbv3cblTCeJYwG2mfRdmfyqSMALKIgXe9yvJ2kl8QgaVOsJjNfQzIKeoHFPIm5Uw 3YB6jgDFc5uzEaH7WSz74A7KhGYjC7huw2TugosHbWxphJKddwxfK1WujYaAeJyJ AhwEEAEIAAYFAlf2sPIACgkQfb+tds3soNuXEQ//XkWYHmJsKyeDZC8MFU+/vsVq dhnFs6UXZkvf7MoNFkuMDL+zgVoMpFHftTdyBqNAoEnndakk212jK8YWF8g4kQXI a9uMRqJLM4mqCl9yco/twJ9z9EMA+JLSXYK0ZbTkLdutSDZEDKgpHbmekx2C1OsW lRLs9PahF5PAZQs0N+m+LJBnw6bEHOSTv4OE5uVUf9nvdes3OARvkGSEGURNmUaF chxWtZ/SF1q9Jfj0K/xgs9Gt855oueveRXLIGpjiEVoKH/drsgyKFMJVrpZDDgS4 GVXG8bq3GTFiMAs7BPPd9bjI+jgvqttgItZcYsW/IQK1BIoG6Fere4cPvu+IshCc km9T8nOK98tZuov8hLbND9mW2d7LChJI1r/HbzbKIl0k6OigdFMrJlun2zmtDxT9 Tp3uxOYSaW2YggcpNUjI28tv6AwoA8okVY93LWjO5kdZGkbliRnf/eJy7NJYn0LO ogsvMUJClRAGnZTHLEr32Whq0MImlXa43kr6oPJT5dwXXyw5ELstEQztczCd1PYB kbQHUpD5j3PwgNVOinCnbd4pc/qVtYSqpg2g6TJi1XiJ1638jhn2k+i8wop/dyet iN8lGR76twYGex9AavEAUpVR9r6qfpp4KBibEhdvL6o2O03RQu17GcRzXSAYzmUi 5U5jZ3dBz5MYUjgUZM+JAhwEEAEKAAYFAllTh9gACgkQXLNh5VL7DRAk7Q//X8eU hwEvl/d9Sv2kBNCZFjAW3QmZp2L/sxhScJZXrOFzKUdmjap9Xlul1qr6/Wif7YLK bOdNUI7KziEBn+9SEd90XauoVkzU2F0Jn9ILGQfUHAIpocRTKuCwBrncaBozHQwD O3Dk33AhZ6lqTv/AVLRKHQXwigGTBJxK4cCEZ+VwK9tKk6BrQB48Rm7pg9HF5ey5 JGPRWgUnn1v0IJN5ysZ5m9ChYbqF8VwvMw0txmgKgvdDKpXbF/S59Bp4TH/7Dr2D kAeNTcuzTFBaFE+siMgksZIYKZ1VkVoiN2qQA7ZaA5LQbUom0WdrKZGefFfPt9ES A4wyL3OfxRsmWmd/5Fxrwm1VbzgPoMd1Dc5ExlyqnecdGzDui2bmltNqRJd9ytRq 6YUGYzXp4qQkWO61CoC3mkm2M8Ex7DGbUtXhdg0zoa08w9lXuOtHVhY7XlLWjO1U p8cp4DVxsN/wOXtyH1pcleGo4aEsgyU/DH57prFLGz7Egp2JhRDHnZmlonWp74G1 VLfqkOqZlqTU4mPA827C8qPCx6cMsRvFS7OEiDBswkFWBKjkUCw4rLC1tBMBCxJW tZlc+Y0LNyOryJ3h6EJmRIHO57oLen345e1WOi4ROOC/wQMErFk7B3P41Lqmrwb8 HGuKn3ca+Aw70hVrZ+7Q3RRFTLlOS/vv107Fqu6JAjkEEwEIACMFAlUh1AUCGwMH CwkIBwMCAQYVCAIJCgsEFgIDAQIeAQIXgAAKCRD8YkZDSHA05RfdD/97wPXnoe7e ipP7UXQ942z1buV6pTGv0Lea2aHn20o2BBjHp97YXroF/e/8W6h+Y+Fq8hWoXdYJ dC9DVgzJhvbXAIG8VrF6/IDGQ62r4ff/AIyQY+kiCOCCVhjwuqOTjVYw2pYRUcI3 UwXVPeptDSXcIZkHCLtEUnS5YMTdkPuZrAmucCCnfcJtevXbHD2yJYP4vwfXMbal sNBDKJi6uYAFc4yv+/DyS13rfXJvu2pYGvtRd+fs7mBETvUTubhI440pIss6TX6M lxWexX6Ty8vI5HCQT281H4zqdbe5GdzGmIx1EiYx1sJbgSBNqCh5sRJY5/BXzVJ3 dfM/Mv5QYY4ulO/qUNFdC8f1cZm0euOo3maB4jY+Sjaff7t0WIz0GufO4dHARwJg 3s0LO9Wf5+z/fbWOMcfvvcfaHNbhaKWk16kslc/g7NYvMfOuleM06YGyGPz//a9c baX53OiMupNvLlhyPO5NfGppvRn5xAElcAw1RLhHJcgvTtIs/zVVfHPaK41u8A9c XKnmIUC39K4BGvOpPzEvCdQ2ZbAqzQLmZ1UICr15w1Nfs6uoERJbnuq+JgOPOcOk ezAWELi5LdZTElnpJpZPTDQ03+3GvxD4R9sR+l5RT8Ul7kF+3PPPzekfQzF+Nisr BhPFb2lPt3Hw32FgTTIuXCMRTKEBb/6z77kCDQRVIdQFARAAtmnsZ9A8ovJIJ9Rl WeIylEhHRyQifqzgc/r50uDZVPBjewOA462LjH3+F6zFGEkU+q2aqSe0A0SJPF/W hj6MNYXLoibxi5D4mGkoIao9ExnXt4LXAc6ogQpY6vFQBJU5Nr8XCefQbm0loa/o y5uK8JHLWCZ2jAossnVpzDwNeN27+B8h5+OifnWhQCTun1xz5EJiyc0yoBmf46zf mU4CMUBsPvrXcLmw4J3wp35qmrHg1tNyPhd7VBlikMrgtrWX9IaPZ40dnrGG/WjO FYB3CKxGb0pTCj7GC4ubxo2upeWZqHLmdIVc7Nzsfp8EcwJbTj+jZ2Zfq6F8y+je sbgh8CaxYn4hEs23aPYRq5H4/buVmZhUw3/AAL9ZmyX6AtAQ0HktVtQe7ykP7DLs EpeLG+vPJFY363QeDsLHwOoxnZSfGziVlB4N/KqIkixNWcFTG8GSE1zKcdJVNoW+ 3MB3+FtMZWUJhH0FyKg5qLaJCtC7Yo5gsddU+QCqTn6gcZBnMX5j4LaAmW4hh1RX ffwwsbfviK5uhXQCeUnbUaokieetDx4s6Kay6t9ahTRr0r/Z3VWzvr+xATxNWZzi xTdezCGOB2ycZ0vq4bKXBuN8CAyOy5X1hf7Rc1BiAVQCILHJDtz0Ak/Hax6DAa2A Hnx9YlugHQf000KroLEY+GaxqYEAEQEAAYkCHwQYAQgACQUCVSHUBQIbDAAKCRD8 YkZDSHA05RtyEACdOEmGolL1xG6I+lDVdot6oBZqC9e021aLWqCUpWJFDp0m0aTm CfmOI1gTaFjScxhq1W0GPUoJKUZhk3tlVfdSCtUckI+xuWKEfqJYtvUtTXpK4jDe aZBovJ3KNpJRIynbr1566zCSQJhHiCGWmE/M5KN3gPsORbCBQXEkONSVsslf1Wm6 6hU6uqSWUaceD+4fl5LClbck1DPWchAP7+uLKPEOtORyH6KRTgKl73zYo7xU1K4Q MN/1aMjobPkqNvvkXnUNwO7QMz18Nx+WqPc4ksJgW1O1aPQ2qL/ARY5jatZ6BBd7 iytfz7d6JOh0FOIlmhBqbWd7fEGrLsSA+EjBGBwW5BnIMmxP1xhjhwrcI18y8kAK 5UzdW2hbbAlc2rlsuxEc+xOYh8kGcc+mZ1j/aMn4gALsTbSO/0T+YJhfODNnL1dC j7oPbJGmmG6pb/o7P4azBUVC9lHOuV3XlAPjSmJylnNsV7+PxwPlXlvKgh4S4C4Z PUc/iPetsxXR2djccOoNxVU4CqJBqYKgul/pUphXkh7QfEKyH+42UETbVhstdBVU azJ6SeUnv9ClVDGsCEhfEZfNOnOoDzJGxDfESoAw7ih91vIhTyHHsK83p2HLDMLP ptLzx/0AFBfo6MWGGpd2RSnMWNbvh59wiThlDeI+Das3ln5nsAo67dMYdA== =fjOq -----END PGP PUBLIC KEY BLOCK----- ================================================ FILE: contrib/build-wine/make_win.sh ================================================ #!/bin/bash set -e here="$(dirname "$(readlink -e "$0")")" test -n "$here" -a -d "$here" || exit if [ -z "$WIN_ARCH" ] ; then export WIN_ARCH="win64" # default fi if [ "$WIN_ARCH" = "win32" ] ; then export GCC_TRIPLET_HOST="i686-w64-mingw32" elif [ "$WIN_ARCH" = "win64" ] ; then export GCC_TRIPLET_HOST="x86_64-w64-mingw32" else echo "unexpected WIN_ARCH: $WIN_ARCH" exit 1 fi export BUILD_TYPE="wine" export GCC_TRIPLET_BUILD="x86_64-pc-linux-gnu" export GCC_STRIP_BINARIES="1" export CONTRIB="$here/.." export PROJECT_ROOT="$CONTRIB/.." export CACHEDIR="$here/.cache/$WIN_ARCH/build" export PIP_CACHE_DIR="$here/.cache/$WIN_ARCH/wine_pip_cache" export WINE_PIP_CACHE_DIR="c:/electrum/contrib/build-wine/.cache/$WIN_ARCH/wine_pip_cache" export DLL_TARGET_DIR="$CACHEDIR/dlls" export WINEPREFIX="/opt/wine64" export WINEDEBUG=-all export WINE_PYHOME="c:/python3" export WINE_PYTHON="wine $WINE_PYHOME/python.exe -B" . "$CONTRIB"/build_tools_util.sh git -C "$PROJECT_ROOT" rev-parse 2>/dev/null || fail "Building outside a git clone is not supported." info "Clearing $here/build and $here/dist..." rm "$here"/build/* -rf rm "$here"/dist/* -rf mkdir -p "$CACHEDIR" "$DLL_TARGET_DIR" "$PIP_CACHE_DIR" if ls "$DLL_TARGET_DIR"/libsecp256k1-*.dll 1> /dev/null 2>&1; then info "libsecp256k1 already built, skipping" else "$CONTRIB"/make_libsecp256k1.sh || fail "Could not build libsecp" fi if [ -f "$DLL_TARGET_DIR/libzbar-0.dll" ]; then info "libzbar already built, skipping" else ( # iconv is needed for zbar. see https://github.com/mchehab/zbar/blob/a549566ea11eb03622bd4458a1728ffe3f589163/README-windows.md # (previously were using win-iconv, but changed to GNU libiconv due to compilation errors with modern gcc) LIBICONV_VER="1.18" download_if_not_exist "$CACHEDIR/libiconv-${LIBICONV_VER}.tar.gz" "https://ftp.gnu.org/pub/gnu/libiconv/libiconv-${LIBICONV_VER}.tar.gz" verify_hash "$CACHEDIR/libiconv-${LIBICONV_VER}.tar.gz" "3b08f5f4f9b4eb82f151a7040bfd6fe6c6fb922efe4b1659c66ea933276965e8" tar xf "$CACHEDIR/libiconv-${LIBICONV_VER}.tar.gz" -C "$CACHEDIR" # ref https://github.com/msys2/MINGW-packages/blob/7f68e9f2488737bbe03888ade094eaee8021d1c5/mingw-w64-libiconv/PKGBUILD info "Building libiconv..." cd "$CACHEDIR/libiconv-${LIBICONV_VER}" # Patches taken from msys2/MINGW-packages patch -p1 < "$here/patches/libiconv-fix-pointer-buf.patch" ./configure \ $AUTOCONF_FLAGS \ --prefix="/usr/${GCC_TRIPLET_HOST}" \ --disable-static \ --enable-shared \ --enable-extra-encodings \ --enable-relocatable \ --disable-rpath \ --enable-silent-rules \ --enable-nls CC="${GCC_TRIPLET_HOST}-gcc" make "-j$CPU_COUNT" || fail "Could not build libiconv" cp -fpv "libcharset/lib/.libs/libcharset-1.dll" "$DLL_TARGET_DIR/" || fail "Could not copy the libcharset binary to DLL_TARGET_DIR" cp -fpv "lib/.libs/libiconv-2.dll" "$DLL_TARGET_DIR/" || fail "Could not copy the libiconv binary to DLL_TARGET_DIR" # FIXME avoid using sudo sudo make install || fail "Could not install libiconv" # workaround to delete files owned by root, created by "make install": make clean ) "$CONTRIB"/make_zbar.sh || fail "Could not build zbar" fi if [ -f "$DLL_TARGET_DIR/libusb-1.0.dll" ]; then info "libusb already built, skipping" else "$CONTRIB"/make_libusb.sh || fail "Could not build libusb" fi "$here/prepare-wine.sh" || fail "prepare-wine failed" info "Resetting modification time in C:\Python..." # (Because of some bugs in pyinstaller) pushd /opt/wine64/drive_c/python* find -exec touch -h -d '2000-11-11T11:11:11+00:00' {} + popd ls -l /opt/wine64/drive_c/python* "$here/build-electrum-git.sh" || fail "build-electrum-git failed" info "Done." ================================================ FILE: contrib/build-wine/patches/libiconv-fix-pointer-buf.patch ================================================ --- a/lib/iconv.c 2018-05-03 23:18:55.997221700 -0400 +++ b/lib/iconv.c 2018-05-03 23:26:47.611682700 -0400 @@ -170,12 +170,12 @@ static const struct stringpool2_t string #include "aliases2.h" #undef S }; #define stringpool2 ((const char *) &stringpool2_contents) static const struct alias sysdep_aliases[] = { -#define S(tag,name,encoding_index) { (int)(long)&((struct stringpool2_t *)0)->stringpool_##tag, encoding_index }, +#define S(tag,name,encoding_index) { (int)(intptr_t)&((struct stringpool2_t *)0)->stringpool_##tag, encoding_index }, #include "aliases2.h" #undef S }; #ifdef __GNUC__ __inline #else --- a/lib/genaliases.c 2023-01-14 00:00:00.000000000 +0000 +++ b/lib/genaliases.c 2023-01-14 10:18:00.000000000 +0000 @@ -50,7 +50,7 @@ putc(c, out2); } } - fprintf(out2,"\")' tmp.h | sed -e 's|^.*\\(stringpool_str[0-9]*\\).*$| (int)(long)\\&((struct stringpool_t *)0)->\\1,|'\n"); + fprintf(out2,"\")' tmp.h | sed -e 's|^.*\\(stringpool_str[0-9]*\\).*$| (int)(intptr_t)\\&((struct stringpool_t *)0)->\\1,|'\n"); for (; n > 0; names++, n--) emit_alias(out1, *names, c_name); } --- a/lib/genaliases2.c 2023-01-14 00:00:00.000000000 +0000 +++ b/lib/genaliases2.c 2023-01-14 10:18:00.000000000 +0000 @@ -44,6 +44,6 @@ static void emit_encoding (FILE* out1, FILE* out2, const char* tag, const char* const* names, size_t n, const char* c_name) { - fprintf(out2," (int)(long)&((struct stringpool2_t *)0)->stringpool_%s_%u,\n",tag,counter); + fprintf(out2," (int)(intptr_t)&((struct stringpool2_t *)0)->stringpool_%s_%u,\n",tag,counter); for (; n > 0; names++, n--) emit_alias(out1, tag, *names, c_name); } ================================================ FILE: contrib/build-wine/prepare-wine.sh ================================================ #!/bin/bash PYINSTALLER_REPO="https://github.com/pyinstaller/pyinstaller.git" PYINSTALLER_COMMIT="306d4d92580fea7be7ff2c89ba112cdc6f73fac1" # ^ tag "v6.13.0" PYTHON_VERSION=3.12.10 # Let's begin! set -e here="$(dirname "$(readlink -e "$0")")" . "$CONTRIB"/build_tools_util.sh info "Booting wine." wine 'wineboot' cd "$CACHEDIR" mkdir -p $WINEPREFIX/drive_c/tmp info "Installing Python." # note: you might need "sudo apt-get install dirmngr" for the following # keys from https://www.python.org/downloads/#pubkeys KEYRING_PYTHON_DEV="keyring-electrum-build-python-dev.gpg" gpg --no-default-keyring --keyring $KEYRING_PYTHON_DEV --import "$here"/gpg_keys/7ED10B6531D7C8E1BC296021FC624643487034E5.asc if [ "$WIN_ARCH" = "win32" ] ; then PYARCH="win32" elif [ "$WIN_ARCH" = "win64" ] ; then PYARCH="amd64" else fail "unexpected WIN_ARCH: $WIN_ARCH" fi PYTHON_DOWNLOADS="$CACHEDIR/python$PYTHON_VERSION" mkdir -p "$PYTHON_DOWNLOADS" for msifile in core dev exe lib pip; do echo "Installing $msifile..." download_if_not_exist "$PYTHON_DOWNLOADS/${msifile}.msi" "https://www.python.org/ftp/python/$PYTHON_VERSION/$PYARCH/${msifile}.msi" download_if_not_exist "$PYTHON_DOWNLOADS/${msifile}.msi.asc" "https://www.python.org/ftp/python/$PYTHON_VERSION/$PYARCH/${msifile}.msi.asc" verify_signature "$PYTHON_DOWNLOADS/${msifile}.msi.asc" $KEYRING_PYTHON_DEV || fail "invalid sig for ${msifile}.msi" wine msiexec /i "$PYTHON_DOWNLOADS/${msifile}.msi" /qb TARGETDIR=$WINE_PYHOME || fail "wine msiexec failed for ${msifile}.msi" done break_legacy_easy_install info "Installing build dependencies." $WINE_PYTHON -m pip install --no-build-isolation --no-dependencies --no-warn-script-location \ --cache-dir "$WINE_PIP_CACHE_DIR" -r "$CONTRIB"/deterministic-build/requirements-build-base.txt $WINE_PYTHON -m pip install --no-build-isolation --no-dependencies --no-binary :all: --no-warn-script-location \ --cache-dir "$WINE_PIP_CACHE_DIR" -r "$CONTRIB"/deterministic-build/requirements-build-wine.txt # copy already built DLLs cp "$DLL_TARGET_DIR"/*.dll "$WINEPREFIX/drive_c/electrum/electrum/" || fail "Could not copy DLLs to destination" info "Building PyInstaller." # we build our own PyInstaller boot loader as the default one has high # anti-virus false positives ( if [ "$WIN_ARCH" = "win32" ] ; then PYINST_ARCH="32bit" elif [ "$WIN_ARCH" = "win64" ] ; then PYINST_ARCH="64bit" else fail "unexpected WIN_ARCH: $WIN_ARCH" fi if [ -f "$CACHEDIR/pyinstaller/PyInstaller/bootloader/Windows-$PYINST_ARCH-intel/runw.exe" ]; then info "pyinstaller already built, skipping" exit 0 fi cd "$WINEPREFIX/drive_c/electrum" ELECTRUM_COMMIT_HASH=$(git rev-parse HEAD) cd "$CACHEDIR" rm -rf pyinstaller mkdir pyinstaller cd pyinstaller # Shallow clone git init git remote add origin $PYINSTALLER_REPO git fetch --depth 1 origin $PYINSTALLER_COMMIT git checkout -b pinned "${PYINSTALLER_COMMIT}^{commit}" rm -fv PyInstaller/bootloader/Windows-*/run*.exe || true # add reproducible randomness. this ensures we build a different bootloader for each commit. # if we built the same one for all releases, that might also get anti-virus false positives echo "const char *electrum_tag = \"tagged by Electrum@$ELECTRUM_COMMIT_HASH\";" >> ./bootloader/src/pyi_main.c pushd bootloader # cross-compile to Windows using host python python3 ./waf all CC="${GCC_TRIPLET_HOST}-gcc" \ CFLAGS="-static" popd # sanity check bootloader is there: [[ -e "PyInstaller/bootloader/Windows-$PYINST_ARCH-intel/runw.exe" ]] || fail "Could not find runw.exe in target dir!" ) info "Installing PyInstaller." $WINE_PYTHON -m pip install --no-build-isolation --no-dependencies --no-warn-script-location ./pyinstaller info "Wine is configured." ================================================ FILE: contrib/build-wine/pyinstaller.spec ================================================ # -*- mode: python -*- import sys import os from typing import TYPE_CHECKING from PyInstaller.utils.hooks import collect_data_files, collect_submodules, collect_dynamic_libs, copy_metadata if TYPE_CHECKING: from PyInstaller.building.build_main import Analysis, PYZ, EXE, COLLECT PYPKG="electrum" MAIN_SCRIPT="run_electrum" PROJECT_ROOT = "C:/electrum" ICONS_FILE=f"{PROJECT_ROOT}/{PYPKG}/gui/icons/electrum.ico" cmdline_name = os.environ.get("ELECTRUM_CMDLINE_NAME") if not cmdline_name: raise Exception('no name') # see https://github.com/pyinstaller/pyinstaller/issues/2005 hiddenimports = [] hiddenimports += collect_submodules('pkg_resources') # workaround for https://github.com/pypa/setuptools/issues/1963 hiddenimports += collect_submodules(f"{PYPKG}.plugins") binaries = [] # Workaround for "Retro Look": binaries += [b for b in collect_dynamic_libs('PyQt6') if 'qwindowsvista' in b[0]] # add libsecp256k1, libusb, etc: binaries += [(f"{PROJECT_ROOT}/{PYPKG}/*.dll", '.')] datas = [ (f"{PROJECT_ROOT}/{PYPKG}/*.json", PYPKG), (f"{PROJECT_ROOT}/{PYPKG}/lnwire/*.csv", f"{PYPKG}/lnwire"), (f"{PROJECT_ROOT}/{PYPKG}/wordlist/english.txt", f"{PYPKG}/wordlist"), (f"{PROJECT_ROOT}/{PYPKG}/wordlist/slip39.txt", f"{PYPKG}/wordlist"), (f"{PROJECT_ROOT}/{PYPKG}/chains", f"{PYPKG}/chains"), (f"{PROJECT_ROOT}/{PYPKG}/locale", f"{PYPKG}/locale"), (f"{PROJECT_ROOT}/{PYPKG}/plugins", f"{PYPKG}/plugins"), (f"{PROJECT_ROOT}/{PYPKG}/gui/icons", f"{PYPKG}/gui/icons"), (f"{PROJECT_ROOT}/{PYPKG}/gui/fonts", f"{PYPKG}/gui/fonts"), ] datas += collect_data_files(f"{PYPKG}.plugins") datas += collect_data_files('trezorlib') # TODO is this needed? and same question for other hww libs datas += collect_data_files('safetlib') datas += collect_data_files('ckcc') datas += collect_data_files('bitbox02') # some deps rely on importlib metadata datas += copy_metadata('slip10') # from trezor->slip10 # Exclude parts of Qt that we never use. Reduces binary size by tens of MBs. see #4815 excludes = [ "PyQt6.QtBluetooth", "PyQt6.QtDesigner", "PyQt6.QtNfc", "PyQt6.QtPositioning", "PyQt6.QtQml", "PyQt6.QtQuick", "PyQt6.QtQuick3D", "PyQt6.QtQuickWidgets", "PyQt6.QtRemoteObjects", "PyQt6.QtSensors", "PyQt6.QtSerialPort", "PyQt6.QtSpatialAudio", "PyQt6.QtSql", "PyQt6.QtTest", "PyQt6.QtTextToSpeech", "PyQt6.QtWebChannel", "PyQt6.QtWebSockets", "PyQt6.QtXml", # "PyQt6.QtNetwork", # needed by QtMultimedia. kinda weird but ok. ] # We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports a = Analysis([f"{PROJECT_ROOT}/{MAIN_SCRIPT}", f"{PROJECT_ROOT}/{PYPKG}/gui/qt/main_window.py", f"{PROJECT_ROOT}/{PYPKG}/gui/qt/qrreader/qtmultimedia/camera_dialog.py", f"{PROJECT_ROOT}/{PYPKG}/gui/text.py", f"{PROJECT_ROOT}/{PYPKG}/util.py", f"{PROJECT_ROOT}/{PYPKG}/wallet.py", f"{PROJECT_ROOT}/{PYPKG}/simple_config.py", f"{PROJECT_ROOT}/{PYPKG}/bitcoin.py", f"{PROJECT_ROOT}/{PYPKG}/dnssec.py", f"{PROJECT_ROOT}/{PYPKG}/commands.py", ], binaries=binaries, datas=datas, hiddenimports=hiddenimports, hookspath=[], excludes=excludes, ) # http://stackoverflow.com/questions/19055089/pyinstaller-onefile-warning-pyconfig-h-when-importing-scipy-or-scipy-signal for d in a.datas: if 'pyconfig' in d[0]: a.datas.remove(d) break # hotfix for #3171 (pre-Win10 binaries) a.binaries = [x for x in a.binaries if not x[1].lower().startswith(r'c:\windows')] pyz = PYZ(a.pure) ##### # "standalone" exe with all dependencies packed into it exe_standalone = EXE( pyz, a.scripts, a.binaries, a.datas, name=os.path.join("build", "pyi.win32", PYPKG, f"{cmdline_name}.exe"), debug=False, strip=None, upx=False, icon=ICONS_FILE, console=False) # console=True makes an annoying black box pop up, but it does make Electrum output command line commands, with this turned off no output will be given but commands can still be used exe_portable = EXE( pyz, a.scripts, a.binaries, a.datas + [('is_portable', 'README.md', 'DATA')], name=os.path.join("build", "pyi.win32", PYPKG, f"{cmdline_name}-portable.exe"), debug=False, strip=None, upx=False, icon=ICONS_FILE, console=False) ##### # exe and separate files that NSIS uses to build installer "setup" exe exe_inside_setup_noconsole = EXE( pyz, a.scripts, exclude_binaries=True, name=os.path.join("build", "pyi.win32", PYPKG, f"{cmdline_name}.exe"), debug=False, strip=None, upx=False, icon=ICONS_FILE, console=False) exe_inside_setup_console = EXE( pyz, a.scripts, exclude_binaries=True, name=os.path.join("build", "pyi.win32", PYPKG, f"{cmdline_name}-debug.exe"), debug=False, strip=None, upx=False, icon=ICONS_FILE, console=True) coll = COLLECT( exe_inside_setup_noconsole, exe_inside_setup_console, a.binaries, a.zipfiles, a.datas, strip=None, upx=True, debug=False, icon=ICONS_FILE, console=False, name=os.path.join('dist', PYPKG)) ================================================ FILE: contrib/build-wine/sign.sh ================================================ #!/bin/bash set -e here="$(dirname "$0")" if [ -z "$WIN_SIGNING_PASSWORD" ]; then echo "password missing" exit 1 fi test -n "$here" -a -d "$here" || exit cd $here CERT_FILE=${CERT_FILE:-~/codesigning/cert.pem} KEY_FILE=${KEY_FILE:-~/codesigning/key.pem} if [[ ! -f "$CERT_FILE" ]]; then ls "$CERT_FILE" echo "Make sure that $CERT_FILE and $KEY_FILE exist" fi if ! which osslsigncode > /dev/null 2>&1; then echo "Please install osslsigncode" fi rm -rf signed mkdir -p signed >/dev/null 2>&1 cd dist echo "Found $(ls *.exe | wc -w) files to sign." for f in $(ls *.exe); do echo "Signing $f..." osslsigncode sign \ -pass "$WIN_SIGNING_PASSWORD" \ -h sha256 \ -certs "$CERT_FILE" \ -key "$KEY_FILE" \ -n "Electrum" \ -i "https://electrum.org/" \ -t "http://timestamp.digicert.com/" \ -in "$f" \ -out "../signed/$f" ls "../signed/$f" -lah done ================================================ FILE: contrib/build-wine/unsign.sh ================================================ #!/bin/bash # exit if command fails set -e PROJECT_ROOT="$(dirname "$(readlink -e "$0")")/../.." CONTRIB="$PROJECT_ROOT/contrib" here="$(dirname "$0")" test -n "$here" -a -d "$here" || exit cd "$here" if ! which osslsigncode > /dev/null 2>&1; then echo "Please install osslsigncode" exit 1 fi rm -rf signed/stripped mkdir -p signed >/dev/null 2>&1 mkdir -p signed/stripped >/dev/null 2>&1 version=$("$CONTRIB"/print_electrum_version.py) echo "Found $(ls dist/*.exe | wc -w) files to verify." for mine in dist/*.exe; do echo "---------------" f="$(basename "$mine")" if test -f "signed/$f"; then echo "Found file at signed/$f" else echo "Downloading https://download.electrum.org/$version/$f" wget -q "https://download.electrum.org/$version/$f" -O "signed/$f" fi out="signed/stripped/$f" # Remove PE signature from signed binary osslsigncode remove-signature -in "signed/$f" -out "$out" > /dev/null 2>&1 chmod +x "$out" if cmp -s "$out" "$mine"; then echo "Success: $f" #gpg --sign --armor --detach signed/$f else echo "Failure: $f" exit 1 fi done exit 0 ================================================ FILE: contrib/build_tools_util.sh ================================================ #!/usr/bin/env bash set -e # Set a fixed umask as this leaks into docker containers umask 0022 RED='\033[0;31m' BLUE='\033[0;34m' YELLOW='\033[0;33m' NC='\033[0m' # No Color function info { printf "\r💬 ${BLUE}INFO:${NC} ${1}\n" } function fail { printf "\r🗯 ${RED}ERROR:${NC} ${1}\n" exit 1 } function warn { printf "\r⚠️ ${YELLOW}WARNING:${NC} ${1}\n" } # based on https://superuser.com/questions/497940/script-to-verify-a-signature-with-gpg function verify_signature() { local file=$1 keyring=$2 out= if out=$(gpg --no-default-keyring --keyring "$keyring" --status-fd 1 --verify "$file" 2>/dev/null) && echo "$out" | grep -qs "^\[GNUPG:\] VALIDSIG "; then return 0 else echo "$out" >&2 exit 1 fi } function verify_hash() { local file=$1 expected_hash=$2 actual_hash=$(sha256sum "$file" | awk '{print $1}') if [ "$actual_hash" == "$expected_hash" ]; then return 0 else echo "$file $actual_hash (unexpected hash)" >&2 rm "$file" exit 1 fi } function download_if_not_exist() { local file_name=$1 url=$2 if [ ! -e "$file_name" ] ; then wget -O "$file_name" "$url" fi } # Function to clone or update a git repository to a specific commit clone_or_update_repo() { local repo_url=$1 local commit_hash=$2 local repo_dir=$3 if [ -z "$repo_url" ] || [ -z "$commit_hash" ] || [ -z "$repo_dir" ]; then fail "clone_or_update_repo: invalid arguments: repo_url='$repo_url', commit_hash='$commit_hash', repo_dir='$repo_dir'" fi if [ -d "$repo_dir" ]; then info "Repository $repo_url exists in $repo_dir, updating..." git -C "$repo_dir" clean -ffxd >/dev/null 2>&1 || fail "Failed to clean repository $repo_dir" git -C "$repo_dir" fetch --all >/dev/null 2>&1 || fail "Failed to fetch from repository" git -C "$repo_dir" reset --hard "$commit_hash^{commit}" >/dev/null 2>&1 || fail "Failed to reset to commit $commit_hash" else info "Cloning repository: $repo_url to $repo_dir" git clone "$repo_url" "$repo_dir" >/dev/null 2>&1 || fail "Failed to clone repository $repo_url" git -C "$repo_dir" checkout "$commit_hash^{commit}" >/dev/null 2>&1 || fail "Failed to checkout commit $commit_hash" fi } apply_patch() { local patch=$1 local path=$2 if [ -z "$patch" ] || [ -z "$path" ]; then fail "apply_patch: invalid arguments: patch='$patch', path='$path'" fi if [ -d "$path" ]; then info "Patching: $patch" cd "$path" patch -p1 <"$patch" cd - else fail "apply_patch: path='$path' not found" fi } # https://github.com/travis-ci/travis-build/blob/master/lib/travis/build/templates/header.sh function retry() { local result=0 local count=1 while [ $count -le 3 ]; do [ $result -ne 0 ] && { echo -e "\nThe command \"$@\" failed. Retrying, $count of 3.\n" >&2 } ! { "$@"; result=$?; } [ $result -eq 0 ] && break count=$(($count + 1)) sleep 1 done [ $count -gt 3 ] && { echo -e "\nThe command \"$@\" failed 3 times.\n" >&2 } return $result } function gcc_with_triplet() { TRIPLET="$1" CMD="$2" shift 2 if [ -n "$TRIPLET" ] ; then "$TRIPLET-$CMD" "$@" else "$CMD" "$@" fi } function gcc_host() { gcc_with_triplet "$GCC_TRIPLET_HOST" "$@" } function gcc_build() { gcc_with_triplet "$GCC_TRIPLET_BUILD" "$@" } function host_strip() { if [ "$GCC_STRIP_BINARIES" -ne "0" ] ; then case "$BUILD_TYPE" in linux|wine) gcc_host strip "$@" ;; darwin) # TODO: Strip on macOS? ;; esac fi } # on MacOS, there is no realpath by default if ! [ -x "$(command -v realpath)" ]; then function realpath() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}" } fi export SOURCE_DATE_EPOCH=1530212462 export ZERO_AR_DATE=1 # for macOS export PYTHONHASHSEED=22 # Set the build type, overridden by wine build export BUILD_TYPE="${BUILD_TYPE:-$(uname | tr '[:upper:]' '[:lower:]')}" # Add host / build flags if the triplets are set if [ -n "$GCC_TRIPLET_HOST" ] ; then export AUTOCONF_FLAGS="$AUTOCONF_FLAGS --host=$GCC_TRIPLET_HOST" fi if [ -n "$GCC_TRIPLET_BUILD" ] ; then export AUTOCONF_FLAGS="$AUTOCONF_FLAGS --build=$GCC_TRIPLET_BUILD" fi export GCC_STRIP_BINARIES="${GCC_STRIP_BINARIES:-0}" if [ -n "$CIRRUS_CPU" ] ; then # special-case for CI. see https://github.com/cirruslabs/cirrus-ci-docs/issues/1115 export CPU_COUNT="$CIRRUS_CPU" else export CPU_COUNT="$(nproc 2> /dev/null || sysctl -n hw.ncpu)" fi info "Found $CPU_COUNT CPUs, which we might use for building." function break_legacy_easy_install() { # We don't want setuptools sneakily installing dependencies, invisible to pip. # This ensures that if setuptools calls distutils which then calls easy_install, # easy_install will not download packages over the network. # see https://pip.pypa.io/en/stable/reference/pip_install/#controlling-setup-requires # see https://github.com/pypa/setuptools/issues/1916#issuecomment-743350566 info "Intentionally breaking legacy easy_install." DISTUTILS_CFG="${HOME}/.pydistutils.cfg" DISTUTILS_CFG_BAK="${HOME}/.pydistutils.cfg.orig" # If we are not inside docker, we might be overwriting a config file on the user's system... if [ -e "$DISTUTILS_CFG" ] && [ ! -e "$DISTUTILS_CFG_BAK" ]; then warn "Overwriting python distutils config file at '$DISTUTILS_CFG'. A copy will be saved at '$DISTUTILS_CFG_BAK'." mv "$DISTUTILS_CFG" "$DISTUTILS_CFG_BAK" fi cat < "$DISTUTILS_CFG" [easy_install] index_url = '' find_links = '' EOF } ================================================ FILE: contrib/deterministic-build/README.md ================================================ # Notes The frozen dependency lists in this folder are *generated* files. - Starting from `contrib/requirements/requirements*.txt`, - we use the `contrib/freeze_packages.sh` script, - to generate `contrib/deterministic-build/requirements*.txt`. The source files list direct dependencies with loose version requirements, while the output files list all transitive dependencies with exact version+hash pins. The build scripts only use these hash pinned requirement files. ================================================ FILE: contrib/deterministic-build/check_submodules.sh ================================================ #!/usr/bin/env bash set -e PROJECT_ROOT="$(dirname "$(readlink -e "$0")")/../.." LOCALE="$PROJECT_ROOT/electrum/locale/" cd "$PROJECT_ROOT" git submodule init git submodule update function get_git_mtime { if [ $# -eq 1 ]; then git log --pretty=%at -n1 -- $1 else git log --pretty=%ar -n1 -- $2 fi } fail=0 if [ $(date +%s -d "2 weeks ago") -gt $(get_git_mtime "$LOCALE") ]; then echo "Last update from electrum-locale is older than 2 weeks."\ "Please update it to incorporate the latest translations from crowdin." fail=1 fi exit ${fail} ================================================ FILE: contrib/deterministic-build/find_restricted_dependencies.py ================================================ #!/usr/bin/env python3 import sys try: import requests except ImportError as e: sys.exit(f"Error: {str(e)}. Try 'python3 -m pip install '") def is_dependency_edge_blacklisted(*, parent_pkg: str, dep: str) -> bool: """Sometimes a package declares a hard dependency for some niche functionality that we really do not care about. """ dep = dep.lower() parent_pkg = parent_pkg.lower() return (parent_pkg, dep) in { ("qrcode", "colorama"), # only needed for using qrcode-CLI on Windows. ("click", "colorama"), # 'click' is a CLI tool, and it only needs colorama on Windows. # In fact, we should blacklist 'click' itself, but that should be done elsewhere. } def check_restriction(*, dep: str, restricted: str, parent_pkg: str): # See: https://www.python.org/dev/peps/pep-0496/ # Hopefully we don't need to parse the whole microlanguage if is_dependency_edge_blacklisted(dep=dep, parent_pkg=parent_pkg): return False if "extra" in restricted and "[" not in dep: return False for marker in ["os_name", "platform_release", "sys_platform", "platform_system"]: if marker in restricted: return True return False def main(): for p in sys.stdin.read().split(): p = p.strip() if not p: continue assert "==" in p, "This script expects a list of packages with pinned version, e.g. package==1.2.3, not {}".format(p) p, v = p.rsplit("==", 1) try: data = requests.get("https://pypi.org/pypi/{}/{}/json".format(p, v)).json()["info"] except ValueError: raise Exception("Package could not be found: {}=={}".format(p, v)) try: for r in data["requires_dist"]: # type: str if ";" not in r: continue # example value for "r" at this point: "pefile (>=2017.8.1) ; sys_platform == \"win32\"" dep, restricted = r.split(";", 1) dep = dep.strip() restricted = restricted.strip() dep_basename = dep.split(" ")[0] if check_restriction(dep=dep, restricted=restricted, parent_pkg=p): print(dep_basename, sep=" ") print("Installing {} from {} although it is only needed for {}".format(dep, p, restricted), file=sys.stderr) except TypeError: # Has no dependencies at all continue if __name__ == "__main__": main() ================================================ FILE: contrib/deterministic-build/requirements-binaries-mac.txt ================================================ cffi==1.17.1 \ --hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \ --hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \ --hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \ --hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \ --hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \ --hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \ --hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \ --hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \ --hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \ --hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \ --hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \ --hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \ --hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \ --hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \ --hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \ --hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \ --hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \ --hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \ --hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \ --hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \ --hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \ --hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \ --hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \ --hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \ --hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \ --hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \ --hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \ --hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \ --hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \ --hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \ --hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \ --hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \ --hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \ --hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \ --hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \ --hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \ --hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \ --hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \ --hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \ --hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \ --hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \ --hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \ --hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \ --hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \ --hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \ --hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \ --hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \ --hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \ --hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \ --hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \ --hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \ --hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \ --hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \ --hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \ --hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \ --hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \ --hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \ --hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \ --hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \ --hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \ --hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \ --hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \ --hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \ --hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \ --hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \ --hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \ --hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b cryptography==45.0.3 \ --hash=sha256:00094838ecc7c6594171e8c8a9166124c1197b074cfca23645cee573910d76bc \ --hash=sha256:050ce5209d5072472971e6efbfc8ec5a8f9a841de5a4db0ebd9c2e392cb81972 \ --hash=sha256:232954730c362638544758a8160c4ee1b832dc011d2c41a306ad8f7cccc5bb0b \ --hash=sha256:25286aacb947286620a31f78f2ed1a32cded7be5d8b729ba3fb2c988457639e4 \ --hash=sha256:2f8f8f0b73b885ddd7f3d8c2b2234a7d3ba49002b0223f58cfde1bedd9563c56 \ --hash=sha256:38deed72285c7ed699864f964a3f4cf11ab3fb38e8d39cfcd96710cd2b5bb716 \ --hash=sha256:3ad69eeb92a9de9421e1f6685e85a10fbcfb75c833b42cc9bc2ba9fb00da4710 \ --hash=sha256:5555365a50efe1f486eed6ac7062c33b97ccef409f5970a0b6f205a7cfab59c8 \ --hash=sha256:555e5e2d3a53b4fabeca32835878b2818b3f23966a4efb0d566689777c5a12c8 \ --hash=sha256:57a6500d459e8035e813bd8b51b671977fb149a8c95ed814989da682314d0782 \ --hash=sha256:5833bb4355cb377ebd880457663a972cd044e7f49585aee39245c0d592904578 \ --hash=sha256:71320fbefd05454ef2d457c481ba9a5b0e540f3753354fff6f780927c25d19b0 \ --hash=sha256:7573d9eebaeceeb55285205dbbb8753ac1e962af3d9640791d12b36864065e71 \ --hash=sha256:92d5f428c1a0439b2040435a1d6bc1b26ebf0af88b093c3628913dd464d13fa1 \ --hash=sha256:97787952246a77d77934d41b62fb1b6f3581d83f71b44796a4158d93b8f5c490 \ --hash=sha256:9bb5bf55dcb69f7067d80354d0a348368da907345a2c448b0babc4215ccd3497 \ --hash=sha256:9cc80ce69032ffa528b5e16d217fa4d8d4bb7d6ba8659c1b4d74a1b0f4235fca \ --hash=sha256:9e4253ed8f5948a3589b3caee7ad9a5bf218ffd16869c516535325fece163dcc \ --hash=sha256:9eda14f049d7f09c2e8fb411dda17dd6b16a3c76a1de5e249188a32aeb92de19 \ --hash=sha256:a2b56de3417fd5f48773ad8e91abaa700b678dc7fe1e0c757e1ae340779acf7b \ --hash=sha256:af3f92b1dc25621f5fad065288a44ac790c5798e986a34d393ab27d2b27fcff9 \ --hash=sha256:c5edcb90da1843df85292ef3a313513766a78fbbb83f584a5a58fb001a5a9d57 \ --hash=sha256:c824c9281cb628015bfc3c59335163d4ca0540d49de4582d6c2637312907e4b1 \ --hash=sha256:c92519d242703b675ccefd0f0562eb45e74d438e001f8ab52d628e885751fb06 \ --hash=sha256:ca932e11218bcc9ef812aa497cdf669484870ecbcf2d99b765d6c27a86000942 \ --hash=sha256:cb6ab89421bc90e0422aca911c69044c2912fc3debb19bb3c1bfe28ee3dff6ab \ --hash=sha256:cfd84777b4b6684955ce86156cfb5e08d75e80dc2585e10d69e47f014f0a5342 \ --hash=sha256:d377dde61c5d67eb4311eace661c3efda46c62113ff56bf05e2d679e02aebb5b \ --hash=sha256:d54ae41e6bd70ea23707843021c778f151ca258081586f0cfa31d936ae43d1b2 \ --hash=sha256:dc10ec1e9f21f33420cc05214989544727e776286c1c16697178978327b95c9c \ --hash=sha256:ec21313dd335c51d7877baf2972569f40a4291b76a0ce51391523ae358d05899 \ --hash=sha256:ec64ee375b5aaa354b2b273c921144a660a511f9df8785e6d1c942967106438e \ --hash=sha256:ed43d396f42028c1f47b5fec012e9e12631266e3825e95c00e3cf94d472dac49 \ --hash=sha256:edd6d51869beb7f0d472e902ef231a9b7689508e83880ea16ca3311a00bf5ce7 \ --hash=sha256:f22af3c78abfbc7cbcdf2c55d23c3e022e1a462ee2481011d518c7fb9c9f3d65 \ --hash=sha256:fae1e637f527750811588e4582988932c222f8251f7b7ea93739acb624e1487f \ --hash=sha256:fed5aaca1750e46db870874c9c273cd5182a9e9deb16f06f7bdffdb5c2bde4b9 pip==25.1.1 \ --hash=sha256:2913a38a2abf4ea6b64ab507bd9e967f3b53dc1ede74b01b0931e1ce548751af \ --hash=sha256:3de45d411d308d5054c2168185d8da7f9a2cd753dbac8acbfa88a8909ecd9077 pycparser==2.22 \ --hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \ --hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc PyQt6==6.6.1 \ --hash=sha256:03a656d5dc5ac31b6a9ad200f7f4f7ef49fa00ad7ce7a991b9bb691617141d12 \ --hash=sha256:5aa0e833cb5a79b93813f8181d9f145517dd5a46f4374544bcd1e93a8beec537 \ --hash=sha256:6b43878d0bbbcf8b7de165d305ec0cb87113c8930c92de748a11c473a6db5085 \ --hash=sha256:9f158aa29d205142c56f0f35d07784b8df0be28378d20a97bcda8bd64ffd0379 PyQt6-Qt6==6.6.2 \ --hash=sha256:5a41fe9d53b9e29e9ec5c23f3c5949dba160f90ca313ee8b96b8ffe6a5059387 \ --hash=sha256:7ef446d3ffc678a8586ff6dc9f0d27caf4dff05dea02c353540d2f614386faf9 \ --hash=sha256:8d7f674a4ec43ca00191e14945ca4129acbe37a2172ed9d08214ad58b170bc11 \ --hash=sha256:b8363d88623342a72ac17da9127dc12f259bb3148796ea029762aa2d499778d9 PyQt6-sip==13.10.2 \ --hash=sha256:061d4a2eb60a603d8be7db6c7f27eb29d9cea97a09aa4533edc1662091ce4f03 \ --hash=sha256:07f77e89d93747dda71b60c3490b00d754451729fbcbcec840e42084bf061655 \ --hash=sha256:0b097eb58b4df936c4a2a88a2f367c8bb5c20ff049a45a7917ad75d698e3b277 \ --hash=sha256:1a6c2f168773af9e6c7ef5e52907f16297d4efd346e4c958eda54ea9135be18e \ --hash=sha256:37af463dcce39285e686d49523d376994d8a2508b9acccb7616c4b117c9c4ed7 \ --hash=sha256:38b5823dca93377f8a4efac3cbfaa1d20229aa5b640c31cf6ebbe5c586333808 \ --hash=sha256:3dde8024d055f496eba7d44061c5a1ba4eb72fc95e5a9d7a0dbc908317e0888b \ --hash=sha256:45ac06f0380b7aa4fcffd89f9e8c00d1b575dc700c603446a9774fda2dcfc0de \ --hash=sha256:464ad156bf526500ce6bd05cac7a82280af6309974d816739b4a9a627156fafe \ --hash=sha256:4ccf197f8fa410e076936bee28ad9abadb450931d5be5625446fd20e0d8b27a6 \ --hash=sha256:4ffa71ddff6ef031d52cd4f88b8bba08b3516313c023c7e5825cf4a0ba598712 \ --hash=sha256:5506b9a795098df3b023cc7d0a37f93d3224a9c040c43804d4bc06e0b2b742b0 \ --hash=sha256:8132ec1cbbecc69d23dcff23916ec07218f1a9bbbc243bf6f1df967117ce303e \ --hash=sha256:83e6a56d3e715f748557460600ec342cbd77af89ec89c4f2a68b185fa14ea46c \ --hash=sha256:8b5d06a0eac36038fa8734657d99b5fe92263ae7a0cd0a67be6acfe220a063e1 \ --hash=sha256:9c67ed66e21b11e04ffabe0d93bc21df22e0a5d7e2e10ebc8c1d77d2f5042991 \ --hash=sha256:ad376a6078da37b049fdf9d6637d71b52727e65c4496a80b753ddc8d27526aca \ --hash=sha256:b1d3cc9015a1bd8c8d3e86a009591e897d4d46b0c514aede7d2970a2208749cd \ --hash=sha256:c7b34a495b92790c70eae690d9e816b53d3b625b45eeed6ae2c0fe24075a237e \ --hash=sha256:c80cc059d772c632f5319632f183e7578cd0976b9498682833035b18a3483e92 \ --hash=sha256:cc6a1dfdf324efaac6e7b890a608385205e652845c62130de919fd73a6326244 \ --hash=sha256:ddd578a8d975bfb5fef83751829bf09a97a1355fa1de098e4fb4d1b74ee872fc \ --hash=sha256:e455a181d45a28ee8d18d42243d4f470d269e6ccdee60f2546e6e71218e05bb4 \ --hash=sha256:e907394795e61f1174134465c889177f584336a98d7a10beade2437bf5942244 setuptools==80.9.0 \ --hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \ --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c wheel==0.45.1 \ --hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 \ --hash=sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248 ================================================ FILE: contrib/deterministic-build/requirements-binaries.txt ================================================ cffi==1.17.1 \ --hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \ --hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \ --hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \ --hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \ --hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \ --hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \ --hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \ --hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \ --hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \ --hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \ --hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \ --hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \ --hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \ --hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \ --hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \ --hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \ --hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \ --hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \ --hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \ --hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \ --hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \ --hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \ --hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \ --hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \ --hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \ --hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \ --hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \ --hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \ --hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \ --hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \ --hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \ --hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \ --hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \ --hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \ --hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \ --hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \ --hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \ --hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \ --hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \ --hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \ --hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \ --hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \ --hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \ --hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \ --hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \ --hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \ --hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \ --hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \ --hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \ --hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \ --hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \ --hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \ --hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \ --hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \ --hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \ --hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \ --hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \ --hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \ --hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \ --hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \ --hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \ --hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \ --hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \ --hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \ --hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \ --hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \ --hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b cryptography==45.0.3 \ --hash=sha256:00094838ecc7c6594171e8c8a9166124c1197b074cfca23645cee573910d76bc \ --hash=sha256:050ce5209d5072472971e6efbfc8ec5a8f9a841de5a4db0ebd9c2e392cb81972 \ --hash=sha256:232954730c362638544758a8160c4ee1b832dc011d2c41a306ad8f7cccc5bb0b \ --hash=sha256:25286aacb947286620a31f78f2ed1a32cded7be5d8b729ba3fb2c988457639e4 \ --hash=sha256:2f8f8f0b73b885ddd7f3d8c2b2234a7d3ba49002b0223f58cfde1bedd9563c56 \ --hash=sha256:38deed72285c7ed699864f964a3f4cf11ab3fb38e8d39cfcd96710cd2b5bb716 \ --hash=sha256:3ad69eeb92a9de9421e1f6685e85a10fbcfb75c833b42cc9bc2ba9fb00da4710 \ --hash=sha256:5555365a50efe1f486eed6ac7062c33b97ccef409f5970a0b6f205a7cfab59c8 \ --hash=sha256:555e5e2d3a53b4fabeca32835878b2818b3f23966a4efb0d566689777c5a12c8 \ --hash=sha256:57a6500d459e8035e813bd8b51b671977fb149a8c95ed814989da682314d0782 \ --hash=sha256:5833bb4355cb377ebd880457663a972cd044e7f49585aee39245c0d592904578 \ --hash=sha256:71320fbefd05454ef2d457c481ba9a5b0e540f3753354fff6f780927c25d19b0 \ --hash=sha256:7573d9eebaeceeb55285205dbbb8753ac1e962af3d9640791d12b36864065e71 \ --hash=sha256:92d5f428c1a0439b2040435a1d6bc1b26ebf0af88b093c3628913dd464d13fa1 \ --hash=sha256:97787952246a77d77934d41b62fb1b6f3581d83f71b44796a4158d93b8f5c490 \ --hash=sha256:9bb5bf55dcb69f7067d80354d0a348368da907345a2c448b0babc4215ccd3497 \ --hash=sha256:9cc80ce69032ffa528b5e16d217fa4d8d4bb7d6ba8659c1b4d74a1b0f4235fca \ --hash=sha256:9e4253ed8f5948a3589b3caee7ad9a5bf218ffd16869c516535325fece163dcc \ --hash=sha256:9eda14f049d7f09c2e8fb411dda17dd6b16a3c76a1de5e249188a32aeb92de19 \ --hash=sha256:a2b56de3417fd5f48773ad8e91abaa700b678dc7fe1e0c757e1ae340779acf7b \ --hash=sha256:af3f92b1dc25621f5fad065288a44ac790c5798e986a34d393ab27d2b27fcff9 \ --hash=sha256:c5edcb90da1843df85292ef3a313513766a78fbbb83f584a5a58fb001a5a9d57 \ --hash=sha256:c824c9281cb628015bfc3c59335163d4ca0540d49de4582d6c2637312907e4b1 \ --hash=sha256:c92519d242703b675ccefd0f0562eb45e74d438e001f8ab52d628e885751fb06 \ --hash=sha256:ca932e11218bcc9ef812aa497cdf669484870ecbcf2d99b765d6c27a86000942 \ --hash=sha256:cb6ab89421bc90e0422aca911c69044c2912fc3debb19bb3c1bfe28ee3dff6ab \ --hash=sha256:cfd84777b4b6684955ce86156cfb5e08d75e80dc2585e10d69e47f014f0a5342 \ --hash=sha256:d377dde61c5d67eb4311eace661c3efda46c62113ff56bf05e2d679e02aebb5b \ --hash=sha256:d54ae41e6bd70ea23707843021c778f151ca258081586f0cfa31d936ae43d1b2 \ --hash=sha256:dc10ec1e9f21f33420cc05214989544727e776286c1c16697178978327b95c9c \ --hash=sha256:ec21313dd335c51d7877baf2972569f40a4291b76a0ce51391523ae358d05899 \ --hash=sha256:ec64ee375b5aaa354b2b273c921144a660a511f9df8785e6d1c942967106438e \ --hash=sha256:ed43d396f42028c1f47b5fec012e9e12631266e3825e95c00e3cf94d472dac49 \ --hash=sha256:edd6d51869beb7f0d472e902ef231a9b7689508e83880ea16ca3311a00bf5ce7 \ --hash=sha256:f22af3c78abfbc7cbcdf2c55d23c3e022e1a462ee2481011d518c7fb9c9f3d65 \ --hash=sha256:fae1e637f527750811588e4582988932c222f8251f7b7ea93739acb624e1487f \ --hash=sha256:fed5aaca1750e46db870874c9c273cd5182a9e9deb16f06f7bdffdb5c2bde4b9 pip==25.1.1 \ --hash=sha256:2913a38a2abf4ea6b64ab507bd9e967f3b53dc1ede74b01b0931e1ce548751af \ --hash=sha256:3de45d411d308d5054c2168185d8da7f9a2cd753dbac8acbfa88a8909ecd9077 pycparser==2.22 \ --hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \ --hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc PyQt6==6.9.0 \ --hash=sha256:0c8b7251608e05b479cfe731f95857e853067459f7cbbcfe90f89de1bcf04280 \ --hash=sha256:1cbc5a282454cf19691be09eadbde019783f1ae0523e269b211b0173b67373f6 \ --hash=sha256:5344240747e81bde1a4e0e98d4e6e2d96ad56a985d8f36b69cd529c1ca9ff760 \ --hash=sha256:6a8ff8e3cd18311bb7d937f7d741e787040ae7ff47ce751c28a94c5cddc1b4e6 \ --hash=sha256:d36482000f0cd7ce84a35863766f88a5e671233d5f1024656b600cd8915b3752 \ --hash=sha256:e344868228c71fc89a0edeb325497df4ff731a89cfa5fe57a9a4e9baecc9512b PyQt6-Qt6==6.9.0 \ --hash=sha256:1188f118d1c570d27fba39707e3d8a48525f979816e73de0da55b9e6fa9ad0a1 \ --hash=sha256:6d3875119dec6bf5f799facea362aa0ad39bb23aa9654112faa92477abccb5ff \ --hash=sha256:9c0e603c934e4f130c110190fbf2c482ff1221a58317266570678bc02db6b152 \ --hash=sha256:b1c4e4a78f0f22fbf88556e3d07c99e5ce93032feae5c1e575958d914612e0f9 \ --hash=sha256:c825a6f5a9875ef04ef6681eda16aa3a9e9ad71847aa78dfafcf388c8007aa0a \ --hash=sha256:cf840e8ae20a0704e0343810cf0e485552db28bf09ea976e58ec0e9b7bb27fcd PyQt6-sip==13.10.2 \ --hash=sha256:061d4a2eb60a603d8be7db6c7f27eb29d9cea97a09aa4533edc1662091ce4f03 \ --hash=sha256:07f77e89d93747dda71b60c3490b00d754451729fbcbcec840e42084bf061655 \ --hash=sha256:0b097eb58b4df936c4a2a88a2f367c8bb5c20ff049a45a7917ad75d698e3b277 \ --hash=sha256:1a6c2f168773af9e6c7ef5e52907f16297d4efd346e4c958eda54ea9135be18e \ --hash=sha256:37af463dcce39285e686d49523d376994d8a2508b9acccb7616c4b117c9c4ed7 \ --hash=sha256:38b5823dca93377f8a4efac3cbfaa1d20229aa5b640c31cf6ebbe5c586333808 \ --hash=sha256:3dde8024d055f496eba7d44061c5a1ba4eb72fc95e5a9d7a0dbc908317e0888b \ --hash=sha256:45ac06f0380b7aa4fcffd89f9e8c00d1b575dc700c603446a9774fda2dcfc0de \ --hash=sha256:464ad156bf526500ce6bd05cac7a82280af6309974d816739b4a9a627156fafe \ --hash=sha256:4ccf197f8fa410e076936bee28ad9abadb450931d5be5625446fd20e0d8b27a6 \ --hash=sha256:4ffa71ddff6ef031d52cd4f88b8bba08b3516313c023c7e5825cf4a0ba598712 \ --hash=sha256:5506b9a795098df3b023cc7d0a37f93d3224a9c040c43804d4bc06e0b2b742b0 \ --hash=sha256:8132ec1cbbecc69d23dcff23916ec07218f1a9bbbc243bf6f1df967117ce303e \ --hash=sha256:83e6a56d3e715f748557460600ec342cbd77af89ec89c4f2a68b185fa14ea46c \ --hash=sha256:8b5d06a0eac36038fa8734657d99b5fe92263ae7a0cd0a67be6acfe220a063e1 \ --hash=sha256:9c67ed66e21b11e04ffabe0d93bc21df22e0a5d7e2e10ebc8c1d77d2f5042991 \ --hash=sha256:ad376a6078da37b049fdf9d6637d71b52727e65c4496a80b753ddc8d27526aca \ --hash=sha256:b1d3cc9015a1bd8c8d3e86a009591e897d4d46b0c514aede7d2970a2208749cd \ --hash=sha256:c7b34a495b92790c70eae690d9e816b53d3b625b45eeed6ae2c0fe24075a237e \ --hash=sha256:c80cc059d772c632f5319632f183e7578cd0976b9498682833035b18a3483e92 \ --hash=sha256:cc6a1dfdf324efaac6e7b890a608385205e652845c62130de919fd73a6326244 \ --hash=sha256:ddd578a8d975bfb5fef83751829bf09a97a1355fa1de098e4fb4d1b74ee872fc \ --hash=sha256:e455a181d45a28ee8d18d42243d4f470d269e6ccdee60f2546e6e71218e05bb4 \ --hash=sha256:e907394795e61f1174134465c889177f584336a98d7a10beade2437bf5942244 setuptools==80.9.0 \ --hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \ --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c wheel==0.45.1 \ --hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 \ --hash=sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248 ================================================ FILE: contrib/deterministic-build/requirements-build-android.txt ================================================ appdirs==1.4.4 \ --hash=sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41 colorama==0.4.5 \ --hash=sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4 Cython==0.29.37 \ --hash=sha256:f813d4a6dd94adee5d4ff266191d1d95bf6d4164a4facc535422c021b2504cfb Jinja2==3.1.6 \ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d MarkupSafe==3.0.2 \ --hash=sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0 pep517==0.13.1 \ --hash=sha256:1b2fa2ffd3938bb4beffe5d6146cbcb2bda996a5a4da9f31abffd8b24e07b317 pexpect==4.9.0 \ --hash=sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f pip==25.1.1 \ --hash=sha256:3de45d411d308d5054c2168185d8da7f9a2cd753dbac8acbfa88a8909ecd9077 ptyprocess==0.7.0 \ --hash=sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220 setuptools==80.9.0 \ --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c sh==2.2.2 \ --hash=sha256:653227a7c41a284ec5302173fbc044ee817c7bad5e6e4d8d55741b9aeb9eb65b toml==0.10.2 \ --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f tomli==2.2.1 \ --hash=sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff typing-extensions==4.13.2 \ --hash=sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef wheel==0.45.1 \ --hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 ================================================ FILE: contrib/deterministic-build/requirements-build-appimage.txt ================================================ Cython==3.1.1 \ --hash=sha256:505ccd413669d5132a53834d792c707974248088c4f60c497deb1b416e366397 pip==25.1.1 \ --hash=sha256:3de45d411d308d5054c2168185d8da7f9a2cd753dbac8acbfa88a8909ecd9077 setuptools==80.9.0 \ --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c wheel==0.45.1 \ --hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 ================================================ FILE: contrib/deterministic-build/requirements-build-base.txt ================================================ expandvars==1.0.0 \ --hash=sha256:f04070b8260264185f81142cd85e5df9ceef7229e836c5844302c4ccfa00c30d \ --hash=sha256:ff1690eceb90bbdeefd1e4b15f4d217f22a3e66f776c2cb060635d2dde4a7689 flit-core==3.12.0 \ --hash=sha256:18f63100d6f94385c6ed57a72073443e1a71a4acb4339491615d0f16d6ff01b2 \ --hash=sha256:e7a0304069ea895172e3c7bb703292e992c5d1555dd1233ab7b5621b5b69e62c packaging==25.0 \ --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f pip==25.1.1 \ --hash=sha256:2913a38a2abf4ea6b64ab507bd9e967f3b53dc1ede74b01b0931e1ce548751af \ --hash=sha256:3de45d411d308d5054c2168185d8da7f9a2cd753dbac8acbfa88a8909ecd9077 poetry-core==2.1.3 \ --hash=sha256:0522a015477ed622c89aad56a477a57813cace0c8e7ff2a2906b7ef4a2e296a4 \ --hash=sha256:2c704f05016698a54ca1d327f46ce2426d72eaca6ff614132c8477c292266771 setuptools==80.9.0 \ --hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \ --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c setuptools-scm==8.3.1 \ --hash=sha256:332ca0d43791b818b841213e76b1971b7711a960761c5bea5fc5cdb5196fbce3 \ --hash=sha256:3d555e92b75dacd037d32bafdf94f97af51ea29ae8c7b234cf94b7a5bd242a63 tomli==2.0.2 \ --hash=sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38 \ --hash=sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed wheel==0.45.1 \ --hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 \ --hash=sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248 ================================================ FILE: contrib/deterministic-build/requirements-build-mac.txt ================================================ altgraph==0.17.4 \ --hash=sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406 Cython==3.1.1 \ --hash=sha256:505ccd413669d5132a53834d792c707974248088c4f60c497deb1b416e366397 macholib==1.16.3 \ --hash=sha256:07ae9e15e8e4cd9a788013d81f5908b3609aa76f9b1421bae9c4d7606ec86a30 packaging==25.0 \ --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f pip==25.1.1 \ --hash=sha256:3de45d411d308d5054c2168185d8da7f9a2cd753dbac8acbfa88a8909ecd9077 pyinstaller-hooks-contrib==2025.4 \ --hash=sha256:5ce1afd1997b03e70f546207031cfdf2782030aabacc102190677059e2856446 setuptools==80.9.0 \ --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c wheel==0.45.1 \ --hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 ================================================ FILE: contrib/deterministic-build/requirements-build-wine.txt ================================================ altgraph==0.17.4 \ --hash=sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406 packaging==25.0 \ --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f pefile==2023.2.7 \ --hash=sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc pip==25.1.1 \ --hash=sha256:3de45d411d308d5054c2168185d8da7f9a2cd753dbac8acbfa88a8909ecd9077 pyinstaller-hooks-contrib==2025.4 \ --hash=sha256:5ce1afd1997b03e70f546207031cfdf2782030aabacc102190677059e2856446 pywin32-ctypes==0.2.3 \ --hash=sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755 setuptools==80.9.0 \ --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c wheel==0.45.1 \ --hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 ================================================ FILE: contrib/deterministic-build/requirements-hw.txt ================================================ base58==2.1.1 \ --hash=sha256:11a36f4d3ce51dfc1043f3218591ac4eb1ceb172919cebe05b52a5bcc8d245c2 \ --hash=sha256:c5d0cb3f5b6e81e8e35da5754388ddcc6d0d14b6c6a132cb93d69ed580a7278c bitbox02==7.0.0 \ --hash=sha256:27d5105eb15a553719fa9d3e68921c864b00c861b3a644044d9ac68426f18447 \ --hash=sha256:4b5b8422b94390b09962a4a93f4a9861429c093eb0f0b6c2d7661bbc1dd0e242 cbor2==5.6.5 \ --hash=sha256:3038523b8fc7de312bb9cdcbbbd599987e64307c4db357cd2030c472a6c7d468 \ --hash=sha256:34cf5ab0dc310c3d0196caa6ae062dc09f6c242e2544bea01691fe60c0230596 \ --hash=sha256:37096663a5a1c46a776aea44906cbe5fa3952f29f50f349179c00525d321c862 \ --hash=sha256:38886c41bebcd7dca57739439455bce759f1e4c551b511f618b8e9c1295b431b \ --hash=sha256:3d1a18b3a58dcd9b40ab55c726160d4a6b74868f2a35b71f9e726268b46dc6a2 \ --hash=sha256:4586a4f65546243096e56a3f18f29d60752ee9204722377021b3119a03ed99ff \ --hash=sha256:47261f54a024839ec649b950013c4de5b5f521afe592a2688eebbe22430df1dc \ --hash=sha256:54c72a3207bb2d4480c2c39dad12d7971ce0853a99e3f9b8d559ce6eac84f66f \ --hash=sha256:559dcf0d897260a9e95e7b43556a62253e84550b77147a1ad4d2c389a2a30192 \ --hash=sha256:5b856fda4c50c5bc73ed3664e64211fa4f015970ed7a15a4d6361bd48462feaf \ --hash=sha256:5ce13a27ef8fddf643fc17a753fe34aa72b251d03c23da6a560c005dc171085b \ --hash=sha256:5cff06464b8f4ca6eb9abcba67bda8f8334a058abc01005c8e616728c387ad32 \ --hash=sha256:61ceb77e6aa25c11c814d4fe8ec9e3bac0094a1f5bd8a2a8c95694596ea01e08 \ --hash=sha256:66dd25dd919cddb0b36f97f9ccfa51947882f064729e65e6bef17c28535dc459 \ --hash=sha256:6797b824b26a30794f2b169c0575301ca9b74ae99064e71d16e6ba0c9057de51 \ --hash=sha256:6e14a1bf6269d25e02ef1d4008e0ce8880aa271d7c6b4c329dba48645764f60e \ --hash=sha256:73b9647eed1493097db6aad61e03d8f1252080ee041a1755de18000dd2c05f37 \ --hash=sha256:7488aec919f8408f9987a3a32760bd385d8628b23a35477917aa3923ff6ad45f \ --hash=sha256:7f6d69f38f7d788b04c09ef2b06747536624b452b3c8b371ab78ad43b0296fab \ --hash=sha256:824f202b556fc204e2e9a67d6d6d624e150fbd791278ccfee24e68caec578afd \ --hash=sha256:863e0983989d56d5071270790e7ed8ddbda88c9e5288efdb759aba2efee670bc \ --hash=sha256:87026fc838370d69f23ed8572939bd71cea2b3f6c8f8bb8283f573374b4d7f33 \ --hash=sha256:8f747b7a9aaa58881a0c5b4cd4a9b8fb27eca984ed261a769b61de1f6b5bd1e6 \ --hash=sha256:90bfa36944caccec963e6ab7e01e64e31cc6664535dc06e6295ee3937c999cbb \ --hash=sha256:93676af02bd9a0b4a62c17c5b20f8e9c37b5019b1a24db70a2ee6cb770423568 \ --hash=sha256:94885903105eec66d7efb55f4ce9884fdc5a4d51f3bd75b6fedc68c5c251511b \ --hash=sha256:97a7e409b864fecf68b2ace8978eb5df1738799a333ec3ea2b9597bfcdd6d7d2 \ --hash=sha256:a34ee99e86b17444ecbe96d54d909dd1a20e2da9f814ae91b8b71cf1ee2a95e4 \ --hash=sha256:a3ac50485cf67dfaab170a3e7b527630e93cb0a6af8cdaa403054215dff93adf \ --hash=sha256:a83b76367d1c3e69facbcb8cdf65ed6948678e72f433137b41d27458aa2a40cb \ --hash=sha256:a88f029522aec5425fc2f941b3df90da7688b6756bd3f0472ab886d21208acbd \ --hash=sha256:a8947c102cac79d049eadbd5e2ffb8189952890df7cbc3ee262bbc2f95b011a9 \ --hash=sha256:ae2b49226224e92851c333b91d83292ec62eba53a19c68a79890ce35f1230d70 \ --hash=sha256:b682820677ee1dbba45f7da11898d2720f92e06be36acec290867d5ebf3d7e09 \ --hash=sha256:b9d15b638539b68aa5d5eacc56099b4543a38b2d2c896055dccf7e83d24b7955 \ --hash=sha256:e16c4a87fc999b4926f5c8f6c696b0d251b4745bc40f6c5aee51d69b30b15ca2 \ --hash=sha256:e25c2aebc9db99af7190e2261168cdde8ed3d639ca06868e4f477cf3a228a8e9 \ --hash=sha256:f0d0a9c5aabd48ecb17acf56004a7542a0b8d8212be52f3102b8218284bd881e \ --hash=sha256:f2764804ffb6553283fc4afb10a280715905a4cea4d6dc7c90d3e89c4a93bc8d \ --hash=sha256:f4c7dbcdc59ea7f5a745d3e30ee5e6b6ff5ce7ac244aa3de6786391b10027bb3 \ --hash=sha256:f91e6d74fa6917df31f8757fdd0e154203b0dd0609ec53eb957016a2b474896a \ --hash=sha256:fa61a02995f3a996c03884cf1a0b5733f88cbfd7fa0e34944bf678d4227ee712 \ --hash=sha256:fde21ac1cf29336a31615a2c469a9cb03cf0add3ae480672d4d38cda467d07fc \ --hash=sha256:fe11c2eb518c882cfbeed456e7a552e544893c17db66fe5d3230dbeaca6b615c certifi==2025.4.26 \ --hash=sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6 \ --hash=sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3 cffi==1.17.1 \ --hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \ --hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \ --hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \ --hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \ --hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \ --hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \ --hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \ --hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \ --hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \ --hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \ --hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \ --hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \ --hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \ --hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \ --hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \ --hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \ --hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \ --hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \ --hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \ --hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \ --hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \ --hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \ --hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \ --hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \ --hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \ --hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \ --hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \ --hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \ --hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \ --hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \ --hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \ --hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \ --hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \ --hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \ --hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \ --hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \ --hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \ --hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \ --hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \ --hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \ --hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \ --hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \ --hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \ --hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \ --hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \ --hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \ --hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \ --hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \ --hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \ --hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \ --hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \ --hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \ --hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \ --hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \ --hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \ --hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \ --hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \ --hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \ --hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \ --hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \ --hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \ --hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \ --hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \ --hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \ --hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \ --hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \ --hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b charset-normalizer==3.4.2 \ --hash=sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4 \ --hash=sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45 \ --hash=sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7 \ --hash=sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0 \ --hash=sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7 \ --hash=sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d \ --hash=sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d \ --hash=sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0 \ --hash=sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184 \ --hash=sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db \ --hash=sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b \ --hash=sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64 \ --hash=sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b \ --hash=sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8 \ --hash=sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff \ --hash=sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344 \ --hash=sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58 \ --hash=sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e \ --hash=sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471 \ --hash=sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148 \ --hash=sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a \ --hash=sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836 \ --hash=sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e \ --hash=sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63 \ --hash=sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c \ --hash=sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1 \ --hash=sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01 \ --hash=sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366 \ --hash=sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58 \ --hash=sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5 \ --hash=sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c \ --hash=sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2 \ --hash=sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a \ --hash=sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597 \ --hash=sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b \ --hash=sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5 \ --hash=sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb \ --hash=sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f \ --hash=sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0 \ --hash=sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941 \ --hash=sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0 \ --hash=sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86 \ --hash=sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7 \ --hash=sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7 \ --hash=sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455 \ --hash=sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6 \ --hash=sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4 \ --hash=sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0 \ --hash=sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3 \ --hash=sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1 \ --hash=sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6 \ --hash=sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981 \ --hash=sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c \ --hash=sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980 \ --hash=sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645 \ --hash=sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7 \ --hash=sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12 \ --hash=sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa \ --hash=sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd \ --hash=sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef \ --hash=sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f \ --hash=sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2 \ --hash=sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d \ --hash=sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5 \ --hash=sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02 \ --hash=sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3 \ --hash=sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd \ --hash=sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e \ --hash=sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214 \ --hash=sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd \ --hash=sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a \ --hash=sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c \ --hash=sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681 \ --hash=sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba \ --hash=sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f \ --hash=sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a \ --hash=sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28 \ --hash=sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691 \ --hash=sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82 \ --hash=sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a \ --hash=sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027 \ --hash=sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7 \ --hash=sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518 \ --hash=sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf \ --hash=sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b \ --hash=sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9 \ --hash=sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544 \ --hash=sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da \ --hash=sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509 \ --hash=sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f \ --hash=sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a \ --hash=sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f ckcc-protocol==1.4.0 \ --hash=sha256:c5fcc4705b4b78ec515b39549642570a660142407fa684c278cb0aea8122defa \ --hash=sha256:cd93d4d3e3308ea4580aa6be5b4613a8266fd96b0cc1af51e7168def27bbece5 click==8.1.8 \ --hash=sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2 \ --hash=sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a construct==2.10.70 \ --hash=sha256:4d2472f9684731e58cc9c56c463be63baa1447d674e0d66aeb5627b22f512c29 \ --hash=sha256:c80be81ef595a1a821ec69dc16099550ed22197615f4320b57cc9ce2a672cb30 construct-classes==0.1.2 \ --hash=sha256:72ac1abbae5bddb4918688713f991f5a7fb6c9b593646a82f4bf3ac53de7eeb5 \ --hash=sha256:e82437261790758bda41e45fb3d5622b54cfbf044ceb14774af68346faf5e08e cryptography==45.0.3 \ --hash=sha256:00094838ecc7c6594171e8c8a9166124c1197b074cfca23645cee573910d76bc \ --hash=sha256:050ce5209d5072472971e6efbfc8ec5a8f9a841de5a4db0ebd9c2e392cb81972 \ --hash=sha256:232954730c362638544758a8160c4ee1b832dc011d2c41a306ad8f7cccc5bb0b \ --hash=sha256:25286aacb947286620a31f78f2ed1a32cded7be5d8b729ba3fb2c988457639e4 \ --hash=sha256:2f8f8f0b73b885ddd7f3d8c2b2234a7d3ba49002b0223f58cfde1bedd9563c56 \ --hash=sha256:38deed72285c7ed699864f964a3f4cf11ab3fb38e8d39cfcd96710cd2b5bb716 \ --hash=sha256:3ad69eeb92a9de9421e1f6685e85a10fbcfb75c833b42cc9bc2ba9fb00da4710 \ --hash=sha256:5555365a50efe1f486eed6ac7062c33b97ccef409f5970a0b6f205a7cfab59c8 \ --hash=sha256:555e5e2d3a53b4fabeca32835878b2818b3f23966a4efb0d566689777c5a12c8 \ --hash=sha256:57a6500d459e8035e813bd8b51b671977fb149a8c95ed814989da682314d0782 \ --hash=sha256:5833bb4355cb377ebd880457663a972cd044e7f49585aee39245c0d592904578 \ --hash=sha256:71320fbefd05454ef2d457c481ba9a5b0e540f3753354fff6f780927c25d19b0 \ --hash=sha256:7573d9eebaeceeb55285205dbbb8753ac1e962af3d9640791d12b36864065e71 \ --hash=sha256:92d5f428c1a0439b2040435a1d6bc1b26ebf0af88b093c3628913dd464d13fa1 \ --hash=sha256:97787952246a77d77934d41b62fb1b6f3581d83f71b44796a4158d93b8f5c490 \ --hash=sha256:9bb5bf55dcb69f7067d80354d0a348368da907345a2c448b0babc4215ccd3497 \ --hash=sha256:9cc80ce69032ffa528b5e16d217fa4d8d4bb7d6ba8659c1b4d74a1b0f4235fca \ --hash=sha256:9e4253ed8f5948a3589b3caee7ad9a5bf218ffd16869c516535325fece163dcc \ --hash=sha256:9eda14f049d7f09c2e8fb411dda17dd6b16a3c76a1de5e249188a32aeb92de19 \ --hash=sha256:a2b56de3417fd5f48773ad8e91abaa700b678dc7fe1e0c757e1ae340779acf7b \ --hash=sha256:af3f92b1dc25621f5fad065288a44ac790c5798e986a34d393ab27d2b27fcff9 \ --hash=sha256:c5edcb90da1843df85292ef3a313513766a78fbbb83f584a5a58fb001a5a9d57 \ --hash=sha256:c824c9281cb628015bfc3c59335163d4ca0540d49de4582d6c2637312907e4b1 \ --hash=sha256:c92519d242703b675ccefd0f0562eb45e74d438e001f8ab52d628e885751fb06 \ --hash=sha256:ca932e11218bcc9ef812aa497cdf669484870ecbcf2d99b765d6c27a86000942 \ --hash=sha256:cb6ab89421bc90e0422aca911c69044c2912fc3debb19bb3c1bfe28ee3dff6ab \ --hash=sha256:cfd84777b4b6684955ce86156cfb5e08d75e80dc2585e10d69e47f014f0a5342 \ --hash=sha256:d377dde61c5d67eb4311eace661c3efda46c62113ff56bf05e2d679e02aebb5b \ --hash=sha256:d54ae41e6bd70ea23707843021c778f151ca258081586f0cfa31d936ae43d1b2 \ --hash=sha256:dc10ec1e9f21f33420cc05214989544727e776286c1c16697178978327b95c9c \ --hash=sha256:ec21313dd335c51d7877baf2972569f40a4291b76a0ce51391523ae358d05899 \ --hash=sha256:ec64ee375b5aaa354b2b273c921144a660a511f9df8785e6d1c942967106438e \ --hash=sha256:ed43d396f42028c1f47b5fec012e9e12631266e3825e95c00e3cf94d472dac49 \ --hash=sha256:edd6d51869beb7f0d472e902ef231a9b7689508e83880ea16ca3311a00bf5ce7 \ --hash=sha256:f22af3c78abfbc7cbcdf2c55d23c3e022e1a462ee2481011d518c7fb9c9f3d65 \ --hash=sha256:fae1e637f527750811588e4582988932c222f8251f7b7ea93739acb624e1487f \ --hash=sha256:fed5aaca1750e46db870874c9c273cd5182a9e9deb16f06f7bdffdb5c2bde4b9 ecdsa==0.19.1 \ --hash=sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3 \ --hash=sha256:478cba7b62555866fcb3bb3fe985e06decbdb68ef55713c4e5ab98c57d508e61 hidapi==0.14.0.post4 \ --hash=sha256:01747e681d138ec614321ef6f069e5be3743fa210112e529a34d3e99635e4ac0 \ --hash=sha256:04357092b39631d8034b17fd111c5583be2790ad7979ac1983173344d28824e7 \ --hash=sha256:0d51f8102a2441ce22e080576f8f370d25cb3962161818a89f236b0401840f18 \ --hash=sha256:10a01af155c51a8089fe44e627af2fbd323cfbef7bd55a86837d971aef6088b0 \ --hash=sha256:129d684c2760fafee9014ce63a58d8e2699cdf00cd1a11bb3d706d4715f5ff96 \ --hash=sha256:1304fdeb694f581c46e7b0d6aebc6adfa66219177f04cacddbec0bd906bd5b7c \ --hash=sha256:142374bb39c8973c6d04a2b8b767d64891741d05b09364b32531d9389c3a15bb \ --hash=sha256:1487312ad50cf2c08a5ea786167b3229afd6478c4b26974157c3845a84e91231 \ --hash=sha256:1591e98c0e6db4cc1e34e96295b4ea68eaf37d365d664570441388869e8e3618 \ --hash=sha256:1807ff8abe3c5dcfa9d8acd71b1ab9f0aeb69cdbb039ddcbb150ed9fbbfd1ba7 \ --hash=sha256:1bee0f731874d78367a3bf131cb0325578bc9fee0678ed00c4ca3ded45d11c20 \ --hash=sha256:20a466e4cf2230687d21f55ffffb1a2384a2262fc343e507dd01d1ab981f7573 \ --hash=sha256:21627bb8a0e2023da1dfb7cb7b970c30d6a86e6498721f1123d018b2f64b426f \ --hash=sha256:21ebd1420db116733536fae227f1cb30ad74bded5090269cdda4facfa73a8867 \ --hash=sha256:293b207e737df4577d27661c5135e7c16976f706d3739d7a53a169dde1aaebaa \ --hash=sha256:2acadb4f1ae569c4f73ddb461af8733e8f5efcb290c3d0ef1b0671ba793b0ae3 \ --hash=sha256:2d1c102f754b2085b270e7c29cb8a148ffb05e10325c373d05ac16e2cbce131c \ --hash=sha256:3253d198b193065d633cde3f9a59dabeeb1608ece26f0f319a151e8c7775d7ae \ --hash=sha256:348e68e3a2145a6ec6bebce13ffdf3e5883d8c720752c365027f16e16764def6 \ --hash=sha256:380a74e743afe7a0241e0efce73ce9697f41d4e2e0a030be5458a44f9119427a \ --hash=sha256:4169893fe5e368777fce7575a8bdedc1861f13d8fb9fda6b05e8155dde6eb7f1 \ --hash=sha256:41d532d5a358a63db4d7fc1e57ea107150445c90167b39ba6f8fb84597396a48 \ --hash=sha256:48fce253e526d17b663fbf9989c71c7ef7653ced5f4be65f1437c313fb3dbdf6 \ --hash=sha256:4939faf6382d1c89462e72aa08636bbfe97ecb5464a34b14997e0ca3e1f92906 \ --hash=sha256:4f04de00e40db2efc0bcdd047c160274ba7ccd861100fd87c295dd63cb932f2f \ --hash=sha256:56d7538a4e156041bb80f07f47c327f8944e39da469b010041ce44e324d0657c \ --hash=sha256:58a0a0c029886de8b301ce1ee2e7fd6914ae1ca49feb37cc9930c26baa683427 \ --hash=sha256:5a5af70dad759b45536a9946d8232ef7d90859845d3554c93bea3e790250df75 \ --hash=sha256:5c14c54cbfd45553cd3e6a23014f8e8f2d12c41cd2783e84c2cb774976d4648f \ --hash=sha256:60115947607b8b0a719420726a541bad68728ece38b20654e81fef77c9e0bd2f \ --hash=sha256:6270677da02e86b56b81afd5f6f313736b8315b493f3c8a431da285e3a3c5de9 \ --hash=sha256:6439fc9686518d0336fac8c5e370093279f53c997540065fce131c97567118d8 \ --hash=sha256:68d7e9ba5c48e50f322057b9f88d799c105b5d46c966981aa8e5047b6091541f \ --hash=sha256:6b424ec16068d58d13fb67c7fb728824a3888f8f7fb6ffa3c82d5a54d8b74b7f \ --hash=sha256:6e08884ee9e1e3963701c1cdf22edd17c7ff708728f163efc396964460b3f9b4 \ --hash=sha256:6eaff1d120c47e1a121eada8dc85eac007d1ed81f3db7fc0da5b6ed17d8edefb \ --hash=sha256:6f96ae777e906f0a9d6f75e873313145dfec2b774f558bfcae8ba34f09792460 \ --hash=sha256:707b1ebf5cb051b020e94b039e603351bf2e6620b48fc970228e0dd5d3a91fca \ --hash=sha256:74ae8ce339655b2568d74e49c8ef644d34a445dd0a9b4b89d1bf09447b83f5af \ --hash=sha256:7d099c259aadcab2bc3f4fb5a1db579ec886c2cade7533016f62778235150746 \ --hash=sha256:80fa94668d21b12daf62b034f647d71236470a8ba9a7580e220c47e9c119d932 \ --hash=sha256:87218eeba366c871adcc273407aacbabab781d6a964919712d5583eded5ca50f \ --hash=sha256:884fa003d899113e14908bd3b519c60b48fc3cec0410264dcbdad1c4a8fc2e8d \ --hash=sha256:8a2d466b995f8ff387d68c052d3b74ee981a4ddc4f1a99f32f2dc7022273dc11 \ --hash=sha256:8d924bd002a1c17ca51905b3b7b3d580e80ec211a9b8fe4667b73db0ff9e9b54 \ --hash=sha256:8de94caca7f2616e41466c0ccdf7a96f567914e9e85e89e0b607018777fc0755 \ --hash=sha256:8e20d0a1298a4bd342d7d927d928f1a5a29e5fc9dbf9a79e95dc6e2d386d5070 \ --hash=sha256:949f437f517e81bc567429f41fb1e67349046eb43e52d47b2852b5847de452ee \ --hash=sha256:97192b7756dd854cb2ebc8a1862ffa009cdc203e0399777764462cae3c459d58 \ --hash=sha256:9e4b462fc1f2b160442618448132aebadb71c06b6eb7654eae4385c490100a67 \ --hash=sha256:9f14ac2737fd6f58d88d2e6bf8ebd03aac7b486c14d3f570b7b1d0013d61b726 \ --hash=sha256:a18af6ebd751eea7ddfb093ddf7d0371b05ba0f9a2f8593c7255a34e6bd753ff \ --hash=sha256:a28de4a03fc276614518d8d0997d8152d0edaf8ca9166522316ef1c455e8bc29 \ --hash=sha256:a2c4c3b3d77b759a4a118aa8428da1daf21c01b49915f44d7a3f198bcee4aa7b \ --hash=sha256:a90cfdd29c10425cd4e4cff34adb12d25048561fc946f3562679e45721060a1c \ --hash=sha256:ac3e6e794a0fd6ee4634bf1beea1c3c91ab6faf8b16f3f672a42541f9c5ea41f \ --hash=sha256:b6b9c4dbf7d7e2635ff129ce6ea82174865c073b75888b8b97dda5a3d9a70493 \ --hash=sha256:bca568a2b7d0d454c7921d70b1cc44f427eb6f95961b6d7b3b9b4532d0de74ef \ --hash=sha256:c45a493dffdfe614a9943a8c7f0df617254f836f1242478f7780fbeafb18a131 \ --hash=sha256:c8f722864a03c1d243a9538f0872e233d07fc3fe1d945c66c0cb632060d6d009 \ --hash=sha256:cb1a2b5da0dcfab6837281342d1785cc373484bd3f27bd06fd2211d88075a7bd \ --hash=sha256:d8ab5ba9fce95e342335ef48640221a46600c1afb66847432fad9823d40a2022 \ --hash=sha256:da700db947562f8c0ac530215b74b5a27e4c669916ec99cfb5accd14ba08562c \ --hash=sha256:da777638f5ecf9ef6c979f6c793417f54104d56ac99a48312d6f7e47858c2dd8 \ --hash=sha256:e11d475429a1bc943ceac4ad8da4be63b240e00da5e10863fc3cbd9a35fdb51c \ --hash=sha256:e1f6409854c0a8ed4d1fdbe88d5ee4baf6f19996d1561f76889a132cb083574d \ --hash=sha256:e70eab52781e58e819730d99e3c825e92c15ec2138b6902ed078c8cd73317ce0 \ --hash=sha256:e749b79d9cafc1e9fd9d397d8039377c928ca10a36847fda6407169513802f68 \ --hash=sha256:e9af3c9191b7a4dade9152454001622519f4ecfa674b78929b739cfbf4b35d51 \ --hash=sha256:f0cc21e82e95cb92ef951df8eb8acf5626ac8fa14ab5292abdab1b2349970445 \ --hash=sha256:f27c74deda0282a97dd0f006fd79d6d08fdb16c7a3ba156d52fce85e48515b0a \ --hash=sha256:f3ce310d366335e1ac9416d8e4a27d6eef2ae896fbee0135484d39d001711bea \ --hash=sha256:f67e60eaa287e0fa35223f2d1f9afda81dd7312c7ba07e08fbdaf1af8a923530 \ --hash=sha256:f787b76288450f60250895597dabb080894f0ea09ad5df0433412fee42452435 \ --hash=sha256:fa66391be8acb358b381c30f32be5880d591a3358e531d980832d593dfe83d5a \ --hash=sha256:fbd2835ff193d0261e0de375fea006cb7cb18a30ae1657af48a43e381f6a0995 \ --hash=sha256:fedb9c3be6a2376de436d13fcb37a686a9b6bc988585bcc4f5ec61cad925e794 \ --hash=sha256:ff021ed0962f2d5d67405ae53c85f6cb3ab8c5af3dff7db8c74672f79f7a39d1 \ --hash=sha256:ff67139fbaa91eed55e7e916bdc1ccdaf8c909a80a9c480011caa65c4ba82a97 idna==3.10 \ --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 ledger-bitcoin==0.4.0 \ --hash=sha256:2242452e78cf4b57c8b8d3509e831860fd4851b0a1bfab95f2f5e3f47d4d1500 \ --hash=sha256:a33e78710671ec21e1003d0483406e955b48866ccf515fd8e7d8d81f4e1c1cf9 ledgercomm==1.2.1 \ --hash=sha256:015cfc05f16b8c59f8cc1d9fc0b8935923f1fcc3806d33eeb6b0e055b44f5a91 \ --hash=sha256:8ffef5703355b8ec7b73bca325f70288f4d0dafcb299c09833de9c197fb6dd34 libusb1==3.3.1 \ --hash=sha256:0ef69825173ce74af34444754c081cc324233edc6acc405658b3ad784833e076 \ --hash=sha256:3951d360f2daf0e0eacf839e15d2d1d2f4f5e7830231eb3188eeffef2dd17bad \ --hash=sha256:6e21b772d80d6487fbb55d3d2141218536db302da82f1983754e96c72781c102 \ --hash=sha256:808c9362299dcee01651aa87e71e9d681ccedb27fc4dbd70aaf14e245fb855f1 mnemonic==0.21 \ --hash=sha256:1fe496356820984f45559b1540c80ff10de448368929b9c60a2b55744cc88acf \ --hash=sha256:72dc9de16ec5ef47287237b9b6943da11647a03fe7cf1f139fc3d7c4a7439288 noiseprotocol==0.3.1 \ --hash=sha256:2e1a603a38439636cf0ffd8b3e8b12cee27d368a28b41be7dbe568b2abb23111 \ --hash=sha256:b092a871b60f6a8f07f17950dc9f7098c8fe7d715b049bd4c24ee3752b90d645 packaging==25.0 \ --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f pip==25.1.1 \ --hash=sha256:2913a38a2abf4ea6b64ab507bd9e967f3b53dc1ede74b01b0931e1ce548751af \ --hash=sha256:3de45d411d308d5054c2168185d8da7f9a2cd753dbac8acbfa88a8909ecd9077 protobuf==3.20.3 \ --hash=sha256:03038ac1cfbc41aa21f6afcbcd357281d7521b4157926f30ebecc8d4ea59dcb7 \ --hash=sha256:28545383d61f55b57cf4df63eebd9827754fd2dc25f80c5253f9184235db242c \ --hash=sha256:2e3427429c9cffebf259491be0af70189607f365c2f41c7c3764af6f337105f2 \ --hash=sha256:398a9e0c3eaceb34ec1aee71894ca3299605fa8e761544934378bbc6c97de23b \ --hash=sha256:44246bab5dd4b7fbd3c0c80b6f16686808fab0e4aca819ade6e8d294a29c7050 \ --hash=sha256:447d43819997825d4e71bf5769d869b968ce96848b6479397e29fc24c4a5dfe9 \ --hash=sha256:67a3598f0a2dcbc58d02dd1928544e7d88f764b47d4a286202913f0b2801c2e7 \ --hash=sha256:74480f79a023f90dc6e18febbf7b8bac7508420f2006fabd512013c0c238f454 \ --hash=sha256:819559cafa1a373b7096a482b504ae8a857c89593cf3a25af743ac9ecbd23480 \ --hash=sha256:899dc660cd599d7352d6f10d83c95df430a38b410c1b66b407a6b29265d66469 \ --hash=sha256:8c0c984a1b8fef4086329ff8dd19ac77576b384079247c770f29cc8ce3afa06c \ --hash=sha256:9aae4406ea63d825636cc11ffb34ad3379335803216ee3a856787bcf5ccc751e \ --hash=sha256:a7ca6d488aa8ff7f329d4c545b2dbad8ac31464f1d8b1c87ad1346717731e4db \ --hash=sha256:b6cc7ba72a8850621bfec987cb72623e703b7fe2b9127a161ce61e61558ad905 \ --hash=sha256:bf01b5720be110540be4286e791db73f84a2b721072a3711efff6c324cdf074b \ --hash=sha256:c02ce36ec760252242a33967d51c289fd0e1c0e6e5cc9397e2279177716add86 \ --hash=sha256:d9e4432ff660d67d775c66ac42a67cf2453c27cb4d738fc22cb53b5d84c135d4 \ --hash=sha256:daa564862dd0d39c00f8086f88700fdbe8bc717e993a21e90711acfed02f2402 \ --hash=sha256:de78575669dddf6099a8a0f46a27e82a1783c557ccc38ee620ed8cc96d3be7d7 \ --hash=sha256:e64857f395505ebf3d2569935506ae0dfc4a15cb80dc25261176c784662cdcc4 \ --hash=sha256:f4bd856d702e5b0d96a00ec6b307b0f51c1982c2bf9c0052cf9019e9a544ba99 \ --hash=sha256:f4c42102bc82a51108e449cbb32b19b180022941c727bac0cfd50170341f16ee pyaes==1.6.1 \ --hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f pycparser==2.22 \ --hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \ --hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc pyserial==3.5 \ --hash=sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb \ --hash=sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0 requests==2.32.3 \ --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \ --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6 safet==0.1.5 \ --hash=sha256:a7fd4b68bb1bc6185298af665c8e8e00e2bb2bcbddbb22844ead929b845c635e \ --hash=sha256:f966a23243312f64d14c7dfe02e8f13f6eeba4c3f51341f2c11ae57831f07de3 semver==3.0.4 \ --hash=sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746 \ --hash=sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602 setuptools==80.9.0 \ --hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \ --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c shamir-mnemonic==0.3.0 \ --hash=sha256:188c6b5bd00d5e756e12e2b186c3cb7c98ff7ff44df608d4c1d2077f6b6e730f \ --hash=sha256:bc04886a1ddfe2a64d8a3ec51abf0f664d98d5b557cc7e78a8ad2d10a1d87438 six==1.17.0 \ --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 slip10==1.0.1 \ --hash=sha256:02b350ae557b591791428b17551f95d7ac57e9211f37debdc814c90b4a123a54 \ --hash=sha256:4aa764369db0a261e468160ec1afeeb2b22d26392dd118c49b9daa91f642947b trezor==0.13.10 \ --hash=sha256:7a0b6ae4628dd0c31a5ceb51258918d9bbdd3ad851388837225826b228ee504f \ --hash=sha256:7c85dc2c47998765c84d309fc753d2b116c943d447289157895488899c95706d typing-extensions==4.13.2 \ --hash=sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c \ --hash=sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef urllib3==1.26.20 \ --hash=sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e \ --hash=sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32 wheel==0.45.1 \ --hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 \ --hash=sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248 ================================================ FILE: contrib/deterministic-build/requirements.txt ================================================ aiohappyeyeballs==2.6.1 \ --hash=sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558 aiohttp==3.12.9 \ --hash=sha256:2c9914c8914ff40b68c6e4ed5da33e88d4e8f368fddd03ceb0eb3175905ca782 aiohttp-socks==0.10.1 \ --hash=sha256:49f2e1f8051f2885719beb1b77e312b5a27c3e4b60f0b045a388f194d995e068 aiorpcX==0.25.0 \ --hash=sha256:940fa250ea5e9fd372d4c6acdc20dcb603bd1960ca324759d29864a4aaf64570 aiosignal==1.3.2 \ --hash=sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54 async-timeout==5.0.1 \ --hash=sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3 attrs==22.2.0 \ --hash=sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99 certifi==2025.4.26 \ --hash=sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6 dnspython==2.4.2 \ --hash=sha256:8dcfae8c7460a2f84b4072e26f1c9f4101ca20c071649cb7c34e8b6a93d58984 electrum-aionostr==0.1.0 \ --hash=sha256:3774f8e8312388272e10851a869c9f4d3d4a54d8d564851c36e2dc40297bec84 electrum-ecc==0.0.7 \ --hash=sha256:ed4134e1dbff0fd83022764c6acc97a02cde3512927d7c41f4d48b9a06e91fb2 frozenlist==1.6.0 \ --hash=sha256:b99655c32c1c8e06d111e7f41c06c29a5318cb1835df23a45518e02a47c63b68 idna==3.10 \ --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 jsonpatch==1.33 \ --hash=sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c jsonpointer==3.0.0 \ --hash=sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef multidict==6.4.4 \ --hash=sha256:69ee9e6ba214b5245031b76233dd95408a0fd57fdb019ddcc1ead4790932a8e8 packaging==25.0 \ --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f pip==25.1.1 \ --hash=sha256:3de45d411d308d5054c2168185d8da7f9a2cd753dbac8acbfa88a8909ecd9077 propcache==0.3.1 \ --hash=sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf protobuf==3.20.3 \ --hash=sha256:2e3427429c9cffebf259491be0af70189607f365c2f41c7c3764af6f337105f2 python-socks==2.7.1 \ --hash=sha256:f1a0bb603830fe81e332442eada96757b8f8dec02bd22d1d6f5c99a79704c550 QDarkStyle==3.2.3 \ --hash=sha256:0c0b7f74a6e92121008992b369bab60468157db1c02cd30d64a5e9a3b402f1ae qrcode==8.2 \ --hash=sha256:35c3f2a4172b33136ab9f6b3ef1c00260dd2f66f858f24d88418a015f446506c QtPy==2.4.3 \ --hash=sha256:db744f7832e6d3da90568ba6ccbca3ee2b3b4a890c3d6fbbc63142f6e4cdf5bb setuptools==80.9.0 \ --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c typing-extensions==4.13.2 \ --hash=sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef wheel==0.45.1 \ --hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 yarl==1.20.0 \ --hash=sha256:686d51e51ee5dfe62dec86e4866ee0e9ed66df700d55c828a615640adc885307 ================================================ FILE: contrib/docker_notes.md ================================================ # Using the build scripts Most of our build scripts are docker-based. (All, except the macOS build, which is a separate beast and always has to be special-cased at the cost of significant maintenance burden...) Typically, the build flow is: - build a docker image, based on debian - the apt sources mirror used is `snapshot.debian.org` - (except for the source tarball build, which is simple enough not to need this) - this helps with historical reproducibility - note that `snapshot.debian.org` is often slow and sometimes keeps timing out :/ (see #8496) - a potential alternative would be `snapshot.notset.fr`, but that mirror is missing e.g. `binary-i386`, which is needed for the wine/windows build. - if you are just trying to build for yourself and don't need reproducibility, you can just switch back to the default debian apt sources mirror. - docker caches the build (locally), and so this step only needs to be rerun if we update the Dockerfile. This caching happens automatically and by default. - you can disable the caching by setting envvar `ELECBUILD_NOCACHE=1`. See below. - create a docker container from the image, and build the final binary inside the container ## Notes about using Docker - To install Docker: This assumes an Ubuntu (x86_64) host, but it should not be too hard to adapt to another similar system. ``` $ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - $ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" $ sudo apt-get update $ sudo apt-get install -y docker-ce ``` - To communicate with the docker daemon, the build scripts either need to be called via sudo, or the unix user on the host system (e.g. the user you run as) needs to be part of the `docker` group. i.e.: ``` $ sudo usermod -aG docker ${USER} ``` (and then reboot or similar for it to take effect) ## Environment variables - `ELECBUILD_COMMIT` When unset or empty, we build directly from the local git clone. These builds are *not* reproducible. When non-empty, it should be set to a git ref. We will create a fresh git clone checked out at that reference in `/tmp/electrum_build/`, and build there. - `ELECBUILD_NOCACHE=1` A non-empty value forces a rebuild of the docker image. Before we started using `snapshot.debian.org` for apt sources, setting this was necessary to properly test historical reproducibility. (we were version-pinning packages installed using `apt`, but it was not realistic to version-pin all transitive dependencies, and sometimes an update of those resulted in changes to our binary builds) I think setting this is no longer necessary for building reproducibly. ================================================ FILE: contrib/freeze_containers_distro.sh ================================================ #!/bin/sh # Run this after a new release to update pin for build container distro packages set -e DEBIAN_SNAPSHOT_BASE="https://snapshot.debian.org/archive/debian/" DEBIAN_APPIMAGE_DISTRO="bullseye" # should match build-linux/appimage Dockerfile base DEBIAN_WINE_DISTRO="trixie" # should match build-wine Dockerfile base DEBIAN_ANDROID_DISTRO="trixie" # should match android Dockerfile base contrib="$(dirname "$0")" if [ ! -x /bin/wget ]; then echo "no wget" exit 1 fi DEBIAN_SNAPSHOT_LATEST=$(wget -O- "${DEBIAN_SNAPSHOT_BASE}$(date +"?year=%Y&month=%m")" 2>/dev/null | grep "^/dev/null echo "Valid!" # build-linux echo "deb ${DEBIAN_SNAPSHOT} ${DEBIAN_APPIMAGE_DISTRO} main" > "$contrib/build-linux/appimage/apt.sources.list" echo "deb-src ${DEBIAN_SNAPSHOT} ${DEBIAN_APPIMAGE_DISTRO} main" >> "$contrib/build-linux/appimage/apt.sources.list" # build-wine echo "deb ${DEBIAN_SNAPSHOT} ${DEBIAN_WINE_DISTRO} main" > "$contrib/build-wine/apt.sources.list" echo "deb-src ${DEBIAN_SNAPSHOT} ${DEBIAN_WINE_DISTRO} main" >> "$contrib/build-wine/apt.sources.list" # android echo "deb ${DEBIAN_SNAPSHOT} ${DEBIAN_ANDROID_DISTRO} main" > "$contrib/android/apt.sources.list" echo "deb-src ${DEBIAN_SNAPSHOT} ${DEBIAN_ANDROID_DISTRO} main" >> "$contrib/android/apt.sources.list" echo "updated APT sources to ${DEBIAN_SNAPSHOT}" ================================================ FILE: contrib/freeze_packages.sh ================================================ #!/bin/bash # Run this after a new release to update dependencies set -e venv_dir=~/.electrum-venv contrib="$(dirname "$0")" # note: we should not use a higher version of python than what the binaries bundle if [[ ! "$SYSTEM_PYTHON" ]] ; then SYSTEM_PYTHON=$(which python3.10) || printf "" else SYSTEM_PYTHON=$(which "$SYSTEM_PYTHON") || printf "" fi if [[ ! "$SYSTEM_PYTHON" ]] ; then echo "Please specify which python to use in \$SYSTEM_PYTHON" && exit 1 fi "${SYSTEM_PYTHON}" -m hashin -h > /dev/null 2>&1 || { "${SYSTEM_PYTHON}" -m pip install hashin; } for suffix in '' '-hw' '-binaries' '-binaries-mac' '-build-wine' '-build-mac' '-build-base' '-build-appimage' '-build-android'; do reqfile="requirements${suffix}.txt" rm -rf "$venv_dir" "${SYSTEM_PYTHON}" -m venv "$venv_dir" source "$venv_dir/bin/activate" echo "Installing dependencies... (${reqfile})" # We pin all python packaging tools (pip and friends). Some of our dependencies might # pull some of them in (e.g. protobuf->setuptools), and all transitive dependencies # must be pinned, so we might as well pin all packaging tools. This however means # that we should explicitly install them now, so that we pin latest versions if possible. python -m pip install --upgrade pip setuptools wheel python -m pip install -r "$contrib/requirements/${reqfile}" --upgrade echo "OK." requirements=$(pip freeze --all) restricted=$(echo $requirements | ${SYSTEM_PYTHON} "$contrib/deterministic-build/find_restricted_dependencies.py") if [ ! -z "$restricted" ]; then python -m pip install $restricted requirements=$(pip freeze --all) fi echo "Generating package hashes... (${reqfile})" rm -f "$contrib/deterministic-build/${reqfile}" touch "$contrib/deterministic-build/${reqfile}" # restrict ourselves to source-only packages. # TODO expand this to all reqfiles... HASHIN_FLAGS="" if [[ "${suffix}" == "" || "${suffix}" == "-build-wine" || "${suffix}" == "-build-mac" || "${suffix}" == "-build-appimage" || "${suffix}" == "-build-android" || "0" == "1" ]] ; then HASHIN_FLAGS="--python-version source" fi echo -e "\r Hashing requirements for $reqfile..." ${SYSTEM_PYTHON} -m hashin $HASHIN_FLAGS -r "$contrib/deterministic-build/${reqfile}" $requirements echo "OK." done echo "Done. Updated requirements" ================================================ FILE: contrib/generate_payreqpb2.sh ================================================ #!/bin/bash # Generates the file paymentrequest_pb2.py set -e CONTRIB="$(dirname "$(readlink -e "$0")")" EL="$CONTRIB"/../electrum if ! which protoc > /dev/null 2>&1; then echo "Please install 'protoc'" echo "If you're on Debian, try 'sudo apt install protobuf-compiler'?" exit 1 fi protoc --proto_path="$EL" --python_out="$EL" "$EL"/paymentrequest.proto ================================================ FILE: contrib/locale/build_cleanlocale.sh ================================================ #!/bin/bash set -e CONTRIB_LOCALE="$(dirname "$(realpath "$0" 2> /dev/null || grealpath "$0")")" CONTRIB="$CONTRIB_LOCALE"/.. PROJECT_ROOT="$CONTRIB"/.. cd "$PROJECT_ROOT" git submodule update --init LOCALE="$PROJECT_ROOT/electrum/locale/" cd "$LOCALE" git clean -ffxd git reset --hard rm -rf llm_proofreader "$CONTRIB_LOCALE/build_locale.sh" "$LOCALE/locale" "$LOCALE/locale" ================================================ FILE: contrib/locale/build_locale.sh ================================================ #!/bin/bash # # This script converts human-readable (.po) locale files to compiled (.mo) locale files. set -e CONTRIB_LOCALE="$(dirname "$(realpath "$0" 2> /dev/null || grealpath "$0")")" if [[ ! -d "$1" || -z "$2" ]]; then echo "usage: $0 locale_source_dir locale_dest_dir" echo " The dirs can match, to build in place." # ^ note: these are the paths to the "inner" locale/ dir exit 1 fi # convert $1 and $2 to abs paths SRC_DIR="$(realpath "$1" 2> /dev/null || grealpath "$1")" DST_DIR="$(realpath "$2" 2> /dev/null || grealpath "$2")" if ! which msgfmt > /dev/null 2>&1; then echo "Please install gettext" exit 1 fi cd "$SRC_DIR" mkdir -p "$DST_DIR" for i in *; do dir="$DST_DIR/$i/LC_MESSAGES" mkdir -p "$dir" (msgfmt --output-file="$dir/electrum.mo" "$i/electrum.po" || true) done echo "running stats.py" "$CONTRIB_LOCALE/stats.py" ================================================ FILE: contrib/locale/push_locale.py ================================================ #!/usr/bin/env python3 # # This script extracts "raw" strings from the codebase, # and uploads them to crowdin, for the community to translate them. # # Dependencies: # $ sudo apt-get install python3-requests gettext qt6-l10n-tools import glob import os import subprocess import sys try: import requests except ImportError as e: sys.exit(f"Error: {str(e)}. Try 'python3 -m pip install --user '") # set cwd project_root = os.path.abspath(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) os.chdir(project_root) locale_dir = os.path.join(project_root, "electrum", "locale") if not os.path.exists(os.path.join(locale_dir, "locale")): raise Exception(f"missing git submodule for locale? {locale_dir}") # check dependencies are available try: subprocess.check_output(["xgettext", "--version"]) subprocess.check_output(["msgcat", "--version"]) except (subprocess.CalledProcessError, OSError) as e2: raise Exception("missing gettext. Maybe try 'apt install gettext'") QT_LUPDATE="lupdate" QT_LCONVERT="lconvert" try: subprocess.check_output([QT_LUPDATE, "-version"]) subprocess.check_output([QT_LCONVERT, "-h"]) except (subprocess.CalledProcessError, OSError) as e1: QT_LUPDATE="/usr/lib/qt6/bin/lupdate" # workaround qt5/qt6 confusion on ubuntu 22.04 QT_LCONVERT="/usr/lib/qt6/bin/lconvert" try: subprocess.check_output([QT_LUPDATE, "-version"]) subprocess.check_output([QT_LCONVERT, "-h"]) except (subprocess.CalledProcessError, OSError) as e2: raise Exception("missing Qt lupdate/convert tools. Maybe try 'apt install qt6-l10n-tools'") # create build dir build_dir = os.path.join(locale_dir, "build") if not os.path.exists(build_dir): os.mkdir(build_dir) # add .py files files_list = glob.glob("electrum/**/*.py", recursive=True) files_list = sorted(files_list) # makes output deterministic across CI runs with open(f"{build_dir}/app.fil", "w", encoding="utf-8") as f: for item in files_list: f.write(item + "\n") print("Found {} .py files to translate".format(len(files_list))) # Generate fresh translation template print('Generating template...') # note: do not use xgettext option "--sort-output", as that makes human translators have to context-switch all the time cmd = ["xgettext", "--from-code", "UTF-8", "--language", "Python", "--no-wrap", "-f", f"{build_dir}/app.fil", f"--output={build_dir}/messages_gettext.pot"] subprocess.check_output(cmd) # add QML translations files_list = glob.glob("electrum/gui/qml/**/*.qml", recursive=True) files_list = sorted(files_list) # makes output deterministic across CI runs with open(f"{build_dir}/qml.lst", "w", encoding="utf-8") as f: for item in files_list: f.write(item + "\n") print("Found {} QML files to translate".format(len(files_list))) # note: lupdate writes relative paths into its output .ts file, relative to the .ts file itself :/ cmd = [QT_LUPDATE, f"@{build_dir}/qml.lst","-ts", f"{build_dir}/qml.ts"] print('Collecting strings') subprocess.check_output(cmd) cmd = [QT_LCONVERT, "-of", "po", "-o", f"{build_dir}/messages_qml.pot", f"{build_dir}/qml.ts"] print('Convert to gettext') subprocess.check_output(cmd) print("Fixing some paths in messages_qml.pot") # sed from " ../../gui/qml/" # to " electrum/gui/qml/" cmd = ["sed", "-i", r"s/ ..\/..\/gui\/qml\// electrum\/gui\/qml\//g", f"{build_dir}/messages_qml.pot"] subprocess.check_output(cmd) cmd = ["msgcat", "-u", "-o", f"{build_dir}/messages.pot", f"{build_dir}/messages_gettext.pot", f"{build_dir}/messages_qml.pot"] print('Generate template') subprocess.check_output(cmd) # Add a custom PO header entry to messages.pot. This header survives crowdin, # and will still be in the translated .po files, and will get compiled into the final .mo files. cnt_src_strings = 0 with open(f"{build_dir}/messages.pot", "r", encoding="utf-8") as f: for line in f.readlines(): if line.startswith('msgid '): cnt_src_strings += 1 with open(f"{build_dir}/messages_customheader.pot", "w", encoding="utf-8") as f: f.write('''msgid ""\n''') f.write('''msgstr ""\n''') f.write(f'''"X-Electrum-SourceStringCount: {cnt_src_strings}"\n''') cmd = ["msgcat", "-u", "-o", f"{build_dir}/messages.pot", f"{build_dir}/messages.pot", f"{build_dir}/messages_customheader.pot"] print('Add custom header to template') subprocess.check_output(cmd) # prepare uploading to crowdin os.chdir(os.path.join(project_root, "electrum")) crowdin_api_key = None filename = os.path.expanduser('~/.crowdin_api_key') if os.path.exists(filename): with open(filename) as f: crowdin_api_key = f.read().strip() if "crowdin_api_key" in os.environ: crowdin_api_key = os.environ["crowdin_api_key"] if not crowdin_api_key: print('Missing crowdin_api_key. Cannot push.') sys.exit(1) print('Found crowdin_api_key. Will push updated source-strings to crowdin.') crowdin_project_id = 20482 # for "Electrum" project on crowdin locale_file_name = os.path.join(build_dir, "messages.pot") crowdin_file_name = "messages.pot" crowdin_file_id = 68 # for "/electrum-client/messages.pot" global_headers = {"Authorization": "Bearer {}".format(crowdin_api_key)} # client.storages.add_storage(f) # https://support.crowdin.com/developer/api/v2/?q=api#tag/Storage/operation/api.storages.post print(f"Uploading to temp storage...") url = f'https://api.crowdin.com/api/v2/storages' with open(locale_file_name, 'rb') as f: headers = {**global_headers, **{"Crowdin-API-FileName": crowdin_file_name}} response = requests.request("POST", url, data=f, headers=headers) response.raise_for_status() print("", "storages.add_storage:", "-" * 20, response.text, "-" * 20, sep="\n") storage_id = response.json()["data"]["id"] # client.source_files.update_file(projectId=crowdin_project_id, storageId=storage_id, fileId=crowdin_file_id) # https://support.crowdin.com/developer/api/v2/?q=api#tag/Source-Files/operation/api.projects.files.put print(f"Copying from temp storage and updating file in perm storage...") url = f'https://api.crowdin.com/api/v2/projects/{crowdin_project_id}/files/{crowdin_file_id}' headers = {**global_headers, **{"content-type": "application/json"}} response = requests.request("PUT", url, json={"storageId": storage_id}, headers=headers) response.raise_for_status() print("", "source_files.update_file:", "-" * 20, response.text, "-" * 20, sep="\n") # client.translations.build_crowdin_project_translation(projectId=crowdin_project_id) # https://support.crowdin.com/developer/api/v2/?q=api#tag/Translations/operation/api.projects.translations.builds.post print(f"Rebuilding translations...") url = f'https://api.crowdin.com/api/v2/projects/{crowdin_project_id}/translations/builds' headers = {**global_headers, **{"content-type": "application/json"}} json_data = { #"exportApprovedOnly": True, # only include translated-strings approved by users with "Proofreader" permission } # note: these settings MUST be verified by electrum-locale/update.py again, at download-time. response = requests.request("POST", url, json=json_data, headers=headers) response.raise_for_status() print("", "translations.build_crowdin_project_translation:", "-" * 20, response.text, "-" * 20, sep="\n") ================================================ FILE: contrib/locale/stats.py ================================================ #!/usr/bin/env python3 # # Copyright (C) 2026 The Electrum developers # Distributed under the MIT software license, see the accompanying # file LICENCE or http://www.opensource.org/licenses/mit-license.php # # # This generates a 'stats.json' file containing some statistics about translation completeness. import gettext import glob import json import os PROJECT_ROOT = os.path.abspath(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) LOCALE_DIR = os.path.join(PROJECT_ROOT, "electrum", "locale", "locale") if __name__ == '__main__': catalog_size = {} # type: dict[str, int] source_string_count = None # - calc stats files_list = glob.glob(f"{LOCALE_DIR}/*/LC_MESSAGES/*.mo") for fname in files_list: lang_code = os.path.basename(os.path.dirname(os.path.dirname(fname))) try: t = gettext.translation('electrum', LOCALE_DIR, languages=[lang_code]) except OSError as e: raise Exception(f"cannot find or parse .mo file matching {fname!r}") from e # calc catalog size of translated strings catalog_size[lang_code] = len(t._catalog) # same SourceStringCount header should be present in all .mo files: t_info = t.info() try: ss_cnt = int(t_info["x-electrum-sourcestringcount"]) except Exception as e: raise Exception( f"missing or malformed 'x-electrum-sourcestringcount' header, for {lang_code!r}.\n" f"found {t_info}" ) from e if source_string_count is None: source_string_count = ss_cnt elif source_string_count != ss_cnt: raise Exception( f"inconsistent 'x-electrum-sourcestringcount' headers! " f"prev_cnt={source_string_count}, new_cnt={ss_cnt} (for lang={lang_code})") # - convert to json data. example: # { # "source_string_count": 9999, # "translations": { # "de_DE": { # "string_count": 400, # }, # ... # } # } json_data = { "source_string_count": source_string_count, "translations": {}, } for lang_code in catalog_size: json_data["translations"][lang_code] = {} json_data["translations"][lang_code]["string_count"] = catalog_size[lang_code] # - write json to disk with open(f"{LOCALE_DIR}/stats.json", "w", encoding="utf-8") as f: json_str = json.dumps( json_data, indent=4, sort_keys=True ) f.write(json_str) print(f"done. created file '{LOCALE_DIR}/stats.json'") ================================================ FILE: contrib/make_download ================================================ #!/usr/bin/python3 import re import os import sys import importlib from collections import defaultdict if len(sys.argv) < 2: print(f"ERROR. usage: {os.path.basename(__file__)} ", file=sys.stderr) sys.exit(1) # cd to project root os.chdir(os.path.dirname(os.path.dirname(__file__))) # load version.py; needlessly complicated alternative to "imp.load_source": version_spec = importlib.util.spec_from_file_location('version', 'electrum/version.py') version_module = importlib.util.module_from_spec(version_spec) version_spec.loader.exec_module(version_module) ELECTRUM_VERSION = version_module.ELECTRUM_VERSION print(f"version: {ELECTRUM_VERSION}", file=sys.stderr) dirname = sys.argv[1] print(f"directory: {dirname}", file=sys.stderr) download_page = os.path.join(dirname, "panel-download.html") download_template = download_page + ".template" with open(download_template) as f: download_page_str = f.read() version = version_win = version_mac = version_android = ELECTRUM_VERSION download_page_str = download_page_str.replace("##VERSION##", version) download_page_str = download_page_str.replace("##VERSION_WIN##", version_win) download_page_str = download_page_str.replace("##VERSION_MAC##", version_mac) download_page_str = download_page_str.replace("##VERSION_ANDROID##", version_android) download_page_str = download_page_str.replace("##VERSION_APK##", version_android) # note: all dist files need to be listed here that we expect sigs for, # even if they are not linked to from the website files = { "tgz": f"Electrum-{version}.tar.gz", "tgz_srconly": f"Electrum-sourceonly-{version}.tar.gz", "appimage": f"electrum-{version}-x86_64.AppImage", "mac": f"electrum-{version_mac}.dmg", "win": f"electrum-{version_win}.exe", "win_setup": f"electrum-{version_win}-setup.exe", "win_portable": f"electrum-{version_win}-portable.exe", "apk_arm64": f"Electrum-{version_android}-arm64-v8a-release.apk", "apk_armeabi": f"Electrum-{version_android}-armeabi-v7a-release.apk", "apk_x86_64": f"Electrum-{version_android}-x86_64-release.apk", } # default signers signers = ['ThomasV', 'sombernight_releasekey'] # detect extra signers list_dir = sorted(os.listdir('dist')) detected_sigs = defaultdict(set) for f in list_dir: if f.endswith('.asc'): parts = f.split('.') signer = parts[-2] filename = '.'.join(parts[0:-2]) detected_sigs[signer].add(filename) for k, v in detected_sigs.items(): if v == set(files.values()): if k not in signers: signers.append(k) print(f"signers: {signers}", file=sys.stderr) friendly_nick = lambda x: 'SomberNight' if x=='sombernight_releasekey' else x signers_list = ', '.join("%s"%(x, friendly_nick(x)) for x in signers) download_page_str = download_page_str.replace("##signers_list##", signers_list) for k, filename in files.items(): path = "dist/%s"%filename assert filename in list_dir link = "https://download.electrum.org/%s/%s"%(version, filename) download_page_str = download_page_str.replace("##link_%s##" % k, link) download_page_str = download_page_str.replace("##sigs_%s##" % k, link + '.asc') # download page has been constructed from template; now insert it into index.html index_html_path = os.path.join(dirname, "index.html") with open(f"{index_html_path}.template") as f: index_html_str = f.read() index_html_str = index_html_str.replace("##DOWNLOAD_PAGE##", download_page_str) with open(index_html_path, 'w') as f: f.write(index_html_str) ================================================ FILE: contrib/make_libsecp256k1.sh ================================================ #!/bin/bash # This script was tested on Linux and MacOS hosts, where it can be used # to build native libsecp256k1 binaries. # # It can also be used to cross-compile to Windows: # $ sudo apt-get install mingw-w64 # For a Windows x86 (32-bit) target, run: # $ GCC_TRIPLET_HOST="i686-w64-mingw32" ./contrib/make_libsecp256k1.sh # Or for a Windows x86_64 (64-bit) target, run: # $ GCC_TRIPLET_HOST="x86_64-w64-mingw32" ./contrib/make_libsecp256k1.sh # # To cross-compile to Linux x86: # sudo apt-get install gcc-multilib g++-multilib # $ AUTOCONF_FLAGS="--host=i686-linux-gnu CFLAGS=-m32 CXXFLAGS=-m32 LDFLAGS=-m32" ./contrib/make_libsecp256k1.sh LIBSECP_VERSION="1a53f4961f337b4d166c25fce72ef0dc88806618" # ^ tag "v0.7.1" # note: this version is duplicated in contrib/android/p4a_recipes/libsecp256k1/__init__.py # (and also in electrum-ecc, for the "secp256k1" git submodule) set -e . "$(dirname "$0")/build_tools_util.sh" || (echo "Could not source build_tools_util.sh" && exit 1) here="$(dirname "$(realpath "$0" 2> /dev/null || grealpath "$0")")" CONTRIB="$here" PROJECT_ROOT="$CONTRIB/.." pkgname="secp256k1" info "Building $pkgname..." ( cd "$CONTRIB" if [ ! -d secp256k1 ]; then git clone https://github.com/bitcoin-core/secp256k1.git fi cd secp256k1 if ! $(git cat-file -e ${LIBSECP_VERSION}) ; then info "Could not find requested version $LIBSECP_VERSION in local clone; fetching..." git fetch --all fi git reset --hard git clean -dfxq git checkout "${LIBSECP_VERSION}^{commit}" if ! [ -x configure ] ; then echo "LDFLAGS = -no-undefined" >> Makefile.am ./autogen.sh || fail "Could not run autogen for $pkgname. Please make sure you have automake and libtool installed, and try again." fi if ! [ -r config.status ] ; then ./configure \ $AUTOCONF_FLAGS \ --prefix="$here/$pkgname/dist" \ --enable-module-recovery \ --enable-module-extrakeys \ --enable-module-schnorrsig \ --enable-experimental \ --enable-module-ecdh \ --disable-benchmark \ --disable-tests \ --disable-exhaustive-tests \ --disable-static \ --enable-shared || fail "Could not configure $pkgname. Please make sure you have a C compiler installed and try again." fi make "-j$CPU_COUNT" || fail "Could not build $pkgname" make install || fail "Could not install $pkgname" . "$here/$pkgname/dist/lib/libsecp256k1.la" host_strip "$here/$pkgname/dist/lib/$dlname" if [ -n "$DLL_TARGET_DIR" ] ; then cp -fpv "$here/$pkgname/dist/lib/$dlname" "$DLL_TARGET_DIR/" || fail "Could not copy the $pkgname binary to DLL_TARGET_DIR" else cp -fpv "$here/$pkgname/dist/lib/$dlname" "$PROJECT_ROOT/electrum" || fail "Could not copy the $pkgname binary to its destination" info "$dlname has been placed in the 'electrum' folder." fi ) ================================================ FILE: contrib/make_libusb.sh ================================================ #!/bin/bash LIBUSB_VERSION="d52e355daa09f17ce64819122cb067b8a2ee0d4b" # ^ tag v1.0.27 set -e . "$(dirname "$0")/build_tools_util.sh" || (echo "Could not source build_tools_util.sh" && exit 1) here="$(dirname "$(realpath "$0" 2> /dev/null || grealpath "$0")")" CONTRIB="$here" PROJECT_ROOT="$CONTRIB/.." pkgname="libusb" info "Building $pkgname..." ( cd "$CONTRIB" if [ ! -d libusb ]; then git clone https://github.com/libusb/libusb.git fi cd libusb if ! $(git cat-file -e ${LIBUSB_VERSION}) ; then info "Could not find requested version $LIBUSB_VERSION in local clone; fetching..." git fetch --all fi git reset --hard git clean -dfxq git checkout "${LIBUSB_VERSION}^{commit}" if [ "$BUILD_TYPE" = "wine" ] ; then echo "libusb_1_0_la_LDFLAGS += -Wc,-static" >> libusb/Makefile.am fi ./bootstrap.sh || fail "Could not bootstrap libusb" if ! [ -r config.status ] ; then if [ "$BUILD_TYPE" = "wine" ] ; then # windows target LDFLAGS="-Wl,--no-insert-timestamp" elif [ $(uname) == "Darwin" ]; then # macos target LDFLAGS="-Wl -lm" else # linux target LDFLAGS="" fi LDFLAGS="$LDFLAGS" ./configure \ $AUTOCONF_FLAGS \ || fail "Could not configure $pkgname. Please make sure you have a C compiler installed and try again." fi make "-j$CPU_COUNT" || fail "Could not build $pkgname" make install || warn "Could not install $pkgname" . "$here/$pkgname/libusb/.libs/libusb-1.0.la" host_strip "$here/$pkgname/libusb/.libs/$dlname" TARGET_NAME="$dlname" if [ $(uname) == "Darwin" ]; then # on mac, dlname is "libusb-1.0.0.dylib" TARGET_NAME="libusb-1.0.dylib" fi cp -fpv "$here/$pkgname/libusb/.libs/$dlname" "$PROJECT_ROOT/electrum/$TARGET_NAME" || fail "Could not copy the $pkgname binary to its destination" info "$TARGET_NAME has been placed in the inner 'electrum' folder." if [ -n "$DLL_TARGET_DIR" ] ; then cp -fpv "$here/$pkgname/libusb/.libs/$dlname" "$DLL_TARGET_DIR/$TARGET_NAME" || fail "Could not copy the $pkgname binary to DLL_TARGET_DIR" fi ) ================================================ FILE: contrib/make_packages.sh ================================================ #!/bin/bash # This script installs our pure python dependencies into the 'packages' folder. set -e CONTRIB="$(dirname "$(readlink -e "$0")")" PROJECT_ROOT="$CONTRIB"/.. PACKAGES="$PROJECT_ROOT"/packages/ test -n "$CONTRIB" -a -d "$CONTRIB" || exit cd "$CONTRIB" if [ -d "$PACKAGES" ]; then rm -r "$PACKAGES" fi # create virtualenv # note: venv path needs to be deterministic as some produced files will contain it venv_dir="$CONTRIB/.venv_make_packages/" rm -rf "$venv_dir" python3 -m venv "$venv_dir" source "$venv_dir"/bin/activate # installing pinned build-time requirements, such as pip/wheel/setuptools python3 -m pip install --no-build-isolation --no-dependencies --no-warn-script-location \ -r "$CONTRIB"/deterministic-build/requirements-build-base.txt # opt out of compiling C extensions export AIOHTTP_NO_EXTENSIONS=1 export YARL_NO_EXTENSIONS=1 export MULTIDICT_NO_EXTENSIONS=1 export FROZENLIST_NO_EXTENSIONS=1 export PROPCACHE_NO_EXTENSIONS=1 export ELECTRUM_ECC_DONT_COMPILE=1 # see https://github.com/python-websockets/websockets/blob/e6d0ea1d6b13a979924329d02fb82f79d82c7236/setup.py#L22 export BUILD_EXTENSION="no" # if we end up having to compile something, at least give reproducibility a fighting chance export LC_ALL=C export TZ=UTC export SOURCE_DATE_EPOCH="$(git log -1 --pretty=%ct 2>/dev/null || printf 1530212462)" export PYTHONHASHSEED="$SOURCE_DATE_EPOCH" export BUILD_DATE="$(LC_ALL=C TZ=UTC date +'%b %e %Y' -d @$SOURCE_DATE_EPOCH)" export BUILD_TIME="$(LC_ALL=C TZ=UTC date +'%H:%M:%S' -d @$SOURCE_DATE_EPOCH)" # FIXME aiohttp will compile some .so files using distutils # (until https://github.com/aio-libs/aiohttp/pull/4079 gets released), # which are not reproducible unless using at least python 3.9 # (as it needs https://github.com/python/cpython/commit/0d30ae1a03102de07758650af9243fd31211325a). # Hence "aiohttp-*.dist-info/" is not reproducible either. # All this means that downstream users of this script, such as the sdist build # and the android apk build need to make sure these files get excluded. # note: --no-build-isolation is needed so that pip uses the locally available setuptools and wheel, # instead of downloading the latest ones python3 -m pip install --no-build-isolation --no-compile --no-dependencies --no-binary :all: \ -r "$CONTRIB"/deterministic-build/requirements.txt -t "$PACKAGES" echo "Pure-python dependencies have been placed into $PACKAGES" ================================================ FILE: contrib/make_plugin ================================================ #!/usr/bin/python3 import os import sys import hashlib import json import zipfile import zipimport # todo: use version number if len(sys.argv) != 2: print(f"usage: {os.path.basename(__file__)} ", file=sys.stderr) sys.exit(1) source_dir = sys.argv[1] # where the plugin source code is if source_dir.endswith('/'): source_dir = source_dir[:-1] plugin_name = os.path.basename(source_dir) dest_dir = os.getcwd() zip_path = os.path.join(dest_dir, plugin_name + '.zip') # remove old zipfile if os.path.exists(zip_path): os.unlink(zip_path) # create zipfile print('creating', zip_path) with zipfile.ZipFile(zip_path, 'w') as zip_object: for folder_name, sub_folders, file_names in os.walk(source_dir): for filename in file_names: file_path = os.path.join(folder_name, filename) dest_path = os.path.join(plugin_name, os.path.relpath(folder_name, source_dir), os.path.basename(file_path)) zip_object.write(file_path, dest_path) print('added', dest_path) # read version try: with open(os.path.join(source_dir, 'manifest.json'), 'r') as f: manifest = json.load(f) version = manifest.get('version') except FileNotFoundError: raise Exception(f"plugin doesn't contain manifest.json") if version: versioned_plugin_name = plugin_name + '-' + version + '.zip' zip_path_with_version = os.path.join(dest_dir, versioned_plugin_name) # rename zip file os.rename(zip_path, zip_path_with_version) print(f'Created {zip_path_with_version}') else: print(f'Created {zip_path}') ================================================ FILE: contrib/make_zbar.sh ================================================ #!/bin/bash # This script can be used on Linux hosts to build native libzbar binaries. # sudo apt-get install pkg-config libx11-dev libx11-6 libv4l-dev libxv-dev libxext-dev libjpeg-dev # # It can also be used to cross-compile to Windows: # $ sudo apt-get install mingw-w64 mingw-w64-tools win-iconv-mingw-w64-dev # For a Windows x86 (32-bit) target, run: # $ GCC_TRIPLET_HOST="i686-w64-mingw32" BUILD_TYPE="wine" ./contrib/make_zbar.sh # Or for a Windows x86_64 (64-bit) target, run: # $ GCC_TRIPLET_HOST="x86_64-w64-mingw32" BUILD_TYPE="wine" ./contrib/make_zbar.sh ZBAR_VERSION="bb05ec54eec57f8397cb13fb9161372a281a1219" # ^ tag 0.23.93 set -e . "$(dirname "$0")/build_tools_util.sh" || (echo "Could not source build_tools_util.sh" && exit 1) here="$(dirname "$(realpath "$0" 2> /dev/null || grealpath "$0")")" CONTRIB="$here" PROJECT_ROOT="$CONTRIB/.." pkgname="zbar" info "Building $pkgname..." ( cd "$CONTRIB" if [ ! -d zbar ]; then git clone https://github.com/mchehab/zbar.git fi cd zbar if ! $(git cat-file -e ${ZBAR_VERSION}) ; then info "Could not find requested version $ZBAR_VERSION in local clone; fetching..." git fetch --all fi git reset --hard git clean -dfxq git checkout "${ZBAR_VERSION}^{commit}" if [ "$BUILD_TYPE" = "wine" ] ; then echo "libzbar_la_LDFLAGS += -Wc,-static" >> zbar/Makefile.am echo "LDFLAGS += -Wc,-static" >> Makefile.am fi if ! [ -x configure ] ; then autoreconf -vfi || fail "Could not run autoreconf for $pkgname. Please make sure you have automake and libtool installed, and try again." fi if ! [ -r config.status ] ; then if [ "$BUILD_TYPE" = "wine" ] ; then # windows target AUTOCONF_FLAGS="$AUTOCONF_FLAGS \ --with-x=no \ --enable-video=yes \ --with-jpeg=no \ --with-directshow=yes \ --disable-dependency-tracking" elif [ $(uname) == "Darwin" ]; then # macos target AUTOCONF_FLAGS="$AUTOCONF_FLAGS \ --with-x=no \ --enable-video=no \ --with-jpeg=no" else # linux target AUTOCONF_FLAGS="$AUTOCONF_FLAGS \ --with-x=yes \ --enable-video=yes \ --with-jpeg=yes" fi ./configure \ $AUTOCONF_FLAGS \ --prefix="$here/$pkgname/dist" \ --enable-pthread=no \ --enable-doc=no \ --with-python=no \ --with-gtk=no \ --with-qt=no \ --with-java=no \ --with-imagemagick=no \ --with-dbus=no \ --enable-codes=qrcode \ --disable-static \ --enable-shared || fail "Could not configure $pkgname. Please make sure you have a C compiler installed and try again." fi make "-j$CPU_COUNT" || fail "Could not build $pkgname" make install || fail "Could not install $pkgname" . "$here/$pkgname/dist/lib/libzbar.la" host_strip "$here/$pkgname/dist/lib/$dlname" cp -fpv "$here/$pkgname/dist/lib/$dlname" "$PROJECT_ROOT/electrum" || fail "Could not copy the $pkgname binary to its destination" info "$dlname has been placed in the inner 'electrum' folder." if [ -n "$DLL_TARGET_DIR" ] ; then cp -fpv "$here/$pkgname/dist/lib/$dlname" "$DLL_TARGET_DIR/" || fail "Could not copy the $pkgname binary to DLL_TARGET_DIR" fi ) ================================================ FILE: contrib/osx/README.md ================================================ Building macOS binaries ======================= ✓ _This binary should be reproducible, meaning you should be able to generate binaries that match the official releases._ - _Minimum supported target system (i.e. what end-users need): macOS 11_ This guide explains how to build Electrum binaries for macOS systems. ## Building the binary This needs to be done on a system running macOS or OS X. The script is only tested on Intel-based (x86_64) Macs, and the binary built targets `x86_64` currently. Notes about compatibility with different macOS versions: - In general the binary is not guaranteed to run on an older version of macOS than what the build machine has. This is due to bundling the compiled Python into the [PyInstaller binary](https://github.com/pyinstaller/pyinstaller/issues/1191). - The [bundled version of Qt](https://github.com/spesmilo/electrum/issues/3685) also imposes a minimum supported macOS version. - If you want to build binaries that conform to the macOS "Gatekeeper", so as to minimise the warnings users get, the binaries need to be codesigned with a certificate issued by Apple, and starting with macOS 10.15 (targets) the binaries also need to be notarized by Apple's central server. To be able to build binaries that Apple will notarize (due to the requirements on the binaries themselves, e.g. hardened runtime) the build machine needs at least macOS 10.14. See [#6128](https://github.com/spesmilo/electrum/issues/6128). - There are two tools that can be used to notarize a binary, both part of Xcode: the old `altool` and the newer `notarytool`. `altool` [was deprecated](https://developer.apple.com/news/?id=y5mjxqmn) by Apple. `notarytool` requires Xcode 13+, and that in turn requires macOS 11.3+. We currently build the release binaries on macOS 11.7.10, and these seem to run on 11 or newer. #### Notes about reproducibility - We recommend creating a VM with a macOS guest, e.g. using VirtualBox, and building there. - The guest should run macOS 11.7.10 (that specific version). - The unix username should be `vagrant`, and `electrum` should be cloned directly to the user's home dir: `/Users/vagrant/electrum`. - Builders need to use the same version of Xcode; and note that full Xcode and Xcode commandline tools differ! We use the Xcode CLI tools as installed by brew. (version 13.2) Sanity checks: ``` $ sw_vers ProductName: macOS ProductVersion: 11.7.10 BuildVersion: 20G1427 $ xcode-select -p /Library/Developer/CommandLineTools $ xcrun --show-sdk-path /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk $ pkgutil --pkg-info=com.apple.pkg.CLTools_Executables package-id: com.apple.pkg.CLTools_Executables version: 13.2.0.0.1.1638488800 volume: / location: / install-time: XXXXXXXXXX groups: com.apple.FindSystemFiles.pkg-group $ gcc --version Configured with: --prefix=/Library/Developer/CommandLineTools/usr --with-gxx-include-dir=/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/4.2.1 Apple clang version 13.0.0 (clang-1300.0.29.30) Target: x86_64-apple-darwin20.6.0 Thread model: posix InstalledDir: /Library/Developer/CommandLineTools/usr/bin ``` - Installing extraneous brew packages can result in build differences. For example, pyinstaller seems to pick up and bundle brew-installed `libffi`. So having a dedicated "electrum binary builder macOS VM" is recommended. - Make sure that you are building from a fresh clone of electrum (or run e.g. `git clean -ffxd` to rm all local changes). #### 1. Install brew Install [`brew`](https://brew.sh/). Let brew install the Xcode CLI tools. #### 2. Build Electrum cd electrum ./contrib/osx/make_osx.sh This creates both a folder named Electrum.app and the .dmg file (both unsigned). ##### 2.1. For release binaries, here be dragons If you want the binaries codesigned for macOS and notarised by Apple's central server, also run the `sign_osx.sh` script: CODESIGN_CERT="Developer ID Application: Electrum Technologies GmbH (L6P37P7P56)" \ APPLE_TEAM_ID="L6P37P7P56" \ APPLE_ID_USER="me@email.com" \ APPLE_ID_PASSWORD="1234" \ ./contrib/osx/sign_osx.sh (note: `APPLE_ID_PASSWORD` is an app-specific password, *not* the account password) ## Verifying reproducibility and comparing against official binary Every user can verify that the official binary was created from the source code in this repository. 1. Build your own binary as described above. 2. Use the provided `compare_dmg` script to compare the binary you built with the official release binary. ``` $ ./contrib/osx/compare_dmg dist/electrum-*.dmg electrum_dmg_official_release.dmg ``` The `compare_dmg` script is mostly only needed as the official release binary is codesigned and notarized. Otherwise, the built `.app` bundles should be byte-identical. (Note that we are using `hdutil` to create the `.dmg`, and its output is not deterministic, but we cannot compare the `.dmg` files directly anyway as they contain codesigned files) ## FAQ ### What is macOS "codesigning" and "notarization"? Codesigning is the macOS OS-native signing of executables/shared-libs, that needs to be done using an ~x509-like certificate that chains back to Apple's root CA. Once a developer certificate is obtained from Apple, it can be used to codesign locally on a dev machine. Notarization is a further step usually done after, which entails uploading a distributable over the network to the Apple mothership central server, which runs some arbitrary checks on it, and if it finds the file ok, the central server gives the dev a notarization staple. This staple can then be optionally "attached" to the distributable, mutating it, which we do. (If the staple is not attached, enduser machines request it from the mothership at runtime.) Both these steps should be done during the build process. ### What is "codesigned" and/or "notarized", re the official release? - `make_osx.sh` builds a `.app`, which is unsigned/unnotarized - at this point, this `.app` is ~"byte-for-byte" reproducible - this is the sanity-check hash printed at the end of `make_osx.sh` - `make_osx.sh` creates a `.dmg` from the `.app` - this `.dmg` is not used for the official release at all, but used as the basis of testing reproducibility using the `compare_dmg` script - `sign_osx.sh` codesigns the `.app` (mutating it) - `sign_osx.sh` -> `notarize_app.sh` notarizes the `.app` (mutating it) - `sign_osx.sh` creates a `.dmg` from the `.app` - `sign_osx.sh` codesigns the `.dmg` (mutating it) - this `.dmg` becomes the official release distributable That is, the official release `.dmg` is codesigned but NOT notarized. It contains a `.app`, which is codesigned AND notarized. ### How to check if a file is codesigned? Both the `.dmg` and the contained `.app` are codesigned: ``` $ codesign --verify --deep --strict --verbose=2 $HOME/Desktop/electrum-4.5.8.dmg && echo "signed" /Users/vagrant/Desktop/electrum-4.5.8.dmg: valid on disk /Users/vagrant/Desktop/electrum-4.5.8.dmg: satisfies its Designated Requirement signed ``` ``` $ codesign --verify --deep --strict --verbose=1 $HOME/Desktop/Electrum-4.5.8.app && echo "signed" /Users/vagrant/Desktop/Electrum-4.5.8.app: valid on disk /Users/vagrant/Desktop/Electrum-4.5.8.app: satisfies its Designated Requirement signed ``` Also see `$ codesign -dvvv $HOME/Desktop/electrum-4.5.8.dmg` ### How to check if a file is notarized? The outer `.dmg` is NOT notarized, but the inner `.app` is notarized: ``` $ spctl -a -vvv -t install $HOME/Desktop/electrum-4.5.8.dmg /Users/vagrant/Desktop/electrum-4.5.8.dmg: rejected source=Unnotarized Developer ID origin=Developer ID Application: Electrum Technologies GmbH (L6P37P7P56) ``` ``` $ spctl -a -vvv -t install $HOME/Desktop/Electrum-4.5.8.app /Users/vagrant/Desktop/Electrum-4.5.8.app: accepted source=Notarized Developer ID origin=Developer ID Application: Electrum Technologies GmbH (L6P37P7P56) ``` ### How to simulate the signing procedure? It is possible to run `sign_osx.sh` using a self-signed certificate to test the signing procedure without using a production certificate. Note that the notarization process will be skipped as it is not possible to notarize an executable with Apple using a self-signed certificate. #### To generate a self-signed certificate, inside your **MacOS VM**: 1. Open the `Keychain Access` application. 2. In the menubar go to `Keychain Access` > `Certificate Assistant` > `Create a Certificate...` 3. Set a name (e.g. `signing_dummy`) 4. Change `Certificate Type` to *'Code Signing'* 5. Click `Create` and `Continue`. You now have a self-signed certificate `signing_dummy` added to your `login` keychain. #### To sign the executables with the self-signed certificate: Assuming you have the two unsigned outputs of `make_osx.sh` inside `~/electrum/dist` (e.g. `Electrum.app` and `electrum-4.5.4-1368-gc8db684cc-unsigned.dmg`). In `~/electrum` run: `$ CODESIGN_CERT="signing_dummy" ./contrib/osx/sign_osx.sh` After `sign_osx.sh` finished, you will have a new `*.dmg` inside `electrum/dist` (without the `-unsigned` postfix) which is signed with your certificate. #### To compare the unsigned executable with the self-signed executable: Running `compare_dmg` with `IS_NOTARIZED=false` should succeed: `$ IS_NOTARIZED=false ./electrum/contrib/osx/compare_dmg ` ================================================ FILE: contrib/osx/README_macos.md ================================================ # Running Electrum from source on macOS (development version) ## Prerequisites - [brew](https://brew.sh/) - python3 - git ## Main steps ### 1. Check out the code from GitHub: ``` $ git clone https://github.com/spesmilo/electrum.git $ cd electrum $ git submodule update --init ``` ### 2. Prepare for compiling libsecp256k1 To be able to build the `electrum-ecc` package from source (which is pulled in when installing Electrum in the next step), you need: ``` $ brew install autoconf automake libtool coreutils ``` ### 3. Install Electrum Run install (this should install the dependencies): ``` $ python3 -m pip install --user -e ".[gui,crypto]" ``` ### 4. Run electrum: ``` $ ./run_electrum ``` ================================================ FILE: contrib/osx/apply_sigs.sh ================================================ #!/bin/sh # Copyright (c) 2014-2019 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. # # This script is based on https://github.com/bitcoin/bitcoin/blob/194b9b8792d9b0798fdb570b79fa51f1d1f5ebaf/contrib/macdeploy/detached-sig-apply.sh export LC_ALL=C set -e if [ $(uname) != "Darwin" ]; then echo "This script needs to be run on macOS." exit 1 fi CP=gcp UNSIGNED="$1" SIGNATURE="$2" ARCH=x86_64 OUTDIR="/tmp/electrum_compare_dmg/signed_app" if [ -z "$UNSIGNED" ]; then echo "usage: $0 " exit 1 fi if [ -z "$SIGNATURE" ]; then echo "usage: $0 " exit 1 fi rm -rf ${OUTDIR} && mkdir -p ${OUTDIR} ${CP} -rf ${UNSIGNED} ${OUTDIR} tar xf "${SIGNATURE}" -C ${OUTDIR} find ${OUTDIR} -name "*.sign" | while read i; do SIZE=$(gstat -c %s "${i}") TARGET_FILE="$(echo "${i}" | sed 's/\.sign$//')" if [ -z ${QUIET} ]; then echo "Allocating space for the signature of size ${SIZE} in ${TARGET_FILE}" fi codesign_allocate -i "${TARGET_FILE}" -a ${ARCH} ${SIZE} -o "${i}.tmp" OFFSET=$(pagestuff "${i}.tmp" -p | tail -2 | grep offset | sed 's/[^0-9]*//g') if [ -z ${QUIET} ]; then echo "Attaching signature at offset ${OFFSET}" fi dd if="$i" of="${i}.tmp" bs=1 seek=${OFFSET} count=${SIZE} 2>/dev/null mv "${i}.tmp" "${TARGET_FILE}" rm "${i}" if [ -z ${QUIET} ]; then echo "Success." fi done echo "Done. .app with sigs applied is at: ${OUTDIR}" ================================================ FILE: contrib/osx/cdrkit-deterministic.patch ================================================ --- cdrkit-1.1.11.old/genisoimage/tree.c 2008-10-21 19:57:47.000000000 -0400 +++ cdrkit-1.1.11/genisoimage/tree.c 2013-12-06 00:23:18.489622668 -0500 @@ -1139,8 +1139,9 @@ scan_directory_tree(struct directory *this_dir, char *path, struct directory_entry *de) { - DIR *current_dir; + int current_file; char whole_path[PATH_MAX]; + struct dirent **d_list; struct dirent *d_entry; struct directory *parent; int dflag; @@ -1164,7 +1165,8 @@ this_dir->dir_flags |= DIR_WAS_SCANNED; errno = 0; /* Paranoia */ - current_dir = opendir(path); + //current_dir = opendir(path); + current_file = scandir(path, &d_list, NULL, alphasort); d_entry = NULL; /* @@ -1173,12 +1175,12 @@ */ old_path = path; - if (current_dir) { + if (current_file >= 0) { errno = 0; - d_entry = readdir(current_dir); + d_entry = d_list[0]; } - if (!current_dir || !d_entry) { + if (current_file < 0 || !d_entry) { int ret = 1; #ifdef USE_LIBSCHILY @@ -1191,8 +1193,8 @@ de->isorec.flags[0] &= ~ISO_DIRECTORY; ret = 0; } - if (current_dir) - closedir(current_dir); + if(d_list) + free(d_list); return (ret); } #ifdef ABORT_DEEP_ISO_ONLY @@ -1208,7 +1210,7 @@ errmsgno(EX_BAD, "use Rock Ridge extensions via -R or -r,\n"); errmsgno(EX_BAD, "or allow deep ISO9660 directory nesting via -D.\n"); } - closedir(current_dir); + free(d_list); return (1); } #endif @@ -1250,13 +1252,13 @@ * The first time through, skip this, since we already asked * for the first entry when we opened the directory. */ - if (dflag) - d_entry = readdir(current_dir); + if (dflag && current_file >= 0) + d_entry = d_list[current_file]; dflag++; - if (!d_entry) + if (current_file < 0) break; - + current_file--; /* OK, got a valid entry */ /* If we do not want all files, then pitch the backups. */ @@ -1348,7 +1350,7 @@ insert_file_entry(this_dir, whole_path, d_entry->d_name); #endif /* APPLE_HYB */ } - closedir(current_dir); + free(d_list); #ifdef APPLE_HYB /* ================================================ FILE: contrib/osx/compare_dmg ================================================ #!/usr/bin/env bash set -e if [ $(uname) != "Darwin" ]; then echo "This script needs to be run on macOS." exit 1 fi UNSIGNED_DMG="$1" RELEASE_DMG="$2" CONTRIB_OSX="$(dirname "$(grealpath "$0")")" PROJECT_ROOT="$CONTRIB_OSX/../.." WORKSPACE="/tmp/electrum_compare_dmg" WS_VOL1="$WORKSPACE/vol1" WS_VOL2="$WORKSPACE/vol2" if [ -z "$UNSIGNED_DMG" ]; then echo "usage: $0 " exit 1 fi if [ -z "$RELEASE_DMG" ]; then echo "usage: $0 " exit 1 fi UNSIGNED_DMG=$(grealpath "$UNSIGNED_DMG") RELEASE_DMG=$(grealpath "$RELEASE_DMG") cd "$PROJECT_ROOT" rm -rf "$WORKSPACE" mkdir -p "$WORKSPACE" "$WS_VOL1" "$WS_VOL2" DMG_UNSIGNED_UNPACKED="$WORKSPACE/dmg1" DMG_RELEASE_UNPACKED="$WORKSPACE/dmg2" hdiutil attach -mountroot "$WS_VOL1" "$UNSIGNED_DMG" cp -r "$WS_VOL1"/Electrum "$DMG_UNSIGNED_UNPACKED" hdiutil detach "$WS_VOL1"/Electrum hdiutil attach -mountroot "$WS_VOL2" "$RELEASE_DMG" cp -r "$WS_VOL2"/Electrum "$DMG_RELEASE_UNPACKED" hdiutil detach "$WS_VOL2"/Electrum # copy signatures from RELEASE_DMG to UNSIGNED_DMG echo "Extracting signatures from release app..." QUIET="1" "$CONTRIB_OSX/extract_sigs.sh" "$DMG_RELEASE_UNPACKED"/Electrum.app echo "Applying extracted signatures to unsigned app..." QUIET="1" "$CONTRIB_OSX/apply_sigs.sh" "$DMG_UNSIGNED_UNPACKED"/Electrum.app mac_extracted_sigs.tar.gz rm mac_extracted_sigs.tar.gz rm -rf "$DMG_UNSIGNED_UNPACKED" set -x diff=$(diff -qr "$WORKSPACE/signed_app" "$DMG_RELEASE_UNPACKED") || diff="diff errored" set +x echo $diff if [ "$diff" ]; then echo "DMGs do *not* match." echo "failure" exit 1 else echo "DMGs match." echo "success" exit 0 fi ================================================ FILE: contrib/osx/entitlements.plist ================================================ com.apple.security.cs.allow-unsigned-executable-memory com.apple.security.cs.disable-library-validation com.apple.security.cs.allow-dyld-environment-variables com.apple.security.cs.allow-jit com.apple.security.device.camera ================================================ FILE: contrib/osx/extract_sigs.sh ================================================ #!/bin/sh # Copyright (c) 2014-2019 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. # # This script is based on https://github.com/bitcoin/bitcoin/blob/194b9b8792d9b0798fdb570b79fa51f1d1f5ebaf/contrib/macdeploy/detached-sig-create.sh export LC_ALL=C set -e if [ $(uname) != "Darwin" ]; then echo "This script needs to be run on macOS." exit 1 fi TEMPDIR="/tmp/electrum_compare_dmg/sigs.temp" OUT=mac_extracted_sigs.tar.gz OUTROOT=. if [ -z "$1" ]; then echo "usage: $0 " exit 1 fi BUNDLE="$1" BUNDLE_BASENAME=$(basename "$BUNDLE") rm -rf ${TEMPDIR} mkdir -p ${TEMPDIR} MAYBE_SIGNED_FILES=$( find "$BUNDLE/Contents/MacOS/" -type f; find "$BUNDLE/Contents/Frameworks/" -type f; find "$BUNDLE/Contents/Resources/" -type f ) echo "${MAYBE_SIGNED_FILES}" | while read i; do # skip files where pagestuff errors; these probably do not need signing: pagestuff "$i" -p 1>/dev/null 2>/dev/null || continue TARGETFILE="${BUNDLE_BASENAME}/$(echo "${i}" | sed "s|.*${BUNDLE}/||")" SIZE=$(pagestuff "$i" -p | tail -2 | grep size | sed 's/[^0-9]*//g') OFFSET=$(pagestuff "$i" -p | tail -2 | grep offset | sed 's/[^0-9]*//g') SIGNFILE="${TEMPDIR}/${OUTROOT}/${TARGETFILE}.sign" DIRNAME="$(dirname "${SIGNFILE}")" mkdir -p "${DIRNAME}" if [ -z ${QUIET} ]; then echo "Adding detached signature for: ${TARGETFILE}. Size: ${SIZE}. Offset: ${OFFSET}" fi dd if="$i" of="${SIGNFILE}" bs=1 skip=${OFFSET} count=${SIZE} 2>/dev/null done # note: "$BUNDLE/Contents/CodeResources" is the "notarization staple id" FILES_TO_COPY=$(cat << EOF $BUNDLE/Contents/_CodeSignature/CodeResources $([ "${IS_NOTARIZED:-true}" != "false" ] && echo "$BUNDLE/Contents/CodeResources") EOF ) echo "${FILES_TO_COPY}" | while read i; do TARGETFILE="${BUNDLE_BASENAME}/$(echo "${i}" | sed "s|.*${BUNDLE}/||")" RESOURCE="${TEMPDIR}/${OUTROOT}/${TARGETFILE}" DIRNAME="$(dirname "${RESOURCE}")" mkdir -p "${DIRNAME}" if [ -z ${QUIET} ]; then echo "Adding resource for: \"${TARGETFILE}\"" fi cp "${i}" "${RESOURCE}" done tar -C "${TEMPDIR}" -czf "${OUT}" . rm -rf "${TEMPDIR}" echo "Created ${OUT}" ================================================ FILE: contrib/osx/make_osx.sh ================================================ #!/usr/bin/env bash set -e # Parameterize PYTHON_VERSION=3.12.10 PY_VER_MAJOR="3.12" # as it appears in fs paths PACKAGE=Electrum GIT_REPO=https://github.com/spesmilo/electrum export GCC_STRIP_BINARIES="1" export PYTHONDONTWRITEBYTECODE=1 # don't create __pycache__/ folders with .pyc files . "$(dirname "$0")/../build_tools_util.sh" CONTRIB_OSX="$(dirname "$(realpath "$0")")" CONTRIB="$CONTRIB_OSX/.." PROJECT_ROOT="$CONTRIB/.." CACHEDIR="$CONTRIB_OSX/.cache" export DLL_TARGET_DIR="$CACHEDIR/dlls" PIP_CACHE_DIR="$CACHEDIR/pip_cache" mkdir -p "$CACHEDIR" "$DLL_TARGET_DIR" "$PIP_CACHE_DIR" cd "$PROJECT_ROOT" git -C "$PROJECT_ROOT" rev-parse 2>/dev/null || fail "Building outside a git clone is not supported." which brew > /dev/null 2>&1 || fail "Please install brew from https://brew.sh/ to continue" which xcodebuild > /dev/null 2>&1 || fail "Please install xcode command line tools to continue" info "Installing Python $PYTHON_VERSION" PKG_FILE="python-${PYTHON_VERSION}-macos11.pkg" if [ ! -f "$CACHEDIR/$PKG_FILE" ]; then curl -o "$CACHEDIR/$PKG_FILE" "https://www.python.org/ftp/python/${PYTHON_VERSION}/$PKG_FILE" fi echo "8373e58da4ea146b3eb1c1f9834f19a319440b6b679b06050b1f9ee3237aa8e4 $CACHEDIR/$PKG_FILE" | shasum -a 256 -c \ || fail "python pkg checksum mismatched" sudo installer -pkg "$CACHEDIR/$PKG_FILE" -target / \ || fail "failed to install python" # sanity check "python3" has the version we just installed. FOUND_PY_VERSION=$(python3 -c 'import sys; print(".".join(map(str, sys.version_info[:3])))') if [[ "$FOUND_PY_VERSION" != "$PYTHON_VERSION" ]]; then fail "python version mismatch: $FOUND_PY_VERSION != $PYTHON_VERSION" fi break_legacy_easy_install # create a fresh virtualenv # This helps to avoid older versions of pip-installed dependencies interfering with the build. VENV_DIR="$CONTRIB_OSX/build-venv" rm -rf "$VENV_DIR" python3 -m venv "$VENV_DIR" source "$VENV_DIR/bin/activate" # don't add debug info to compiled C files (e.g. when pip calls setuptools/wheel calls gcc) # see https://github.com/pypa/pip/issues/6505#issuecomment-526613584 # note: this does not seem sufficient when cython is involved (although it is on linux, just not on mac... weird.) # see additional "strip" pass on built files later in the file. export CFLAGS="-g0" # Do not build universal binaries. The default on macos 11+ and xcode 12+ is "-arch arm64 -arch x86_64" # but with that e.g. "hid.cpython-310-darwin.so" is not reproducible as built by clang. export ARCHFLAGS="-arch x86_64" info "Installing build dependencies" # note: re pip installing from PyPI, # we prefer compiling C extensions ourselves, instead of using binary wheels, # hence "--no-binary :all:" flags. However, we specifically allow # - PyQt6, as it's harder to build from source # - cryptography, as it's harder to build from source # - the whole of "requirements-build-base.txt", which includes pip and friends, as it also includes "wheel", # and I am not quite sure how to break the circular dependence there (I guess we could introduce # "requirements-build-base-base.txt" with just wheel in it...) python3 -m pip install --no-build-isolation --no-dependencies --no-warn-script-location \ --cache-dir "$PIP_CACHE_DIR" -Ir ./contrib/deterministic-build/requirements-build-base.txt \ || fail "Could not install build dependencies (base)" python3 -m pip install --no-build-isolation --no-dependencies --no-binary :all: --no-warn-script-location \ --cache-dir "$PIP_CACHE_DIR" -Ir ./contrib/deterministic-build/requirements-build-mac.txt \ || fail "Could not install build dependencies (mac)" info "Installing some build-time deps for compilation..." brew install autoconf automake libtool gettext coreutils pkgconfig info "Building PyInstaller." PYINSTALLER_REPO="https://github.com/pyinstaller/pyinstaller.git" PYINSTALLER_COMMIT="306d4d92580fea7be7ff2c89ba112cdc6f73fac1" # ^ tag "v6.13.0" ( if [ -f "$CACHEDIR/pyinstaller/PyInstaller/bootloader/Darwin-64bit/runw" ]; then info "pyinstaller already built, skipping" exit 0 fi cd "$PROJECT_ROOT" ELECTRUM_COMMIT_HASH=$(git rev-parse HEAD) cd "$CACHEDIR" rm -rf pyinstaller mkdir pyinstaller cd pyinstaller # Shallow clone git init git remote add origin $PYINSTALLER_REPO git fetch --depth 1 origin $PYINSTALLER_COMMIT git checkout -b pinned "${PYINSTALLER_COMMIT}^{commit}" rm -fv PyInstaller/bootloader/Darwin-*/run* || true # add reproducible randomness. this ensures we build a different bootloader for each commit. # if we built the same one for all releases, that might also get anti-virus false positives echo "const char *electrum_tag = \"tagged by Electrum@$ELECTRUM_COMMIT_HASH\";" >> ./bootloader/src/pyi_main.c pushd bootloader # compile bootloader python3 ./waf all CFLAGS="-static" popd # sanity check bootloader is there: [[ -e "PyInstaller/bootloader/Darwin-64bit/runw" ]] || fail "Could not find runw in target dir!" ) info "Installing PyInstaller." python3 -m pip install --no-build-isolation --no-dependencies \ --cache-dir "$PIP_CACHE_DIR" --no-warn-script-location "$CACHEDIR/pyinstaller" info "Using these versions for building $PACKAGE:" sw_vers python3 --version echo -n "Pyinstaller " pyinstaller --version rm -rf ./dist info "resetting git submodules." # note: --force is less critical in other build scripts, but as the mac build is not doing a fresh clone, # it is very useful here for reproducibility git submodule update --init --force info "preparing electrum-locale." ( if ! which msgfmt > /dev/null 2>&1; then brew install gettext brew link --force gettext fi "$CONTRIB/locale/build_cleanlocale.sh" # we want the binary to have only compiled (.mo) locale files; not source (.po) files rm -r "$PROJECT_ROOT/electrum/locale/locale"/*/electrum.po ) if ls "$DLL_TARGET_DIR"/libsecp256k1.*.dylib 1> /dev/null 2>&1; then info "libsecp256k1 already built, skipping" else info "Building libsecp256k1 dylib..." "$CONTRIB"/make_libsecp256k1.sh || fail "Could not build libsecp" fi cp -f "$DLL_TARGET_DIR"/libsecp256k1.*.dylib "$PROJECT_ROOT/electrum" || fail "Could not copy libsecp256k1 dylib" if [ ! -f "$DLL_TARGET_DIR/libzbar.0.dylib" ]; then info "Building ZBar dylib..." "$CONTRIB"/make_zbar.sh || fail "Could not build ZBar dylib" else info "Skipping ZBar build: reusing already built dylib." fi cp -f "$DLL_TARGET_DIR/libzbar.0.dylib" "$PROJECT_ROOT/electrum/" || fail "Could not copy ZBar dylib" if [ ! -f "$DLL_TARGET_DIR/libusb-1.0.dylib" ]; then info "Building libusb dylib..." "$CONTRIB"/make_libusb.sh || fail "Could not build libusb dylib" else info "Skipping libusb build: reusing already built dylib." fi cp -f "$DLL_TARGET_DIR/libusb-1.0.dylib" "$PROJECT_ROOT/electrum/" || fail "Could not copy libusb dylib" # opt out of compiling C extensions export YARL_NO_EXTENSIONS=1 export PROPCACHE_NO_EXTENSIONS=1 export ELECTRUM_ECC_DONT_COMPILE=1 info "Installing requirements..." python3 -m pip install --no-build-isolation --no-dependencies --no-binary :all: \ --cache-dir "$PIP_CACHE_DIR" --no-warn-script-location \ -Ir ./contrib/deterministic-build/requirements.txt \ || fail "Could not install requirements" info "Installing hardware wallet requirements..." python3 -m pip install --no-build-isolation --no-dependencies --no-binary :all: --only-binary cryptography \ --cache-dir "$PIP_CACHE_DIR" --no-warn-script-location \ -Ir ./contrib/deterministic-build/requirements-hw.txt \ || fail "Could not install hardware wallet requirements" info "Installing dependencies specific to binaries..." python3 -m pip install --no-build-isolation --no-dependencies --no-binary :all: --only-binary PyQt6,PyQt6-Qt6,cryptography \ --cache-dir "$PIP_CACHE_DIR" --no-warn-script-location \ -Ir ./contrib/deterministic-build/requirements-binaries-mac.txt \ || fail "Could not install dependencies specific to binaries" info "Building $PACKAGE..." python3 -m pip install --no-build-isolation --no-dependencies \ --cache-dir "$PIP_CACHE_DIR" --no-warn-script-location . > /dev/null || fail "Could not build $PACKAGE" # pyinstaller needs to be able to "import electrum_ecc", for which we need libsecp256k1: # (or could try "pip install -e" instead) cp "$DLL_TARGET_DIR"/libsecp256k1.*.dylib "$VENV_DIR/lib/python$PY_VER_MAJOR/site-packages/electrum_ecc/" # strip debug symbols of some compiled libs # - hidapi (hid.cpython-39-darwin.so) in particular is not reproducible without this find "$VENV_DIR/lib/python$PY_VER_MAJOR/site-packages/" -type f -name '*.so' -print0 \ | xargs -0 -t strip -x info "Faking timestamps..." find . -exec touch -t '200101220000' {} + || true # note: no --dirty, as we have dirtied electrum/locale/ ourselves. VERSION=$(git describe --tags --always) info "Building binary" ELECTRUM_VERSION=$VERSION pyinstaller --noconfirm --clean contrib/osx/pyinstaller.spec || fail "Could not build binary" info "Finished building unsigned dist/${PACKAGE}.app. This hash should be reproducible:" find "dist/${PACKAGE}.app" -type f -print0 | sort -z | xargs -0 shasum -a 256 | shasum -a 256 info "Creating unsigned .DMG" hdiutil create -fs HFS+ -volname $PACKAGE -srcfolder dist/$PACKAGE.app dist/electrum-$VERSION-unsigned.dmg || fail "Could not create .DMG" info "App was built successfully but was not code signed. Users may get security warnings from macOS." info "Now you also need to run sign_osx.sh to codesign/notarize the binary." ================================================ FILE: contrib/osx/notarize_app.sh ================================================ #!/usr/bin/env bash # from https://github.com/metabrainz/picard/blob/e1354632d2db305b7a7624282701d34d73afa225/scripts/package/macos-notarize-app.sh set -e if [ -z "$1" ]; then echo "Specify app bundle as first parameter" exit 1 fi if [ -z "$APPLE_ID_USER" ] || [ -z "$APPLE_ID_PASSWORD" ] || [ -z "$APPLE_TEAM_ID" ]; then echo "You need to set your Apple ID credentials with \$APPLE_ID_USER and \$APPLE_ID_PASSWORD." exit 1 fi APP_BUNDLE=$(basename "$1") APP_BUNDLE_DIR=$(dirname "$1") cd "$APP_BUNDLE_DIR" || exit 1 # Package app for submission echo "Generating ZIP archive ${APP_BUNDLE}.zip..." ditto -c -k --rsrc --keepParent "$APP_BUNDLE" "${APP_BUNDLE}.zip" # Submit for notarization echo "Submitting $APP_BUNDLE for notarization..." RESULT=$(xcrun notarytool submit \ --team-id "$APPLE_TEAM_ID" \ --apple-id "$APPLE_ID_USER" \ --password "$APPLE_ID_PASSWORD" \ --output-format plist \ --wait \ --timeout 10m \ "${APP_BUNDLE}.zip" ) if [ $? -ne 0 ]; then echo "Submitting $APP_BUNDLE failed:" echo "$RESULT" exit 1 fi STATUS=$(echo "$RESULT" | xpath -e \ "//key[normalize-space(text()) = 'status']/following-sibling::string[1]/text()" 2> /dev/null) if [ "$STATUS" = "Accepted" ]; then echo "Notarization of $APP_BUNDLE succeeded!" else echo "Notarization of $APP_BUNDLE failed:" echo "$RESULT" exit 1 fi # Staple the notary ticket xcrun stapler staple "$APP_BUNDLE" # rm zip rm "${APP_BUNDLE}.zip" ================================================ FILE: contrib/osx/package.sh ================================================ #!/usr/bin/env bash set -ex PROJECT_ROOT="$(dirname "$(readlink -e "$0")")/../.." CONTRIB="$PROJECT_ROOT/contrib" . "$CONTRIB"/build_tools_util.sh # note: GCC 10.1 will need an extra option, see https://github.com/bitcoin/bitcoin/pull/19553 cdrkit_version=1.1.11 cdrkit_download_path=http://distro.ibiblio.org/fatdog/source/600/c cdrkit_file_name=cdrkit-${cdrkit_version}.tar.bz2 cdrkit_sha256_hash=b50d64c214a65b1a79afe3a964c691931a4233e2ba605d793eb85d0ac3652564 cdrkit_patches=cdrkit-deterministic.patch genisoimage=genisoimage-$cdrkit_version libdmg_url=https://github.com/theuni/libdmg-hfsplus export LD_PRELOAD=$(locate libfaketime.so.1) export FAKETIME="2000-01-22 00:00:00" export PATH=$PATH:~/bin if [ -z "$1" ]; then echo "Usage: $0 Electrum.app" exit -127 fi mkdir -p ~/bin if ! which ${genisoimage} > /dev/null 2>&1; then mkdir -p /tmp/electrum-macos cd /tmp/electrum-macos info "Downloading cdrkit $cdrkit_version" wget -nc ${cdrkit_download_path}/${cdrkit_file_name} tar xvf ${cdrkit_file_name} info "Patching genisoimage" cd cdrkit-${cdrkit_version} patch -p1 <$CONTRIB/osx/cdrkit-deterministic.patch info "Building genisoimage" cmake . -Wno-dev make genisoimage cp genisoimage/genisoimage ~/bin/${genisoimage} fi if ! which dmg > /dev/null 2>&1; then mkdir -p /tmp/electrum-macos cd /tmp/electrum-macos info "Downloading libdmg" LD_PRELOAD= git clone ${libdmg_url} cd libdmg-hfsplus info "Building libdmg" cmake . make cp dmg/dmg ~/bin fi ${genisoimage} -version || fail "Unable to install genisoimage" dmg - || fail "Unable to install libdmg" plist=$1/Contents/Info.plist test -f "$plist" || fail "Info.plist not found" VERSION=$(grep -1 ShortVersionString $plist | tail -1 | gawk 'match($0, /(.*)<\/string>/, a) {print a[1]}') echo $VERSION rm -rf /tmp/electrum-macos/image > /dev/null 2>&1 mkdir /tmp/electrum-macos/image/ cp -r $1 /tmp/electrum-macos/image/ build_dir=$(dirname "$1") test -n "$build_dir" -a -d "$build_dir" || exit cd $build_dir ${genisoimage} \ -no-cache-inodes \ -D \ -l \ -probe \ -V "Electrum" \ -no-pad \ -r \ -dir-mode 0755 \ -apple \ -o Electrum_uncompressed.dmg \ /tmp/electrum-macos/image || fail "Unable to create uncompressed dmg" dmg dmg Electrum_uncompressed.dmg electrum-$VERSION.dmg || fail "Unable to create compressed dmg" rm Electrum_uncompressed.dmg echo "Done." sha256sum electrum-$VERSION.dmg ================================================ FILE: contrib/osx/pyinstaller.spec ================================================ # -*- mode: python -*- import sys import os from typing import TYPE_CHECKING from PyInstaller.utils.hooks import collect_data_files, collect_submodules, collect_dynamic_libs, copy_metadata if TYPE_CHECKING: from PyInstaller.building.build_main import Analysis, PYZ, EXE, BUNDLE PACKAGE_NAME='Electrum.app' PYPKG='electrum' MAIN_SCRIPT='run_electrum' PROJECT_ROOT = os.path.abspath(".") ICONS_FILE=f"{PROJECT_ROOT}/{PYPKG}/gui/icons/electrum.icns" VERSION = os.environ.get("ELECTRUM_VERSION") if not VERSION: raise Exception('no version') block_cipher = None # see https://github.com/pyinstaller/pyinstaller/issues/2005 hiddenimports = [] hiddenimports += collect_submodules('pkg_resources') # workaround for https://github.com/pypa/setuptools/issues/1963 hiddenimports += collect_submodules(f"{PYPKG}.plugins") binaries = [] # Workaround for "Retro Look": binaries += [b for b in collect_dynamic_libs('PyQt6') if 'macstyle' in b[0]] # add libsecp256k1, libusb, etc: binaries += [(f"{PROJECT_ROOT}/{PYPKG}/*.dylib", ".")] datas = [ (f"{PROJECT_ROOT}/{PYPKG}/*.json", PYPKG), (f"{PROJECT_ROOT}/{PYPKG}/lnwire/*.csv", f"{PYPKG}/lnwire"), (f"{PROJECT_ROOT}/{PYPKG}/wordlist/english.txt", f"{PYPKG}/wordlist"), (f"{PROJECT_ROOT}/{PYPKG}/wordlist/slip39.txt", f"{PYPKG}/wordlist"), (f"{PROJECT_ROOT}/{PYPKG}/chains", f"{PYPKG}/chains"), (f"{PROJECT_ROOT}/{PYPKG}/locale", f"{PYPKG}/locale"), (f"{PROJECT_ROOT}/{PYPKG}/plugins", f"{PYPKG}/plugins"), (f"{PROJECT_ROOT}/{PYPKG}/gui/icons", f"{PYPKG}/gui/icons"), (f"{PROJECT_ROOT}/{PYPKG}/gui/fonts", f"{PYPKG}/gui/fonts"), ] datas += collect_data_files(f"{PYPKG}.plugins") datas += collect_data_files('trezorlib') # TODO is this needed? and same question for other hww libs datas += collect_data_files('safetlib') datas += collect_data_files('ckcc') datas += collect_data_files('bitbox02') # some deps rely on importlib metadata datas += copy_metadata('slip10') # from trezor->slip10 # Exclude parts of Qt that we never use. Reduces binary size by tens of MBs. see #4815 excludes = [ "PyQt6.QtBluetooth", "PyQt6.QtDesigner", "PyQt6.QtNfc", "PyQt6.QtPositioning", "PyQt6.QtQml", "PyQt6.QtQuick", "PyQt6.QtQuick3D", "PyQt6.QtQuickWidgets", "PyQt6.QtRemoteObjects", "PyQt6.QtSensors", "PyQt6.QtSerialPort", "PyQt6.QtSpatialAudio", "PyQt6.QtSql", "PyQt6.QtTest", "PyQt6.QtTextToSpeech", "PyQt6.QtWebChannel", "PyQt6.QtWebSockets", "PyQt6.QtXml", # "PyQt6.QtNetwork", # needed by QtMultimedia. kinda weird but ok. ] # We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports a = Analysis([f"{PROJECT_ROOT}/{MAIN_SCRIPT}", f"{PROJECT_ROOT}/{PYPKG}/gui/qt/main_window.py", f"{PROJECT_ROOT}/{PYPKG}/gui/qt/qrreader/qtmultimedia/camera_dialog.py", f"{PROJECT_ROOT}/{PYPKG}/gui/text.py", f"{PROJECT_ROOT}/{PYPKG}/util.py", f"{PROJECT_ROOT}/{PYPKG}/wallet.py", f"{PROJECT_ROOT}/{PYPKG}/simple_config.py", f"{PROJECT_ROOT}/{PYPKG}/bitcoin.py", f"{PROJECT_ROOT}/{PYPKG}/dnssec.py", f"{PROJECT_ROOT}/{PYPKG}/commands.py", ], binaries=binaries, datas=datas, hiddenimports=hiddenimports, hookspath=[], excludes=excludes, ) # http://stackoverflow.com/questions/19055089/pyinstaller-onefile-warning-pyconfig-h-when-importing-scipy-or-scipy-signal for d in a.datas: if 'pyconfig' in d[0]: a.datas.remove(d) break pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) exe = EXE( pyz, a.scripts, exclude_binaries=True, name=MAIN_SCRIPT, debug=False, strip=False, upx=True, icon=ICONS_FILE, console=False, target_arch='x86_64', # TODO investigate building 'universal2' ) app = BUNDLE( exe, a.binaries, a.zipfiles, a.datas, version=VERSION, name=PACKAGE_NAME, icon=ICONS_FILE, bundle_identifier=None, info_plist={ 'NSHighResolutionCapable': 'True', 'NSSupportsAutomaticGraphicsSwitching': 'True', 'CFBundleURLTypes': [{ 'CFBundleURLName': 'bitcoin', 'CFBundleURLSchemes': ['bitcoin', 'lightning', ], }], 'LSMinimumSystemVersion': '11', 'NSCameraUsageDescription': 'Electrum would like to access the camera to scan for QR codes', }, ) ================================================ FILE: contrib/osx/sign_osx.sh ================================================ #!/usr/bin/env bash set -e security -v unlock-keychain login.keychain PACKAGE=Electrum . "$(dirname "$0")/../build_tools_util.sh" CONTRIB_OSX="$(dirname "$(realpath "$0")")" CONTRIB="$CONTRIB_OSX/.." PROJECT_ROOT="$CONTRIB/.." CACHEDIR="$CONTRIB_OSX/.cache" cd "$PROJECT_ROOT" # Code Signing: See https://developer.apple.com/library/archive/documentation/Security/Conceptual/CodeSigningGuide/Procedures/Procedures.html if [ -n "$CODESIGN_CERT" ]; then # Test the identity is valid for signing by doing this hack. There is no other way to do this. cp -f /bin/ls ./CODESIGN_TEST set +e codesign -s "$CODESIGN_CERT" --dryrun -f ./CODESIGN_TEST > /dev/null 2>&1 res=$? set -e rm -f ./CODESIGN_TEST if ((res)); then fail "Code signing identity \"$CODESIGN_CERT\" appears to be invalid." fi unset res info "Code signing enabled using identity \"$CODESIGN_CERT\"" else fail "Code signing DISABLED. Specify a valid macOS Developer identity installed on the system to enable signing." fi function DoCodeSignMaybe { # ARGS: infoName fileOrDirName infoName="$1" file="$2" deep="" if [ -z "$CODESIGN_CERT" ]; then # no cert -> we won't codesign return fi if [ -d "$file" ]; then deep="--deep" fi if [ -z "$infoName" ] || [ -z "$file" ] || [ ! -e "$file" ]; then fail "Argument error to internal function DoCodeSignMaybe()" fi hardened_arg="--entitlements=${CONTRIB_OSX}/entitlements.plist -o runtime" info "Code signing ${infoName}..." codesign -f -v $deep -s "$CODESIGN_CERT" $hardened_arg "$file" || fail "Could not code sign ${infoName}" } # note: no --dirty, as we have dirtied electrum/locale/ ourselves. VERSION=$(git describe --tags --always) DoCodeSignMaybe "app bundle" "dist/${PACKAGE}.app" if [ ! -z "$CODESIGN_CERT" ]; then if [ ! -z "$APPLE_ID_USER" ]; then info "Notarizing .app with Apple's central server..." "${CONTRIB_OSX}/notarize_app.sh" "dist/${PACKAGE}.app" || fail "Could not notarize binary." else warn "AppleID details not set! Skipping Apple notarization." fi fi info "Creating .DMG" hdiutil create -fs HFS+ -volname $PACKAGE -srcfolder dist/$PACKAGE.app dist/electrum-$VERSION.dmg || fail "Could not create .DMG" DoCodeSignMaybe ".DMG" "dist/electrum-${VERSION}.dmg" ================================================ FILE: contrib/print_electrum_version.py ================================================ #!/usr/bin/python3 # For usage in shell, to get the version of electrum, without needing electrum installed. # usage: ./print_electrum_version.py [] # # For example: # $ VERSION=$("$CONTRIB"/print_electrum_version.py) # instead of # $ VERSION=$(python3 -c "import electrum; print(electrum.version.ELECTRUM_VERSION)") import importlib.util import os import sys if __name__ == '__main__': if len(sys.argv) >= 2: attr_name = sys.argv[1] else: attr_name = "ELECTRUM_VERSION" project_root = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) version_file_path = os.path.join(project_root, "electrum", "version.py") # load version.py; needlessly complicated alternative to "imp.load_source": version_spec = importlib.util.spec_from_file_location('version', version_file_path) version_module = version = importlib.util.module_from_spec(version_spec) version_spec.loader.exec_module(version_module) attr_val = getattr(version, attr_name) print(attr_val, file=sys.stdout) ================================================ FILE: contrib/release.sh ================================================ #!/bin/bash # # This script is used for stage 1 of the release process. It operates exclusively on the airlock. # This script, for the RELEASEMANAGER (RM): # - builds and uploads all binaries to airlock, # - assumes all keys are available, and signs everything # This script, for other builders: # - builds all reproducible binaries, # - downloads binaries built by the release manager (from airlock if SFTPUSER, else from website), # compares and signs them, # - and then uploads sigs (if SFTPUSER), else they can be submitted as PR to spesmilo/electrum-signatures # Note: the .dmg should be built separately beforehand and copied into dist/ # (as it is built on a separate machine) # # # env vars: # - ELECBUILD_NOCACHE: if set, forces rebuild of docker images # # "uploadserver" is set in /etc/hosts # # Note: steps before doing a new release: # - update locale: # 1. cd /opt/electrum-locale && ./update.py && git push # 2. cd to the submodule dir, and git pull # 3. cd .. && git push # - update RELEASE-NOTES and version.py # - $ git tag -s "$VERSION" -m "$VERSION" # - $ git push "$REMOTE_ORIGIN" tag "$VERSION" # # ----- # Then, typical release flow: # - RM runs release.sh # - Another SFTPUSER BUILDER runs `$ ./release.sh` # - now airlock contains new binaries and two sigs for each # - deploy.sh will verify sigs and move binaries across airlock # - new binaries are now publicly available on uploadserver, but not linked from website yet # - other BUILDERS can now also try to reproduce binaries and open PRs with sigs against spesmilo/electrum-signatures # - these PRs can get merged as they come # - run add_cosigner # - after some time, RM can run release_www.sh to create and commit website-update # - then run WWW_DIR/publish.sh to update website # - at least two people need to run WWW_DIR/publish.sh # set -e PROJECT_ROOT="$(dirname "$(readlink -e "$0")")/.." CONTRIB="$PROJECT_ROOT/contrib" cd "$PROJECT_ROOT" . "$CONTRIB"/build_tools_util.sh # rm -rf dist/* # rm -f .buildozer GPGUSER=$1 if [ -z "$GPGUSER" ]; then fail "usage: $0 gpg_username" fi RELEASEMANAGER="" if [ "$GPGUSER" == "ThomasV" ]; then PUBKEY="--local-user 6694D8DE7BE8EE5631BED9502BD5824B7F9470E6" export SSHUSER=thomasv RELEASEMANAGER=1 elif [ "$GPGUSER" == "sombernight_releasekey" ]; then PUBKEY="--local-user 0EEDCFD5CAFB459067349B23CA9EEEC43DF911DC" export SSHUSER=sombernight else warn "unexpected GPGUSER=$GPGUSER" PUBKEY="" export SSHUSER="" fi if [ ! -z "$RELEASEMANAGER" ] ; then echo -n "Code signing passphrase:" read -s password # tests password against keystore keytool -list -storepass $password # the same password is used for windows signing export WIN_SIGNING_PASSWORD=$password fi VERSION=$("$CONTRIB"/print_electrum_version.py) info "VERSION: $VERSION" REV=$(git describe --tags) info "REV: $REV" COMMIT=$(git rev-parse HEAD) export ELECBUILD_COMMIT="${COMMIT}^{commit}" git_status=$(git status --porcelain) if [ ! -z "$git_status" ]; then echo "$git_status" fail "git repo not clean, aborting" fi set -x # create tarball tarball="Electrum-$VERSION.tar.gz" if test -f "dist/$tarball"; then info "file exists: $tarball" else ./contrib/build-linux/sdist/build.sh fi # create source-only tarball srctarball="Electrum-sourceonly-$VERSION.tar.gz" if test -f "dist/$srctarball"; then info "file exists: $srctarball" else OMIT_UNCLEAN_FILES=1 ./contrib/build-linux/sdist/build.sh fi # appimage appimage="electrum-$REV-x86_64.AppImage" if test -f "dist/$appimage"; then info "file exists: $appimage" else ./contrib/build-linux/appimage/build.sh fi # windows win1="electrum-$REV.exe" win2="electrum-$REV-portable.exe" win3="electrum-$REV-setup.exe" if test -f "dist/$win1"; then info "file exists: $win1" else pushd . if test -f "contrib/build-wine/dist/$win1"; then info "unsigned file exists: $win1" else ./contrib/build-wine/build.sh fi cd contrib/build-wine/ if [ ! -z "$RELEASEMANAGER" ] ; then ./sign.sh cp ./signed/*.exe "$PROJECT_ROOT/dist/" else cp ./dist/*.exe "$PROJECT_ROOT/dist/" fi popd fi # android apk1="Electrum-$VERSION-armeabi-v7a-release.apk" apk2="Electrum-$VERSION-arm64-v8a-release.apk" apk3="Electrum-$VERSION-x86_64-release.apk" for arch in armeabi-v7a arm64-v8a x86_64 do apk="Electrum-$VERSION-$arch-release.apk" apk_unsigned="Electrum-$VERSION-$arch-release-unsigned.apk" if test -f "dist/$apk"; then info "file exists: $apk" else info "file does not exists: $apk" if [ ! -z "$RELEASEMANAGER" ] ; then ./contrib/android/build.sh qml $arch release $password else ./contrib/android/build.sh qml $arch release-unsigned mv "dist/$apk_unsigned" "dist/$apk" fi fi done # the macos binary is built on a separate machine. # the file that needs to be copied over is the codesigned release binary (regardless of builder role) dmg="electrum-$VERSION.dmg" if ! test -f "dist/$dmg"; then if [ ! -z "$RELEASEMANAGER" ] ; then # RM fail "dmg is missing, aborting. Please build and codesign the dmg on a mac and copy it over." else # other builders fail "dmg is missing, aborting. Please build the unsigned dmg on a mac, compare it with file built by RM, and if matches, copy RM's dmg." fi fi # now that we have all binaries, if we are the RM, sign them. if [ ! -z "$RELEASEMANAGER" ] ; then if test -f "dist/$dmg.asc"; then info "packages are already signed" else info "signing packages" ./contrib/sign_packages "$GPGUSER" fi fi info "build complete" sha256sum dist/*.tar.gz sha256sum dist/*.AppImage sha256sum contrib/build-wine/dist/*.exe echo -n "proceed (y/n)? " read answer if [ "$answer" != "y" ]; then echo "exit" exit 1 fi if [ -z "$RELEASEMANAGER" ] ; then # people OTHER THAN release manager. # download binaries built by RM rm -rf "$PROJECT_ROOT/dist/releasemanager" mkdir --parent "$PROJECT_ROOT/dist/releasemanager" cd "$PROJECT_ROOT/dist/releasemanager" if [ -z "$SSHUSER" ]; then info "No SFTP access, downloading binaries from website" BASE_URL="https://download.electrum.org/$VERSION" FILES_TO_DOWNLOAD=( "$tarball" "$srctarball" "$appimage" "$win1" "$win2" "$win3" "$apk1" "$apk2" "$apk3" "$dmg" ) for filename in "${FILES_TO_DOWNLOAD[@]}"; do if [ ! -f "$filename" ]; then info "Downloading $filename..." wget -q "$BASE_URL/$filename" -O "$filename" || fail "Failed to download $filename" else info "File already exists: $filename" fi done else # TODO check somehow that RM had finished uploading sftp -oBatchMode=no -b - "$SSHUSER@uploadserver" <<-EOF cd electrum-downloads-airlock cd "$VERSION" mget * bye EOF fi # check we have each binary test -f "$tarball" || fail "tarball not found among sftp downloads" test -f "$srctarball" || fail "srctarball not found among sftp downloads" test -f "$appimage" || fail "appimage not found among sftp downloads" test -f "$win1" || fail "win1 not found among sftp downloads" test -f "$win2" || fail "win2 not found among sftp downloads" test -f "$win3" || fail "win3 not found among sftp downloads" test -f "$apk1" || fail "apk1 not found among sftp downloads" test -f "$apk2" || fail "apk2 not found among sftp downloads" test -f "$apk3" || fail "apk3 not found among sftp downloads" test -f "$dmg" || fail "dmg not found among sftp downloads" test -f "$PROJECT_ROOT/dist/$tarball" || fail "tarball not found among built files" test -f "$PROJECT_ROOT/dist/$srctarball" || fail "srctarball not found among built files" test -f "$PROJECT_ROOT/dist/$appimage" || fail "appimage not found among built files" test -f "$CONTRIB/build-wine/dist/$win1" || fail "win1 not found among built files" test -f "$CONTRIB/build-wine/dist/$win2" || fail "win2 not found among built files" test -f "$CONTRIB/build-wine/dist/$win3" || fail "win3 not found among built files" test -f "$PROJECT_ROOT/dist/$apk1" || fail "apk1 not found among built files" test -f "$PROJECT_ROOT/dist/$apk2" || fail "apk2 not found among built files" test -f "$PROJECT_ROOT/dist/$apk3" || fail "apk3 not found among built files" test -f "$PROJECT_ROOT/dist/$dmg" || fail "dmg not found among built files" # compare downloaded binaries against ones we built cmp --silent "$tarball" "$PROJECT_ROOT/dist/$tarball" || fail "files are different. tarball." cmp --silent "$srctarball" "$PROJECT_ROOT/dist/$srctarball" || fail "files are different. srctarball." cmp --silent "$appimage" "$PROJECT_ROOT/dist/$appimage" || fail "files are different. appimage." rm -rf "$CONTRIB/build-wine/signed/" && mkdir --parents "$CONTRIB/build-wine/signed/" cp -f "$win1" "$win2" "$win3" "$CONTRIB/build-wine/signed/" "$CONTRIB/build-wine/unsign.sh" || fail "files are different. windows." "$CONTRIB/android/apkdiff.py" "$apk1" "$PROJECT_ROOT/dist/$apk1" || fail "files are different. android." "$CONTRIB/android/apkdiff.py" "$apk2" "$PROJECT_ROOT/dist/$apk2" || fail "files are different. android." "$CONTRIB/android/apkdiff.py" "$apk3" "$PROJECT_ROOT/dist/$apk3" || fail "files are different. android." cmp --silent "$dmg" "$PROJECT_ROOT/dist/$dmg" || fail "files are different. macos." # all files matched. sign them. rm -rf "$PROJECT_ROOT/dist/sigs/" mkdir --parents "$PROJECT_ROOT/dist/sigs/" for fname in "$tarball" "$srctarball" "$appimage" "$win1" "$win2" "$win3" "$apk1" "$apk2" "$apk3" "$dmg" ; do signame="$fname.$GPGUSER.asc" gpg --sign --armor --detach $PUBKEY --output "$PROJECT_ROOT/dist/sigs/$signame" "$fname" done if [ -z "$SSHUSER" ]; then info "Signing successfully, now open a pull request with your signatures to spesmilo/electrum-signatures" exit 0 else # upload sigs ELECBUILD_UPLOADFROM="$PROJECT_ROOT/dist/sigs/" "$CONTRIB/upload.sh" fi else # ONLY release manager cd "$PROJECT_ROOT" # check we have each binary test -f "$PROJECT_ROOT/dist/$tarball" || fail "tarball not found among built files" test -f "$PROJECT_ROOT/dist/$srctarball" || fail "srctarball not found among built files" test -f "$PROJECT_ROOT/dist/$appimage" || fail "appimage not found among built files" test -f "$PROJECT_ROOT/dist/$win1" || fail "win1 not found among built files" test -f "$PROJECT_ROOT/dist/$win2" || fail "win2 not found among built files" test -f "$PROJECT_ROOT/dist/$win3" || fail "win3 not found among built files" test -f "$PROJECT_ROOT/dist/$apk1" || fail "apk1 not found among built files" test -f "$PROJECT_ROOT/dist/$apk2" || fail "apk2 not found among built files" test -f "$PROJECT_ROOT/dist/$apk3" || fail "apk3 not found among built files" test -f "$PROJECT_ROOT/dist/$dmg" || fail "dmg not found among built files" if [ "$REV" != "$VERSION" ]; then fail "versions differ, not uploading" fi # upload the files ./contrib/upload.sh fi set +x info "release.sh finished successfully." info "After two people ran release.sh, the binaries will be publicly available on uploadserver." info "Then, we wait for additional signers, and run add_cosigner for them." info "Finally, release_www.sh needs to be run, for the website to be updated." ================================================ FILE: contrib/release_www.sh ================================================ #!/bin/bash # # env vars: # - WWW_DIR: path to "electrum-web" git clone # - for signing the version announcement file: # - ELECTRUM_SIGNING_ADDRESS (required) # - ELECTRUM_SIGNING_WALLET (required) # set -e PROJECT_ROOT="$(dirname "$(readlink -e "$0")")/.." CONTRIB="$PROJECT_ROOT/contrib" cd "$PROJECT_ROOT" . "$CONTRIB"/build_tools_util.sh echo -n "Remember to run add_cosigner to add any additional sigs. Continue (y/n)? " read answer if [ "$answer" != "y" ]; then echo "exit" exit 1 fi if [ -z "$WWW_DIR" ] ; then WWW_DIR=/opt/electrum-web fi if [ -z "$ELECTRUM_SIGNING_WALLET" ] || [ -z "$ELECTRUM_SIGNING_ADDRESS" ]; then echo "You need to set env vars ELECTRUM_SIGNING_WALLET and ELECTRUM_SIGNING_ADDRESS!" exit 1 fi VERSION=$("$CONTRIB"/print_electrum_version.py) info "VERSION: $VERSION" ANDROID_VERSIONCODE_NULLARCH=$("$CONTRIB"/android/get_apk_versioncode.py "null") # ^ note: should parse as an integer in the final json info "ANDROID_VERSIONCODE_NULLARCH: $ANDROID_VERSIONCODE_NULLARCH" set -x info "updating www repo" ./contrib/make_download "$WWW_DIR" info "signing the version announcement file" sig=$(./run_electrum -o signmessage "$ELECTRUM_SIGNING_ADDRESS" "$VERSION" -w "$ELECTRUM_SIGNING_WALLET") # note: the contents of "extradata" are currently not signed. We could add another field, extradata_sigs, # containing signature(s) for "extradata". extradata, being json, would have to be canonically # serialized before signing. cat < "$WWW_DIR"/version { "version": "$VERSION", "signatures": {"$ELECTRUM_SIGNING_ADDRESS": "$sig"}, "extradata": { "android_versioncode_nullarch": $ANDROID_VERSIONCODE_NULLARCH } } EOF # push changes to website repo pushd "$WWW_DIR" git diff git commit -a -m "version $VERSION" git push popd info "release_www.sh finished successfully." info "now you should run WWW_DIR/publish.sh to sign the website commit and upload signature" ================================================ FILE: contrib/requirements/requirements-binaries-mac.txt ================================================ # Qt 6.8 would require macOS 12+, 6.7 still supports macOS 11 # Qt 6.7 has issue "No QtMultimedia backends found." (i.e. camera does not work) # PyQt6-Qt6==6.6.3 segfaults with "illegal hardware instruction" PyQt6<6.7 PyQt6-Qt6<6.7,!=6.6.3 cryptography>=2.6 ================================================ FILE: contrib/requirements/requirements-binaries.txt ================================================ PyQt6 # we need at least cryptography>=2.1 for electrum.crypto, # and at least cryptography>=2.6 for dnspython[DNSSEC] cryptography>=2.6 ================================================ FILE: contrib/requirements/requirements-build-android.txt ================================================ pip setuptools wheel # needed by buildozer: pexpect sh # some p4a recipes don't work with cython 3+ cython<3.0 # needed by python-for-android: appdirs # colorama upper bound to avoid needing hatchling colorama>=0.3.3,<0.4.6 jinja2 sh>=1.10 pep517 toml # needed for the Qt/QML Android GUI: # TODO double-check this typing-extensions ================================================ FILE: contrib/requirements/requirements-build-appimage.txt ================================================ pip setuptools wheel # Note: hidapi requires Cython at build-time (not needed at runtime). # For reproducible builds, the version of Cython must be pinned down. # The pinned Cython must be installed before hidapi is built; # otherwise when installing hidapi, pip just downloads the latest Cython. # see https://github.com/spesmilo/electrum/issues/5859 Cython>=0.27 ================================================ FILE: contrib/requirements/requirements-build-base.txt ================================================ # This file contains build-time dependencies needed to build other higher level build-time dependencies # and runtime dependencies. # For reproducibility, some build-time deps, most notably "wheel", need to be pinned. (see #7640) # By default, when doing e.g. "pip install", pip downloads the latest version of wheel (and setuptools, etc), # regardless whether a sufficiently recent version of wheel is already installed locally... # The only way I have found to avoid this, is to use the "--no-build-isolation" flag, # in which case it becomes our responsibility to install *all* build time deps... pip setuptools wheel # importlib_metadata also needs: # https://github.com/python/importlib_metadata/blob/1e2381fe101fd70742a0171e51c1be82aedf519b/pyproject.toml#L2 setuptools_scm[toml]>=3.4.1 # from https://github.com/pypa/setuptools-scm/commit/c766df10c18c3c5a6b5741e9f372e193412c0f69 : # (but also to avoid the binary wheels introduced in tomli 2.2) tomli<=2.0.2 # dnspython also needs: # https://github.com/rthalley/dnspython/blob/1a7c14fb6c200be02ef5c2f3bb9fd84b85004459/pyproject.toml#L64 poetry-core # typing-extensions also needs: # https://github.com/python/typing/blob/a2371460d184c96aab7a69acc47fd059f875e3b4/typing_extensions/pyproject.toml#L3 flit_core>=3.4,<4 # aio-libs/frozenlist and aio-libs/propcache needs: # https://github.com/aio-libs/frozenlist/blob/c28f32d6816ca0fa56a5876e84831c46084bb85d/pyproject.toml#L6 expandvars ================================================ FILE: contrib/requirements/requirements-build-mac.txt ================================================ pip setuptools wheel # needed by pyinstaller: # fixme: ugly to have to duplicate this here from upstream macholib>=1.8 altgraph pyinstaller-hooks-contrib>=2025.2 packaging>=22.0 # Note: hidapi requires Cython at build-time (not needed at runtime). # For reproducible builds, the version of Cython must be pinned down. # The pinned Cython must be installed before hidapi is built; # otherwise when installing hidapi, pip just downloads the latest Cython. # see https://github.com/spesmilo/electrum/issues/5859 Cython>=0.27 ================================================ FILE: contrib/requirements/requirements-build-wine.txt ================================================ pip setuptools wheel # needed by pyinstaller: # fixme: ugly to have to duplicate this here from upstream pefile>=2022.5.30,!=2024.8.26 altgraph pywin32-ctypes>=0.2.1 pyinstaller-hooks-contrib>=2025.2 packaging>=22.0 ================================================ FILE: contrib/requirements/requirements-ci.txt ================================================ pytest coverage coveralls ================================================ FILE: contrib/requirements/requirements-hw.txt ================================================ hidapi # device plugin: trezor trezor[hidapi]>=0.13.0,<0.14 # device plugin: safe_t safet>=0.1.5 # device plugin: keepkey ecdsa>=0.9 protobuf>=3.20 mnemonic>=0.8 hidapi>=0.7.99.post15 libusb1>=1.6 # device plugin: ledger ledger-bitcoin>=0.2.0,<1.0 hidapi # device plugin: coldcard ckcc-protocol>=0.7.7 # device plugin: bitbox02 bitbox02>=7.0.0 # device plugin: jade cbor2>=5.4.6,<6.0.0 pyserial>=3.5.0,<4.0.0 # prefer older urllib3 to avoid needing hatchling # (pulled in via trezor -> requests -> urllib3) urllib3<2 ================================================ FILE: contrib/requirements/requirements.txt ================================================ qrcode protobuf>=3.20 qdarkstyle>=3.2 aiorpcx>=0.25.0,<0.26 aiohttp>=3.11.0,<4.0.0 aiohttp_socks>=0.9.2 certifi jsonpatch electrum_ecc>=0.0.4,<0.1 electrum_aionostr>=0.1.0,<0.2 # - upper limit to avoid needing hatchling at build-time :/ # (however newer versions should work at runtime) attrs>=20.1.0,<23 # Note that we also need the dnspython[DNSSEC] extra which pulls in cryptography, # but as that is not pure-python it cannot be listed in this file! # - upper limit to avoid needing hatchling at build-time :/ # (however newer versions should work at runtime) dnspython>=2.2,<2.5 ================================================ FILE: contrib/sign_packages ================================================ #!/usr/bin/env python3 import os, sys if __name__ == '__main__': username = sys.argv[1] os.chdir("dist") for fname in os.listdir('.'): if fname.endswith('asc'): continue sig_name = fname + '.' + username + '.asc' os.system(f"gpg --sign --armor --detach --output {sig_name} {fname}") os.chdir("..") ================================================ FILE: contrib/trigger_deploy.sh ================================================ #!/bin/bash # Triggers deploy.sh to maybe update the website or move binaries. # uploadserver needs to be defined in /etc/hosts SSHUSER=$1 TRIGGERVERSION=$2 if [ -z "$SSHUSER" ] || [ -z "$TRIGGERVERSION" ]; then echo "usage: $0 SSHUSER TRIGGERVERSION" echo "e.g. $0 thomasv 3.0.0" echo "e.g. $0 thomasv website" exit 1 fi set -ex cd "$(dirname "$0")" if [ "$TRIGGERVERSION" == "website" ]; then rm -f trigger_website touch trigger_website echo "uploading file: trigger_website..." sftp -oBatchMode=no -b - "$SSHUSER@uploadserver" << ! cd electrum-downloads-airlock mput trigger_website bye ! else rm -f trigger_binaries printf "$TRIGGERVERSION" > trigger_binaries echo "uploading file: trigger_binaries..." sftp -oBatchMode=no -b - "$SSHUSER@uploadserver" << ! cd electrum-downloads-airlock mput trigger_binaries bye ! fi ================================================ FILE: contrib/udev/20-hw1.rules ================================================ # HW.1, Nano SUBSYSTEMS=="usb", ATTRS{idVendor}=="2581", ATTRS{idProduct}=="1b7c|2b7c|3b7c|4b7c", TAG+="uaccess", TAG+="udev-acl" # Blue, NanoS, Aramis, HW.2, Nano X, NanoSP, Stax, Ledger Test, SUBSYSTEMS=="usb", ATTRS{idVendor}=="2c97", TAG+="uaccess", TAG+="udev-acl" # Same, but with hidraw-based library (instead of libusb) KERNEL=="hidraw*", ATTRS{idVendor}=="2c97", MODE="0666" ================================================ FILE: contrib/udev/51-coinkite.rules ================================================ # Linux udev support file. # # This is a example udev file for HIDAPI devices which changes the permissions # to 0666 (world readable/writable) for a specific device on Linux systems. # # - Copy this file into /etc/udev/rules.d and unplug and re-plug your Coldcard. # - Udev does not have to be restarted. # # probably not needed: SUBSYSTEMS=="usb", ATTRS{idVendor}=="d13e", ATTRS{idProduct}=="cc10", GROUP="plugdev", MODE="0666" # required: # from KERNEL=="hidraw*", ATTRS{idVendor}=="d13e", ATTRS{idProduct}=="cc10", GROUP="plugdev", MODE="0666" ================================================ FILE: contrib/udev/51-hid-digitalbitbox.rules ================================================ SUBSYSTEM=="usb", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="dbb%n", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2402" ================================================ FILE: contrib/udev/51-safe-t.rules ================================================ # Put this file into /usr/lib/udev/rules.d or /etc/udev/rules.d # Archos Safe-T mini SUBSYSTEM=="usb", ATTR{idVendor}=="0e79", ATTR{idProduct}=="6000", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="safe-tr%n" KERNEL=="hidraw*", ATTRS{idVendor}=="0e79", ATTRS{idProduct}=="6000", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl" # Archos Safe-T mini Bootloader SUBSYSTEM=="usb", ATTR{idVendor}=="0e79", ATTR{idProduct}=="6001", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="safe-t%n" KERNEL=="hidraw*", ATTRS{idVendor}=="0e79", ATTRS{idProduct}=="6001", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl" ================================================ FILE: contrib/udev/51-trezor.rules ================================================ # Trezor: The Original Hardware Wallet # https://trezor.io/ # # Put this file into /etc/udev/rules.d # # If you are creating a distribution package, # put this into /usr/lib/udev/rules.d or /lib/udev/rules.d # depending on your distribution # Trezor SUBSYSTEM=="usb", ATTR{idVendor}=="534c", ATTR{idProduct}=="0001", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="trezor%n" KERNEL=="hidraw*", ATTRS{idVendor}=="534c", ATTRS{idProduct}=="0001", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl" # Trezor v2 SUBSYSTEM=="usb", ATTR{idVendor}=="1209", ATTR{idProduct}=="53c0", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="trezor%n" SUBSYSTEM=="usb", ATTR{idVendor}=="1209", ATTR{idProduct}=="53c1", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="trezor%n" KERNEL=="hidraw*", ATTRS{idVendor}=="1209", ATTRS{idProduct}=="53c1", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl" ================================================ FILE: contrib/udev/51-usb-keepkey.rules ================================================ # KeepKey: Your Private Bitcoin Vault # http://www.keepkey.com/ # Put this file into /usr/lib/udev/rules.d or /etc/udev/rules.d # KeepKey HID Firmware/Bootloader SUBSYSTEM=="usb", ATTR{idVendor}=="2b24", ATTR{idProduct}=="0001", MODE="0666", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="keepkey%n" KERNEL=="hidraw*", ATTRS{idVendor}=="2b24", ATTRS{idProduct}=="0001", MODE="0666", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl" # KeepKey WebUSB Firmware/Bootloader SUBSYSTEM=="usb", ATTR{idVendor}=="2b24", ATTR{idProduct}=="0002", MODE="0666", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="keepkey%n" KERNEL=="hidraw*", ATTRS{idVendor}=="2b24", ATTRS{idProduct}=="0002", MODE="0666", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl" ================================================ FILE: contrib/udev/52-hid-digitalbitbox.rules ================================================ KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2402", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="dbbf%n" ================================================ FILE: contrib/udev/53-hid-bitbox02.rules ================================================ SUBSYSTEM=="usb", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="bitbox02_%n", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2403" ================================================ FILE: contrib/udev/54-hid-bitbox02.rules ================================================ KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2403", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="bitbox02-%n" ================================================ FILE: contrib/udev/55-usb-jade.rules ================================================ KERNEL=="ttyUSB*", SUBSYSTEMS=="usb", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="jade%n" KERNEL=="ttyACM*", SUBSYSTEMS=="usb", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="55d4", MODE="0660", GROUP="plugdev", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="jade%n" ================================================ FILE: contrib/udev/README.md ================================================ # udev rules This directory contains all of the udev rules for the supported devices as retrieved from vendor websites and repositories. These are necessary for the devices to be usable on Linux environments. - `20-hw1.rules` (Ledger): https://github.com/LedgerHQ/udev-rules/blob/master/20-hw1.rules - `51-coinkite.rules` (Coldcard): https://github.com/Coldcard/ckcc-protocol/blob/master/51-coinkite.rules - `51-hid-digitalbitbox.rules`, `52-hid-digitalbitbox.rules` (Digital Bitbox): https://github.com/digitalbitbox/bitbox-wallet-app/blob/master/frontends/qt/resources/deb-afterinstall.sh - `53-hid-bitbox02.rules`, `54-hid-bitbox02.rules` (BitBox02): https://github.com/digitalbitbox/bitbox-wallet-app/blob/master/frontends/qt/resources/deb-afterinstall.sh - `51-trezor.rules` (Trezor): https://github.com/trezor/trezor-common/blob/master/udev/51-trezor.rules - `51-usb-keepkey.rules` (Keepkey): https://github.com/keepkey/udev-rules/blob/master/51-usb-keepkey.rules - `51-safe-t.rules` (Archos): https://github.com/archos-safe-t/safe-t-common/blob/master/udev/51-safe-t.rules - `55-usb-jade.rules` (Blockstream Jade): https://github.com/Blockstream/Jade # Usage Apply these rules by copying them to `/etc/udev/rules.d/` and notifying `udevadm`. Your user will need to be added to the `plugdev` group, which needs to be created if it does not already exist. ``` $ sudo groupadd plugdev $ sudo usermod -aG plugdev $(whoami) $ sudo cp contrib/udev/*.rules /etc/udev/rules.d/ $ sudo udevadm control --reload-rules && sudo udevadm trigger ``` ================================================ FILE: contrib/upload.sh ================================================ #!/bin/bash # uploadserver is set in /etc/hosts # # env vars: # - ELECBUILD_UPLOADFROM # - SSHUSER set -ex PROJECT_ROOT="$(dirname "$(readlink -e "$0")")/.." CONTRIB="$PROJECT_ROOT/contrib" if [ -z "$SSHUSER" ]; then SSHUSER=thomasv fi cd "$PROJECT_ROOT" VERSION=$("$CONTRIB"/print_electrum_version.py) echo "$VERSION" if [ -z "$ELECBUILD_UPLOADFROM" ]; then cd "$PROJECT_ROOT/dist" else cd "$ELECBUILD_UPLOADFROM" fi # do not fail sftp if directory exists # see https://stackoverflow.com/questions/51437924/bash-shell-sftp-check-if-directory-exists-before-creating sftp -oBatchMode=no -b - "$SSHUSER@uploadserver" << ! cd electrum-downloads-airlock -mkdir "$VERSION" -chmod 777 "$VERSION" cd "$VERSION" -mput * -chmod 444 * # this prevents future re-uploads of same file bye ! "$CONTRIB/trigger_deploy.sh" "$SSHUSER" "$VERSION" ================================================ FILE: electrum/__init__.py ================================================ import sys import os # these are ~duplicated from run_electrum: is_bundle = getattr(sys, 'frozen', False) is_local = not is_bundle and os.path.exists(os.path.join(os.path.dirname(os.path.dirname(__file__)), "electrum.desktop")) # when running from source, on Windows, also search for DLLs in inner 'electrum' folder if is_local and os.name == 'nt': # fixme: duplicated between main script and __init__.py :( os.add_dll_directory(os.path.dirname(__file__)) class GuiImportError(ImportError): pass from .version import ELECTRUM_VERSION from .util import format_satoshis from .wallet import Wallet from .storage import WalletStorage from .coinchooser import COIN_CHOOSERS from .network import Network, pick_random_server from .interface import Interface from .simple_config import SimpleConfig from . import bitcoin from . import transaction from . import daemon from .transaction import Transaction from .plugin import BasePlugin from .commands import Commands, known_commands from .logging import get_logger __version__ = ELECTRUM_VERSION _logger = get_logger(__name__) # Ensure that asserts are enabled. For sanity and paranoia, we require this. # Code *should not rely* on asserts being enabled. In particular, safety and security checks should # always explicitly raise exceptions. However, this rule is mistakenly broken occasionally... try: assert False # noqa: B011 except AssertionError: pass else: raise ImportError("Running with asserts disabled. Refusing to continue. Exiting...") # Check that os.urandom works import zlib length = len(zlib.compress(os.urandom(1000))) if length <= 900: raise ImportError("Broken PRNG. Refusing to continue. Exiting...") ================================================ FILE: electrum/_vendor/__init__.py ================================================ ================================================ FILE: electrum/_vendor/distutils/LICENSE ================================================ A. HISTORY OF THE SOFTWARE ========================== Python was created in the early 1990s by Guido van Rossum at Stichting Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands as a successor of a language called ABC. Guido remains Python's principal author, although it includes many contributions from others. In 1995, Guido continued his work on Python at the Corporation for National Research Initiatives (CNRI, see http://www.cnri.reston.va.us) in Reston, Virginia where he released several versions of the software. In May 2000, Guido and the Python core development team moved to BeOpen.com to form the BeOpen PythonLabs team. In October of the same year, the PythonLabs team moved to Digital Creations, which became Zope Corporation. In 2001, the Python Software Foundation (PSF, see https://www.python.org/psf/) was formed, a non-profit organization created specifically to own Python-related Intellectual Property. Zope Corporation was a sponsoring member of the PSF. All Python releases are Open Source (see http://www.opensource.org for the Open Source Definition). Historically, most, but not all, Python releases have also been GPL-compatible; the table below summarizes the various releases. Release Derived Year Owner GPL- from compatible? (1) 0.9.0 thru 1.2 1991-1995 CWI yes 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes 1.6 1.5.2 2000 CNRI no 2.0 1.6 2000 BeOpen.com no 1.6.1 1.6 2001 CNRI yes (2) 2.1 2.0+1.6.1 2001 PSF no 2.0.1 2.0+1.6.1 2001 PSF yes 2.1.1 2.1+2.0.1 2001 PSF yes 2.1.2 2.1.1 2002 PSF yes 2.1.3 2.1.2 2002 PSF yes 2.2 and above 2.1.1 2001-now PSF yes Footnotes: (1) GPL-compatible doesn't mean that we're distributing Python under the GPL. All Python licenses, unlike the GPL, let you distribute a modified version without making your changes open source. The GPL-compatible licenses make it possible to combine Python with other software that is released under the GPL; the others don't. (2) According to Richard Stallman, 1.6.1 is not GPL-compatible, because its license has a choice of law clause. According to CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 is "not incompatible" with the GPL. Thanks to the many outside volunteers who have worked under Guido's direction to make these releases possible. B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON =============================================================== Python software and documentation are licensed under the Python Software Foundation License Version 2. Starting with Python 3.8.6, examples, recipes, and other code in the documentation are dual licensed under the PSF License Version 2 and the Zero-Clause BSD license. Some software incorporated into Python is under different licenses. The licenses are listed with code falling under that license. PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 -------------------------------------------- 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization ("Licensee") accessing and otherwise using this software ("Python") in source or binary form and its associated documentation. 2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022 Python Software Foundation; All Rights Reserved" are retained in Python alone or in any derivative version prepared by Licensee. 3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in any such work a brief summary of the changes made to Python. 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions. 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade name in a trademark sense to endorse or promote products or services of Licensee, or any third party. 8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this License Agreement. BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 ------------------------------------------- BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the Individual or Organization ("Licensee") accessing and otherwise using this software in source or binary form and its associated documentation ("the Software"). 2. Subject to the terms and conditions of this BeOpen Python License Agreement, BeOpen hereby grants Licensee a non-exclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use the Software alone or in any derivative version, provided, however, that the BeOpen Python License is retained in the Software, alone or in any derivative version prepared by Licensee. 3. BeOpen is making the Software available to Licensee on an "AS IS" basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. 4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 5. This License Agreement will automatically terminate upon a material breach of its terms and conditions. 6. This License Agreement shall be governed by and interpreted in all respects by the law of the State of California, excluding conflict of law provisions. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between BeOpen and Licensee. This License Agreement does not grant permission to use BeOpen trademarks or trade names in a trademark sense to endorse or promote products or services of Licensee, or any third party. As an exception, the "BeOpen Python" logos available at http://www.pythonlabs.com/logos.html may be used according to the permissions granted on that web page. 7. By copying, installing or otherwise using the software, Licensee agrees to be bound by the terms and conditions of this License Agreement. CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 --------------------------------------- 1. This LICENSE AGREEMENT is between the Corporation for National Research Initiatives, having an office at 1895 Preston White Drive, Reston, VA 20191 ("CNRI"), and the Individual or Organization ("Licensee") accessing and otherwise using Python 1.6.1 software in source or binary form and its associated documentation. 2. Subject to the terms and conditions of this License Agreement, CNRI hereby grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use Python 1.6.1 alone or in any derivative version, provided, however, that CNRI's License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) 1995-2001 Corporation for National Research Initiatives; All Rights Reserved" are retained in Python 1.6.1 alone or in any derivative version prepared by Licensee. Alternately, in lieu of CNRI's License Agreement, Licensee may substitute the following text (omitting the quotes): "Python 1.6.1 is made available subject to the terms and conditions in CNRI's License Agreement. This Agreement together with Python 1.6.1 may be located on the internet using the following unique, persistent identifier (known as a handle): 1895.22/1013. This Agreement may also be obtained from a proxy server on the internet using the following URL: http://hdl.handle.net/1895.22/1013". 3. In the event Licensee prepares a derivative work that is based on or incorporates Python 1.6.1 or any part thereof, and wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in any such work a brief summary of the changes made to Python 1.6.1. 4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. 5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON 1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions. 7. This License Agreement shall be governed by the federal intellectual property law of the United States, including without limitation the federal copyright law, and, to the extent such U.S. federal law does not apply, by the law of the Commonwealth of Virginia, excluding Virginia's conflict of law provisions. Notwithstanding the foregoing, with regard to derivative works based on Python 1.6.1 that incorporate non-separable material that was previously distributed under the GNU General Public License (GPL), the law of the Commonwealth of Virginia shall govern this License Agreement only as to issues arising under or with respect to Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between CNRI and Licensee. This License Agreement does not grant permission to use CNRI trademarks or trade name in a trademark sense to endorse or promote products or services of Licensee, or any third party. 8. By clicking on the "ACCEPT" button where indicated, or by copying, installing or otherwise using Python 1.6.1, Licensee agrees to be bound by the terms and conditions of this License Agreement. ACCEPT CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 -------------------------------------------------- Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, The Netherlands. All rights reserved. Permission to use, copy, modify, and distribute this software and its documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of Stichting Mathematisch Centrum or CWI not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION ---------------------------------------------------------------------- Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ================================================ FILE: electrum/_vendor/distutils/__init__.py ================================================ """(part of) distutils, taken from the cpython standard library at commit https://github.com/python/cpython/tree/9d38120e335357a3b294277fd5eff0a10e46e043/Lib/distutils """ ================================================ FILE: electrum/_vendor/distutils/version.py ================================================ # # distutils/version.py # # Implements multiple version numbering conventions for the # Python Module Distribution Utilities. # # $Id$ # """Provides classes to represent module version numbers (one class for each style of version numbering). There are currently two such classes implemented: StrictVersion and LooseVersion. Every version number class implements the following interface: * the 'parse' method takes a string and parses it to some internal representation; if the string is an invalid version number, 'parse' raises a ValueError exception * the class constructor takes an optional string argument which, if supplied, is passed to 'parse' * __str__ reconstructs the string that was passed to 'parse' (or an equivalent string -- ie. one that will generate an equivalent version number instance) * __repr__ generates Python code to recreate the version number instance * _cmp compares the current instance with either another instance of the same class or a string (which will be parsed to an instance of the same class, thus must follow the same rules) """ import re class Version: """Abstract base class for version numbering classes. Just provides constructor (__init__) and reproducer (__repr__), because those seem to be the same for all version numbering classes; and route rich comparisons to _cmp. """ def __init__ (self, vstring=None): if vstring: self.parse(vstring) def __repr__ (self): return "%s ('%s')" % (self.__class__.__name__, str(self)) def __eq__(self, other): c = self._cmp(other) if c is NotImplemented: return c return c == 0 def __lt__(self, other): c = self._cmp(other) if c is NotImplemented: return c return c < 0 def __le__(self, other): c = self._cmp(other) if c is NotImplemented: return c return c <= 0 def __gt__(self, other): c = self._cmp(other) if c is NotImplemented: return c return c > 0 def __ge__(self, other): c = self._cmp(other) if c is NotImplemented: return c return c >= 0 # Interface for version-number classes -- must be implemented # by the following classes (the concrete ones -- Version should # be treated as an abstract class). # __init__ (string) - create and take same action as 'parse' # (string parameter is optional) # parse (string) - convert a string representation to whatever # internal representation is appropriate for # this style of version numbering # __str__ (self) - convert back to a string; should be very similar # (if not identical to) the string supplied to parse # __repr__ (self) - generate Python code to recreate # the instance # _cmp (self, other) - compare two version numbers ('other' may # be an unparsed version string, or another # instance of your version class) class StrictVersion (Version): """Version numbering for anal retentives and software idealists. Implements the standard interface for version number classes as described above. A version number consists of two or three dot-separated numeric components, with an optional "pre-release" tag on the end. The pre-release tag consists of the letter 'a' or 'b' followed by a number. If the numeric components of two version numbers are equal, then one with a pre-release tag will always be deemed earlier (lesser) than one without. The following are valid version numbers (shown in the order that would be obtained by sorting according to the supplied cmp function): 0.4 0.4.0 (these two are equivalent) 0.4.1 0.5a1 0.5b3 0.5 0.9.6 1.0 1.0.4a3 1.0.4b1 1.0.4 The following are examples of invalid version numbers: 1 2.7.2.2 1.3.a4 1.3pl1 1.3c4 The rationale for this version numbering system will be explained in the distutils documentation. """ version_re = re.compile(r'^(\d+) \. (\d+) (\. (\d+))? ([ab](\d+))?$', re.VERBOSE | re.ASCII) def parse (self, vstring): match = self.version_re.match(vstring) if not match: raise ValueError("invalid version number '%s'" % vstring) (major, minor, patch, prerelease, prerelease_num) = \ match.group(1, 2, 4, 5, 6) if patch: self.version = tuple(map(int, [major, minor, patch])) else: self.version = tuple(map(int, [major, minor])) + (0,) if prerelease: self.prerelease = (prerelease[0], int(prerelease_num)) else: self.prerelease = None def __str__ (self): if self.version[2] == 0: vstring = '.'.join(map(str, self.version[0:2])) else: vstring = '.'.join(map(str, self.version)) if self.prerelease: vstring = vstring + self.prerelease[0] + str(self.prerelease[1]) return vstring def _cmp (self, other): if isinstance(other, str): other = StrictVersion(other) elif not isinstance(other, StrictVersion): return NotImplemented if self.version != other.version: # numeric versions don't match # prerelease stuff doesn't matter if self.version < other.version: return -1 else: return 1 # have to compare prerelease # case 1: neither has prerelease; they're equal # case 2: self has prerelease, other doesn't; other is greater # case 3: self doesn't have prerelease, other does: self is greater # case 4: both have prerelease: must compare them! if (not self.prerelease and not other.prerelease): return 0 elif (self.prerelease and not other.prerelease): return -1 elif (not self.prerelease and other.prerelease): return 1 elif (self.prerelease and other.prerelease): if self.prerelease == other.prerelease: return 0 elif self.prerelease < other.prerelease: return -1 else: return 1 else: assert False, "never get here" # end class StrictVersion # The rules according to Greg Stein: # 1) a version number has 1 or more numbers separated by a period or by # sequences of letters. If only periods, then these are compared # left-to-right to determine an ordering. # 2) sequences of letters are part of the tuple for comparison and are # compared lexicographically # 3) recognize the numeric components may have leading zeroes # # The LooseVersion class below implements these rules: a version number # string is split up into a tuple of integer and string components, and # comparison is a simple tuple comparison. This means that version # numbers behave in a predictable and obvious way, but a way that might # not necessarily be how people *want* version numbers to behave. There # wouldn't be a problem if people could stick to purely numeric version # numbers: just split on period and compare the numbers as tuples. # However, people insist on putting letters into their version numbers; # the most common purpose seems to be: # - indicating a "pre-release" version # ('alpha', 'beta', 'a', 'b', 'pre', 'p') # - indicating a post-release patch ('p', 'pl', 'patch') # but of course this can't cover all version number schemes, and there's # no way to know what a programmer means without asking him. # # The problem is what to do with letters (and other non-numeric # characters) in a version number. The current implementation does the # obvious and predictable thing: keep them as strings and compare # lexically within a tuple comparison. This has the desired effect if # an appended letter sequence implies something "post-release": # eg. "0.99" < "0.99pl14" < "1.0", and "5.001" < "5.001m" < "5.002". # # However, if letters in a version number imply a pre-release version, # the "obvious" thing isn't correct. Eg. you would expect that # "1.5.1" < "1.5.2a2" < "1.5.2", but under the tuple/lexical comparison # implemented here, this just isn't so. # # Two possible solutions come to mind. The first is to tie the # comparison algorithm to a particular set of semantic rules, as has # been done in the StrictVersion class above. This works great as long # as everyone can go along with bondage and discipline. Hopefully a # (large) subset of Python module programmers will agree that the # particular flavour of bondage and discipline provided by StrictVersion # provides enough benefit to be worth using, and will submit their # version numbering scheme to its domination. The free-thinking # anarchists in the lot will never give in, though, and something needs # to be done to accommodate them. # # Perhaps a "moderately strict" version class could be implemented that # lets almost anything slide (syntactically), and makes some heuristic # assumptions about non-digits in version number strings. This could # sink into special-case-hell, though; if I was as talented and # idiosyncratic as Larry Wall, I'd go ahead and implement a class that # somehow knows that "1.2.1" < "1.2.2a2" < "1.2.2" < "1.2.2pl3", and is # just as happy dealing with things like "2g6" and "1.13++". I don't # think I'm smart enough to do it right though. # # In any case, I've coded the test suite for this module (see # ../test/test_version.py) specifically to fail on things like comparing # "1.2a2" and "1.2". That's not because the *code* is doing anything # wrong, it's because the simple, obvious design doesn't match my # complicated, hairy expectations for real-world version numbers. It # would be a snap to fix the test suite to say, "Yep, LooseVersion does # the Right Thing" (ie. the code matches the conception). But I'd rather # have a conception that matches common notions about version numbers. class LooseVersion (Version): """Version numbering for anarchists and software realists. Implements the standard interface for version number classes as described above. A version number consists of a series of numbers, separated by either periods or strings of letters. When comparing version numbers, the numeric components will be compared numerically, and the alphabetic components lexically. The following are all valid version numbers, in no particular order: 1.5.1 1.5.2b2 161 3.10a 8.02 3.4j 1996.07.12 3.2.pl0 3.1.1.6 2g6 11g 0.960923 2.2beta29 1.13++ 5.5.kw 2.0b1pl0 In fact, there is no such thing as an invalid version number under this scheme; the rules for comparison are simple and predictable, but may not always give the results you want (for some definition of "want"). """ component_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE) def __init__ (self, vstring=None): if vstring: self.parse(vstring) def parse (self, vstring): # I've given up on thinking I can reconstruct the version string # from the parsed tuple -- so I just store the string here for # use by __str__ self.vstring = vstring components = [x for x in self.component_re.split(vstring) if x and x != '.'] for i, obj in enumerate(components): try: components[i] = int(obj) except ValueError: pass self.version = components def __str__ (self): return self.vstring def __repr__ (self): return "LooseVersion ('%s')" % str(self) def _cmp (self, other): if isinstance(other, str): other = LooseVersion(other) elif not isinstance(other, LooseVersion): return NotImplemented if self.version == other.version: return 0 if self.version < other.version: return -1 if self.version > other.version: return 1 # end class LooseVersion ================================================ FILE: electrum/_vendor/pyperclip/LICENSE.txt ================================================ Copyright (c) 2014, Al Sweigart All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the {organization} nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: electrum/_vendor/pyperclip/README.md ================================================ This is a stripped-down copy of the 3rd-party `pyperclip` package. It is used by the "text" GUI. At revision https://github.com/asweigart/pyperclip/blob/781603ea491eefce3b58f4f203bf748dbf9ff003/src/pyperclip/__init__.py (version 1.8.2) Modifications: - excluded most files - added support for pyqt6 ================================================ FILE: electrum/_vendor/pyperclip/__init__.py ================================================ """ Pyperclip A cross-platform clipboard module for Python, with copy & paste functions for plain text. By Al Sweigart al@inventwithpython.com BSD License Usage: import pyperclip pyperclip.copy('The text to be copied to the clipboard.') spam = pyperclip.paste() if not pyperclip.is_available(): print("Copy functionality unavailable!") On Windows, no additional modules are needed. On Mac, the pyobjc module is used, falling back to the pbcopy and pbpaste cli commands. (These commands should come with OS X.). On Linux, install xclip, xsel, or wl-clipboard (for "wayland" sessions) via package manager. For example, in Debian: sudo apt-get install xclip sudo apt-get install xsel sudo apt-get install wl-clipboard Otherwise on Linux, you will need the gtk or PyQt5/PyQt4 modules installed. gtk and PyQt4 modules are not available for Python 3, and this module does not work with PyGObject yet. Note: There seems to be a way to get gtk on Python 3, according to: https://askubuntu.com/questions/697397/python3-is-not-supporting-gtk-module Cygwin is currently not supported. Security Note: This module runs programs with these names: - which - where - pbcopy - pbpaste - xclip - xsel - wl-copy/wl-paste - klipper - qdbus A malicious user could rename or add programs with these names, tricking Pyperclip into running them with whatever permissions the Python process has. """ __version__ = '1.8.2' import contextlib import ctypes import os import platform import subprocess import sys import time import warnings from ctypes import c_size_t, sizeof, c_wchar_p, get_errno, c_wchar # `import PyQt4` sys.exit()s if DISPLAY is not in the environment. # Thus, we need to detect the presence of $DISPLAY manually # and not load PyQt4 if it is absent. HAS_DISPLAY = os.getenv("DISPLAY", False) EXCEPT_MSG = """ Pyperclip could not find a copy/paste mechanism for your system. For more information, please visit https://pyperclip.readthedocs.io/en/latest/index.html#not-implemented-error """ PY2 = sys.version_info[0] == 2 STR_OR_UNICODE = unicode if PY2 else str # For paste(): Python 3 uses str, Python 2 uses unicode. ENCODING = 'utf-8' try: from shutil import which as _executable_exists except ImportError: # The "which" unix command finds where a command is. if platform.system() == 'Windows': WHICH_CMD = 'where' else: WHICH_CMD = 'which' def _executable_exists(name): return subprocess.call([WHICH_CMD, name], stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0 # Exceptions class PyperclipException(RuntimeError): pass class PyperclipWindowsException(PyperclipException): def __init__(self, message): message += " (%s)" % ctypes.WinError() super(PyperclipWindowsException, self).__init__(message) class PyperclipTimeoutException(PyperclipException): pass def _stringifyText(text): if PY2: acceptedTypes = (unicode, str, int, float, bool) else: acceptedTypes = (str, int, float, bool) if not isinstance(text, acceptedTypes): raise PyperclipException('only str, int, float, and bool values can be copied to the clipboard, not %s' % (text.__class__.__name__)) return STR_OR_UNICODE(text) def init_osx_pbcopy_clipboard(): def copy_osx_pbcopy(text): text = _stringifyText(text) # Converts non-str values to str. p = subprocess.Popen(['pbcopy', 'w'], stdin=subprocess.PIPE, close_fds=True) p.communicate(input=text.encode(ENCODING)) def paste_osx_pbcopy(): p = subprocess.Popen(['pbpaste', 'r'], stdout=subprocess.PIPE, close_fds=True) stdout, stderr = p.communicate() return stdout.decode(ENCODING) return copy_osx_pbcopy, paste_osx_pbcopy def init_osx_pyobjc_clipboard(): def copy_osx_pyobjc(text): '''Copy string argument to clipboard''' text = _stringifyText(text) # Converts non-str values to str. newStr = Foundation.NSString.stringWithString_(text).nsstring() newData = newStr.dataUsingEncoding_(Foundation.NSUTF8StringEncoding) board = AppKit.NSPasteboard.generalPasteboard() board.declareTypes_owner_([AppKit.NSStringPboardType], None) board.setData_forType_(newData, AppKit.NSStringPboardType) def paste_osx_pyobjc(): "Returns contents of clipboard" board = AppKit.NSPasteboard.generalPasteboard() content = board.stringForType_(AppKit.NSStringPboardType) return content return copy_osx_pyobjc, paste_osx_pyobjc def init_gtk_clipboard(): global gtk import gtk def copy_gtk(text): global cb text = _stringifyText(text) # Converts non-str values to str. cb = gtk.Clipboard() cb.set_text(text) cb.store() def paste_gtk(): clipboardContents = gtk.Clipboard().wait_for_text() # for python 2, returns None if the clipboard is blank. if clipboardContents is None: return '' else: return clipboardContents return copy_gtk, paste_gtk def init_qt_clipboard(): global QApplication # $DISPLAY should exist # Try to import from qtpy, but if that fails try PyQt5 then PyQt4 try: from qtpy.QtWidgets import QApplication except: try: from PyQt6.QtWidgets import QApplication except: try: from PyQt5.QtWidgets import QApplication except: from PyQt4.QtGui import QApplication app = QApplication.instance() if app is None: app = QApplication([]) def copy_qt(text): text = _stringifyText(text) # Converts non-str values to str. cb = app.clipboard() cb.setText(text) def paste_qt(): cb = app.clipboard() return STR_OR_UNICODE(cb.text()) return copy_qt, paste_qt def init_xclip_clipboard(): DEFAULT_SELECTION='c' PRIMARY_SELECTION='p' def copy_xclip(text, primary=False): text = _stringifyText(text) # Converts non-str values to str. selection=DEFAULT_SELECTION if primary: selection=PRIMARY_SELECTION p = subprocess.Popen(['xclip', '-selection', selection], stdin=subprocess.PIPE, close_fds=True) p.communicate(input=text.encode(ENCODING)) def paste_xclip(primary=False): selection=DEFAULT_SELECTION if primary: selection=PRIMARY_SELECTION p = subprocess.Popen(['xclip', '-selection', selection, '-o'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) stdout, stderr = p.communicate() # Intentionally ignore extraneous output on stderr when clipboard is empty return stdout.decode(ENCODING) return copy_xclip, paste_xclip def init_xsel_clipboard(): DEFAULT_SELECTION='-b' PRIMARY_SELECTION='-p' def copy_xsel(text, primary=False): text = _stringifyText(text) # Converts non-str values to str. selection_flag = DEFAULT_SELECTION if primary: selection_flag = PRIMARY_SELECTION p = subprocess.Popen(['xsel', selection_flag, '-i'], stdin=subprocess.PIPE, close_fds=True) p.communicate(input=text.encode(ENCODING)) def paste_xsel(primary=False): selection_flag = DEFAULT_SELECTION if primary: selection_flag = PRIMARY_SELECTION p = subprocess.Popen(['xsel', selection_flag, '-o'], stdout=subprocess.PIPE, close_fds=True) stdout, stderr = p.communicate() return stdout.decode(ENCODING) return copy_xsel, paste_xsel def init_wl_clipboard(): PRIMARY_SELECTION = "-p" def copy_wl(text, primary=False): text = _stringifyText(text) # Converts non-str values to str. args = ["wl-copy"] if primary: args.append(PRIMARY_SELECTION) if not text: args.append('--clear') subprocess.check_call(args, close_fds=True) else: pass p = subprocess.Popen(args, stdin=subprocess.PIPE, close_fds=True) p.communicate(input=text.encode(ENCODING)) def paste_wl(primary=False): args = ["wl-paste", "-n"] if primary: args.append(PRIMARY_SELECTION) p = subprocess.Popen(args, stdout=subprocess.PIPE, close_fds=True) stdout, _stderr = p.communicate() return stdout.decode(ENCODING) return copy_wl, paste_wl def init_klipper_clipboard(): def copy_klipper(text): text = _stringifyText(text) # Converts non-str values to str. p = subprocess.Popen( ['qdbus', 'org.kde.klipper', '/klipper', 'setClipboardContents', text.encode(ENCODING)], stdin=subprocess.PIPE, close_fds=True) p.communicate(input=None) def paste_klipper(): p = subprocess.Popen( ['qdbus', 'org.kde.klipper', '/klipper', 'getClipboardContents'], stdout=subprocess.PIPE, close_fds=True) stdout, stderr = p.communicate() # Workaround for https://bugs.kde.org/show_bug.cgi?id=342874 # TODO: https://github.com/asweigart/pyperclip/issues/43 clipboardContents = stdout.decode(ENCODING) # even if blank, Klipper will append a newline at the end assert len(clipboardContents) > 0 # make sure that newline is there assert clipboardContents.endswith('\n') if clipboardContents.endswith('\n'): clipboardContents = clipboardContents[:-1] return clipboardContents return copy_klipper, paste_klipper def init_dev_clipboard_clipboard(): def copy_dev_clipboard(text): text = _stringifyText(text) # Converts non-str values to str. if text == '': warnings.warn('Pyperclip cannot copy a blank string to the clipboard on Cygwin. This is effectively a no-op.') if '\r' in text: warnings.warn('Pyperclip cannot handle \\r characters on Cygwin.') fo = open('/dev/clipboard', 'wt') fo.write(text) fo.close() def paste_dev_clipboard(): fo = open('/dev/clipboard', 'rt') content = fo.read() fo.close() return content return copy_dev_clipboard, paste_dev_clipboard def init_no_clipboard(): class ClipboardUnavailable(object): def __call__(self, *args, **kwargs): raise PyperclipException(EXCEPT_MSG) if PY2: def __nonzero__(self): return False else: def __bool__(self): return False return ClipboardUnavailable(), ClipboardUnavailable() # Windows-related clipboard functions: class CheckedCall(object): def __init__(self, f): super(CheckedCall, self).__setattr__("f", f) def __call__(self, *args): ret = self.f(*args) if not ret and get_errno(): raise PyperclipWindowsException("Error calling " + self.f.__name__) return ret def __setattr__(self, key, value): setattr(self.f, key, value) def init_windows_clipboard(): global HGLOBAL, LPVOID, DWORD, LPCSTR, INT, HWND, HINSTANCE, HMENU, BOOL, UINT, HANDLE from ctypes.wintypes import (HGLOBAL, LPVOID, DWORD, LPCSTR, INT, HWND, HINSTANCE, HMENU, BOOL, UINT, HANDLE) windll = ctypes.windll msvcrt = ctypes.CDLL('msvcrt') safeCreateWindowExA = CheckedCall(windll.user32.CreateWindowExA) safeCreateWindowExA.argtypes = [DWORD, LPCSTR, LPCSTR, DWORD, INT, INT, INT, INT, HWND, HMENU, HINSTANCE, LPVOID] safeCreateWindowExA.restype = HWND safeDestroyWindow = CheckedCall(windll.user32.DestroyWindow) safeDestroyWindow.argtypes = [HWND] safeDestroyWindow.restype = BOOL OpenClipboard = windll.user32.OpenClipboard OpenClipboard.argtypes = [HWND] OpenClipboard.restype = BOOL safeCloseClipboard = CheckedCall(windll.user32.CloseClipboard) safeCloseClipboard.argtypes = [] safeCloseClipboard.restype = BOOL safeEmptyClipboard = CheckedCall(windll.user32.EmptyClipboard) safeEmptyClipboard.argtypes = [] safeEmptyClipboard.restype = BOOL safeGetClipboardData = CheckedCall(windll.user32.GetClipboardData) safeGetClipboardData.argtypes = [UINT] safeGetClipboardData.restype = HANDLE safeSetClipboardData = CheckedCall(windll.user32.SetClipboardData) safeSetClipboardData.argtypes = [UINT, HANDLE] safeSetClipboardData.restype = HANDLE safeGlobalAlloc = CheckedCall(windll.kernel32.GlobalAlloc) safeGlobalAlloc.argtypes = [UINT, c_size_t] safeGlobalAlloc.restype = HGLOBAL safeGlobalLock = CheckedCall(windll.kernel32.GlobalLock) safeGlobalLock.argtypes = [HGLOBAL] safeGlobalLock.restype = LPVOID safeGlobalUnlock = CheckedCall(windll.kernel32.GlobalUnlock) safeGlobalUnlock.argtypes = [HGLOBAL] safeGlobalUnlock.restype = BOOL wcslen = CheckedCall(msvcrt.wcslen) wcslen.argtypes = [c_wchar_p] wcslen.restype = UINT GMEM_MOVEABLE = 0x0002 CF_UNICODETEXT = 13 @contextlib.contextmanager def window(): """ Context that provides a valid Windows hwnd. """ # we really just need the hwnd, so setting "STATIC" # as predefined lpClass is just fine. hwnd = safeCreateWindowExA(0, b"STATIC", None, 0, 0, 0, 0, 0, None, None, None, None) try: yield hwnd finally: safeDestroyWindow(hwnd) @contextlib.contextmanager def clipboard(hwnd): """ Context manager that opens the clipboard and prevents other applications from modifying the clipboard content. """ # We may not get the clipboard handle immediately because # some other application is accessing it (?) # We try for at least 500ms to get the clipboard. t = time.time() + 0.5 success = False while time.time() < t: success = OpenClipboard(hwnd) if success: break time.sleep(0.01) if not success: raise PyperclipWindowsException("Error calling OpenClipboard") try: yield finally: safeCloseClipboard() def copy_windows(text): # This function is heavily based on # http://msdn.com/ms649016#_win32_Copying_Information_to_the_Clipboard text = _stringifyText(text) # Converts non-str values to str. with window() as hwnd: # http://msdn.com/ms649048 # If an application calls OpenClipboard with hwnd set to NULL, # EmptyClipboard sets the clipboard owner to NULL; # this causes SetClipboardData to fail. # => We need a valid hwnd to copy something. with clipboard(hwnd): safeEmptyClipboard() if text: # http://msdn.com/ms649051 # If the hMem parameter identifies a memory object, # the object must have been allocated using the # function with the GMEM_MOVEABLE flag. count = wcslen(text) + 1 handle = safeGlobalAlloc(GMEM_MOVEABLE, count * sizeof(c_wchar)) locked_handle = safeGlobalLock(handle) ctypes.memmove(c_wchar_p(locked_handle), c_wchar_p(text), count * sizeof(c_wchar)) safeGlobalUnlock(handle) safeSetClipboardData(CF_UNICODETEXT, handle) def paste_windows(): with clipboard(None): handle = safeGetClipboardData(CF_UNICODETEXT) if not handle: # GetClipboardData may return NULL with errno == NO_ERROR # if the clipboard is empty. # (Also, it may return a handle to an empty buffer, # but technically that's not empty) return "" locked_handle = safeGlobalLock(handle) return_value = c_wchar_p(locked_handle).value safeGlobalUnlock(handle) return return_value return copy_windows, paste_windows def init_wsl_clipboard(): def copy_wsl(text): text = _stringifyText(text) # Converts non-str values to str. p = subprocess.Popen(['clip.exe'], stdin=subprocess.PIPE, close_fds=True) p.communicate(input=text.encode(ENCODING)) def paste_wsl(): # '-noprofile' speeds up load time p = subprocess.Popen(['powershell.exe', '-noprofile', '-command', 'Get-Clipboard'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) stdout, stderr = p.communicate() # WSL appends "\r\n" to the contents. return stdout[:-2].decode(ENCODING) return copy_wsl, paste_wsl # Automatic detection of clipboard mechanisms and importing is done in deteremine_clipboard(): def determine_clipboard(): ''' Determine the OS/platform and set the copy() and paste() functions accordingly. ''' global Foundation, AppKit, gtk, qtpy, PyQt4, PyQt5, PyQt6 # Setup for the CYGWIN platform: if 'cygwin' in platform.system().lower(): # Cygwin has a variety of values returned by platform.system(), such as 'CYGWIN_NT-6.1' # FIXME: pyperclip currently does not support Cygwin, # see https://github.com/asweigart/pyperclip/issues/55 if os.path.exists('/dev/clipboard'): warnings.warn('Pyperclip\'s support for Cygwin is not perfect, see https://github.com/asweigart/pyperclip/issues/55') return init_dev_clipboard_clipboard() # Setup for the WINDOWS platform: elif os.name == 'nt' or platform.system() == 'Windows': return init_windows_clipboard() if platform.system() == 'Linux' and os.path.isfile('/proc/version'): with open('/proc/version', 'r') as f: if "microsoft" in f.read().lower(): return init_wsl_clipboard() # Setup for the MAC OS X platform: if os.name == 'mac' or platform.system() == 'Darwin': try: import Foundation # check if pyobjc is installed import AppKit except ImportError: return init_osx_pbcopy_clipboard() else: return init_osx_pyobjc_clipboard() # Setup for the LINUX platform: if HAS_DISPLAY: try: import gtk # check if gtk is installed except ImportError: pass # We want to fail fast for all non-ImportError exceptions. else: return init_gtk_clipboard() if ( os.environ.get("WAYLAND_DISPLAY") and _executable_exists("wl-copy") ): return init_wl_clipboard() if _executable_exists("xsel"): return init_xsel_clipboard() if _executable_exists("xclip"): return init_xclip_clipboard() if _executable_exists("klipper") and _executable_exists("qdbus"): return init_klipper_clipboard() try: # qtpy is a small abstraction layer that lets you write applications using a single api call to either PyQt or PySide. # https://pypi.python.org/pypi/QtPy import qtpy # check if qtpy is installed except ImportError: # If qtpy isn't installed, fall back on importing PyQt4. try: import PyQt6 # check if PyQt6 is installed except ImportError: try: import PyQt5 # check if PyQt5 is installed except ImportError: try: import PyQt4 # check if PyQt4 is installed except ImportError: pass # We want to fail fast for all non-ImportError exceptions. else: return init_qt_clipboard() else: return init_qt_clipboard() else: return init_qt_clipboard() else: return init_qt_clipboard() return init_no_clipboard() def set_clipboard(clipboard): ''' Explicitly sets the clipboard mechanism. The "clipboard mechanism" is how the copy() and paste() functions interact with the operating system to implement the copy/paste feature. The clipboard parameter must be one of: - pbcopy - pbobjc (default on Mac OS X) - gtk - qt - xclip - xsel - klipper - windows (default on Windows) - no (this is what is set when no clipboard mechanism can be found) ''' global copy, paste clipboard_types = { "pbcopy": init_osx_pbcopy_clipboard, "pyobjc": init_osx_pyobjc_clipboard, "gtk": init_gtk_clipboard, "qt": init_qt_clipboard, # TODO - split this into 'qtpy', 'pyqt4', and 'pyqt5' "xclip": init_xclip_clipboard, "xsel": init_xsel_clipboard, "wl-clipboard": init_wl_clipboard, "klipper": init_klipper_clipboard, "windows": init_windows_clipboard, "no": init_no_clipboard, } if clipboard not in clipboard_types: raise ValueError('Argument must be one of %s' % (', '.join([repr(_) for _ in clipboard_types.keys()]))) # Sets pyperclip's copy() and paste() functions: copy, paste = clipboard_types[clipboard]() def lazy_load_stub_copy(text): ''' A stub function for copy(), which will load the real copy() function when called so that the real copy() function is used for later calls. This allows users to import pyperclip without having determine_clipboard() automatically run, which will automatically select a clipboard mechanism. This could be a problem if it selects, say, the memory-heavy PyQt4 module but the user was just going to immediately call set_clipboard() to use a different clipboard mechanism. The lazy loading this stub function implements gives the user a chance to call set_clipboard() to pick another clipboard mechanism. Or, if the user simply calls copy() or paste() without calling set_clipboard() first, will fall back on whatever clipboard mechanism that determine_clipboard() automatically chooses. ''' global copy, paste copy, paste = determine_clipboard() return copy(text) def lazy_load_stub_paste(): ''' A stub function for paste(), which will load the real paste() function when called so that the real paste() function is used for later calls. This allows users to import pyperclip without having determine_clipboard() automatically run, which will automatically select a clipboard mechanism. This could be a problem if it selects, say, the memory-heavy PyQt4 module but the user was just going to immediately call set_clipboard() to use a different clipboard mechanism. The lazy loading this stub function implements gives the user a chance to call set_clipboard() to pick another clipboard mechanism. Or, if the user simply calls copy() or paste() without calling set_clipboard() first, will fall back on whatever clipboard mechanism that determine_clipboard() automatically chooses. ''' global copy, paste copy, paste = determine_clipboard() return paste() def is_available(): return copy != lazy_load_stub_copy and paste != lazy_load_stub_paste # Initially, copy() and paste() are set to lazy loading wrappers which will # set `copy` and `paste` to real functions the first time they're used, unless # set_clipboard() or determine_clipboard() is called first. copy, paste = lazy_load_stub_copy, lazy_load_stub_paste def waitForPaste(timeout=None): """This function call blocks until a non-empty text string exists on the clipboard. It returns this text. This function raises PyperclipTimeoutException if timeout was set to a number of seconds that has elapsed without non-empty text being put on the clipboard.""" startTime = time.time() while True: clipboardText = paste() if clipboardText != '': return clipboardText time.sleep(0.01) if timeout is not None and time.time() > startTime + timeout: raise PyperclipTimeoutException('waitForPaste() timed out after ' + str(timeout) + ' seconds.') def waitForNewPaste(timeout=None): """This function call blocks until a new text string exists on the clipboard that is different from the text that was there when the function was first called. It returns this text. This function raises PyperclipTimeoutException if timeout was set to a number of seconds that has elapsed without non-empty text being put on the clipboard.""" startTime = time.time() originalText = paste() while True: currentText = paste() if currentText != originalText: return currentText time.sleep(0.01) if timeout is not None and time.time() > startTime + timeout: raise PyperclipTimeoutException('waitForNewPaste() timed out after ' + str(timeout) + ' seconds.') __all__ = ['copy', 'paste', 'waitForPaste', 'waitForNewPaste', 'set_clipboard', 'determine_clipboard'] ================================================ FILE: electrum/address_synchronizer.py ================================================ # Electrum - lightweight Bitcoin client # Copyright (C) 2018 The Electrum Developers # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files # (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, # publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import asyncio import copy import dataclasses import threading import itertools from collections import defaultdict from typing import TYPE_CHECKING, Dict, Optional, Set, Tuple, NamedTuple, Sequence, List from .crypto import sha256 from . import bitcoin, util from .bitcoin import COINBASE_MATURITY from .util import profiler, bfh, TxMinedInfo, UnrelatedTransactionException, with_lock, OldTaskGroup from .transaction import Transaction, TxOutput, TxInput, PartialTxInput, TxOutpoint, PartialTransaction, tx_from_any from .synchronizer import Synchronizer from .verifier import SPV from .blockchain import hash_header, Blockchain from .i18n import _ from .logging import Logger from .util import EventListener, event_listener if TYPE_CHECKING: from .network import Network from .wallet_db import WalletDB from .simple_config import SimpleConfig TX_HEIGHT_FUTURE = -3 TX_HEIGHT_LOCAL = -2 TX_HEIGHT_UNCONF_PARENT = -1 TX_HEIGHT_UNCONFIRMED = 0 TX_TIMESTAMP_INF = 999_999_999_999 TX_HEIGHT_INF = 10 ** 9 from enum import IntEnum, auto class TxMinedDepth(IntEnum): """ IntEnum because we call min() in get_deepest_tx_mined_depth_for_txids """ DEEP = auto() SHALLOW = auto() MEMPOOL = auto() FREE = auto() class HistoryItem(NamedTuple): txid: str tx_mined_status: TxMinedInfo delta: int fee: Optional[int] balance: int class AddressSynchronizer(Logger, EventListener): """ address database """ network: Optional['Network'] asyncio_loop: Optional['asyncio.AbstractEventLoop'] = None synchronizer: Optional['Synchronizer'] verifier: Optional['SPV'] def __init__(self, db: 'WalletDB', config: 'SimpleConfig', *, name: str = None): self.db = db self.config = config self.name = name self.network = None Logger.__init__(self) # verifier (SPV) and synchronizer are started in start_network self.synchronizer = None self.verifier = None self.lock = threading.RLock() self.future_tx = {} # type: Dict[str, int] # txid -> wanted (abs) height # Txs the server claims are mined but still pending verification: self.unverified_tx = defaultdict(int) # type: Dict[str, int] # txid -> height. Access with self.lock. # Txs the server claims are in the mempool: self.unconfirmed_tx = defaultdict(int) # type: Dict[str, int] # txid -> height. Access with self.lock. # thread local storage for caching stuff self.threadlocal_cache = threading.local() self._get_balance_cache = {} self._get_utxos_cache = {} self.load_and_cleanup() @with_lock def invalidate_cache(self): self._get_balance_cache.clear() self._get_utxos_cache.clear() def diagnostic_name(self): return self.name or "" @with_lock def load_and_cleanup(self): self.load_local_history() self.check_history() self.load_unverified_transactions() self.remove_local_transactions_we_dont_have() def is_mine(self, address: Optional[str]) -> bool: """Returns whether an address is in our set. Differences between adb.is_mine and wallet.is_mine: - adb.is_mine: addrs that we are watching (e.g. via Synchronizer) - lnwatcher adds its own lightning-related addresses that are not part of the wallet - wallet.is_mine: addrs that are part of the wallet balance or the wallet might sign for - an offline wallet might learn from a PSBT about addrs beyond its gap limit Neither set is guaranteed to be a subset of the other. """ if not address: return False return self.db.is_addr_in_history(address) def get_addresses(self): return sorted(self.db.get_history()) @with_lock def get_address_history(self, addr: str) -> Dict[str, int]: """Returns the history for the address, as a txid->height dict. In addition to what we have from the server, this includes local and future txns. Note: heights are SPV-verified. Also see related method db.get_addr_history, which stores the response from the server, so that only includes txns the server sees. """ h = {} related_txns = self._history_local.get(addr, set()) for tx_hash in related_txns: tx_height = self.get_tx_height(tx_hash).height() h[tx_hash] = tx_height return h def get_address_history_len(self, addr: str) -> int: """Return number of transactions where address is involved.""" return len(self._history_local.get(addr, ())) @with_lock def get_txin_address(self, txin: TxInput) -> Optional[str]: if txin.address: return txin.address prevout_hash = txin.prevout.txid.hex() prevout_n = txin.prevout.out_idx for addr in self.db.get_txo_addresses(prevout_hash): d = self.db.get_txo_addr(prevout_hash, addr) if prevout_n in d: return addr tx = self.db.get_transaction(prevout_hash) if tx: return tx.outputs()[prevout_n].address return None @with_lock def get_txin_value(self, txin: TxInput, *, address: str = None) -> Optional[int]: if txin.value_sats() is not None: return txin.value_sats() prevout_hash = txin.prevout.txid.hex() prevout_n = txin.prevout.out_idx if address is None: address = self.get_txin_address(txin) if address: d = self.db.get_txo_addr(prevout_hash, address) try: v, cb = d[prevout_n] return v except KeyError: pass tx = self.db.get_transaction(prevout_hash) if tx: return tx.outputs()[prevout_n].value return None @with_lock def load_unverified_transactions(self): # review transactions that are in the history for addr in self.db.get_history(): hist = self.db.get_addr_history(addr) for tx_hash, tx_height in hist: # add it in case it was previously unconfirmed self.add_unverified_or_unconfirmed_tx(tx_hash, tx_height) def start_network(self, network: Optional['Network']) -> None: assert self.network is None, "already started" self.network = network if self.network is not None: self.synchronizer = Synchronizer(self) self.verifier = SPV(self.network, self) self.asyncio_loop = network.asyncio_loop self.register_callbacks() self._update_stored_local_height() @event_listener @with_lock def on_event_blockchain_updated(self, *args): self.invalidate_cache() self._update_stored_local_height() async def stop(self): if self.network: try: async with OldTaskGroup() as group: if self.synchronizer: await group.spawn(self.synchronizer.stop()) if self.verifier: await group.spawn(self.verifier.stop()) finally: # even if we get cancelled self.synchronizer = None self.verifier = None self.unregister_callbacks() self.network = None def add_address(self, address: str) -> None: if address not in self.db.history: self.db.history[address] = [] if self.synchronizer: self.synchronizer.add(address) self.up_to_date_changed() @with_lock def get_conflicting_transactions(self, tx: Transaction, *, include_self: bool = False) -> Set[str]: """Returns a set of transaction hashes from the wallet history that are directly conflicting with tx, i.e. they have common outpoints being spent with tx. include_self specifies whether the tx itself should be reported as a conflict (if already in wallet history) """ conflicting_txns = set() for txin in tx.inputs(): if txin.is_coinbase_input(): continue prevout_hash = txin.prevout.txid.hex() prevout_n = txin.prevout.out_idx spending_tx_hash = self.db.get_spent_outpoint(prevout_hash, prevout_n) if spending_tx_hash is None: continue # this outpoint has already been spent, by spending_tx # annoying assert that has revealed several bugs over time: assert self.db.get_transaction(spending_tx_hash), "spending tx not in wallet db" conflicting_txns |= {spending_tx_hash} if tx_hash := tx.txid(): if tx_hash in conflicting_txns: # this tx is already in history, so it conflicts with itself if len(conflicting_txns) > 1: raise Exception('Found conflicting transactions already in wallet history.') if not include_self: conflicting_txns -= {tx_hash} return conflicting_txns @with_lock def get_transaction(self, txid: str) -> Optional[Transaction]: tx = self.db.get_transaction(txid) if tx: tx.deserialize() for txin in tx._inputs: tx_mined_info = self.get_tx_height(txin.prevout.txid.hex()) txin.block_height = tx_mined_info.height() txin.block_txpos = tx_mined_info.txpos return tx def add_transaction(self, tx: Transaction, *, allow_unrelated=False, is_new=True) -> bool: """ Returns whether the tx was successfully added to the wallet history. Note that a transaction may need to be added several times, if our list of addresses has increased. This will return True even if the transaction was already in self.db. """ assert tx, tx # note: tx.is_complete() is not necessarily True; tx might be partial # but it *needs* to have a txid: tx_hash = tx.txid() if tx_hash is None: raise Exception("cannot add tx without txid to wallet history") # For sanity, try to serialize and deserialize tx early: tx_from_any(str(tx)) # see if raises (no-side-effects) with self.lock: # NOTE: returning if tx in self.transactions might seem like a good idea # BUT we track is_mine inputs in a txn, and during subsequent calls # of add_transaction tx, we might learn of more-and-more inputs of # being is_mine, as we roll the gap_limit forward is_coinbase = tx.inputs()[0].is_coinbase_input() tx_height = self.get_tx_height(tx_hash, force_local_if_missing_tx=False).height() if not allow_unrelated: # note that during sync, if the transactions are not properly sorted, # it could happen that we think tx is unrelated but actually one of the inputs is is_mine. # this is the main motivation for allow_unrelated is_mine = any([self.is_mine(self.get_txin_address(txin)) for txin in tx.inputs()]) is_for_me = any([self.is_mine(txo.address) for txo in tx.outputs()]) if not is_mine and not is_for_me: raise UnrelatedTransactionException() # Find all conflicting transactions. # In case of a conflict, # 1. confirmed > mempool > local # 2. this new txn has priority over existing ones # When this method exits, there must NOT be any conflict, so # either keep this txn and remove all conflicting (along with dependencies) # or drop this txn conflicting_txns = self.get_conflicting_transactions(tx) if conflicting_txns: existing_mempool_txn = any( self.get_tx_height(tx_hash2).height() in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT) for tx_hash2 in conflicting_txns) existing_confirmed_txn = any( self.get_tx_height(tx_hash2).height() > 0 for tx_hash2 in conflicting_txns) if existing_confirmed_txn and tx_height <= 0: # this is a non-confirmed tx that conflicts with confirmed txns; drop. return False if existing_mempool_txn and tx_height == TX_HEIGHT_LOCAL: # this is a local tx that conflicts with non-local txns; drop. return False # keep this txn and remove all conflicting for tx_hash2 in conflicting_txns: self.remove_transaction(tx_hash2) # add inputs def add_value_from_prev_output(): # note: this takes linear time in num is_mine outputs of prev_tx addr = self.get_txin_address(txi) if addr and self.is_mine(addr): outputs = self.db.get_txo_addr(prevout_hash, addr) try: v, is_cb = outputs[prevout_n] except KeyError: pass else: self.db.add_txi_addr(tx_hash, addr, ser, v) self.invalidate_cache() for txi in tx.inputs(): if txi.is_coinbase_input(): continue prevout_hash = txi.prevout.txid.hex() prevout_n = txi.prevout.out_idx ser = txi.prevout.to_str() self.db.set_spent_outpoint(prevout_hash, prevout_n, tx_hash) add_value_from_prev_output() # add outputs for n, txo in enumerate(tx.outputs()): v = txo.value ser = tx_hash + ':%d'%n scripthash = bitcoin.script_to_scripthash(txo.scriptpubkey) self.db.add_prevout_by_scripthash(scripthash, prevout=TxOutpoint.from_str(ser), value=v) addr = txo.address if addr and self.is_mine(addr): self.db.add_txo_addr(tx_hash, addr, n, v, is_coinbase) self.invalidate_cache() # give v to txi that spends me next_tx = self.db.get_spent_outpoint(tx_hash, n) if next_tx is not None: self.db.add_txi_addr(next_tx, addr, ser, v) self._add_tx_to_local_history(next_tx) # add to local history self._add_tx_to_local_history(tx_hash) # save self.db.add_transaction(tx_hash, tx) self.db.add_num_inputs_to_tx(tx_hash, len(tx.inputs())) if is_new: util.trigger_callback('adb_added_tx', self, tx_hash, tx) return True @with_lock def remove_transaction(self, tx_hash: str) -> None: """Removes a transaction AND all its dependents/children from the wallet history. """ to_remove = {tx_hash} to_remove |= self.get_depending_transactions(tx_hash) for txid in to_remove: self._remove_transaction(txid) def _remove_transaction(self, tx_hash: str) -> None: """Removes a single transaction from the wallet history, and attempts to undo all effects of the tx (spending inputs, creating outputs, etc). """ def remove_from_spent_outpoints(): # undo spends in spent_outpoints if tx is not None: # if we have the tx, this branch is faster for txin in tx.inputs(): if txin.is_coinbase_input(): continue prevout_hash = txin.prevout.txid.hex() prevout_n = txin.prevout.out_idx self.db.remove_spent_outpoint(prevout_hash, prevout_n) else: # expensive but always works for prevout_hash, prevout_n in self.db.list_spent_outpoints(): spending_txid = self.db.get_spent_outpoint(prevout_hash, prevout_n) if spending_txid == tx_hash: self.db.remove_spent_outpoint(prevout_hash, prevout_n) with self.lock: self.logger.info(f"removing tx from history {tx_hash}") tx = self.db.remove_transaction(tx_hash) remove_from_spent_outpoints() self._remove_tx_from_local_history(tx_hash) self.invalidate_cache() self.db.remove_txi(tx_hash) self.db.remove_txo(tx_hash) self.db.remove_tx_fee(tx_hash) self.db.remove_verified_tx(tx_hash) self.unverified_tx.pop(tx_hash, None) self.unconfirmed_tx.pop(tx_hash, None) if tx: for idx, txo in enumerate(tx.outputs()): scripthash = bitcoin.script_to_scripthash(txo.scriptpubkey) prevout = TxOutpoint(bfh(tx_hash), idx) self.db.remove_prevout_by_scripthash(scripthash, prevout=prevout, value=txo.value) util.trigger_callback('adb_removed_tx', self, tx_hash, tx) @with_lock def get_depending_transactions(self, tx_hash: str) -> Set[str]: """Returns all (grand-)children of tx_hash in this wallet.""" children = set() for n in self.db.get_spent_outpoints(tx_hash): other_hash = self.db.get_spent_outpoint(tx_hash, n) children.add(other_hash) children |= self.get_depending_transactions(other_hash) return children @with_lock def receive_tx_callback(self, tx: Transaction, *, tx_height: Optional[int] = None) -> None: txid = tx.txid() assert txid is not None if tx_height is not None: # note: tx_height is only set by the unit tests: to inject a tx into the history self.add_unverified_or_unconfirmed_tx(txid, tx_height) self.add_transaction(tx, allow_unrelated=True) @with_lock def receive_history_callback(self, addr: str, hist, tx_fees: Dict[str, int]): old_hist = self.get_address_history(addr) for tx_hash, height in old_hist.items(): if (tx_hash, height) not in hist: # make tx local self.unverified_tx.pop(tx_hash, None) self.unconfirmed_tx.pop(tx_hash, None) self.db.remove_verified_tx(tx_hash) if self.verifier: self.verifier.remove_spv_proof_for_tx(tx_hash) self.db.set_addr_history(addr, hist) for tx_hash, tx_height in hist: # add it in case it was previously unconfirmed self.add_unverified_or_unconfirmed_tx(tx_hash, tx_height) # if addr is new, we have to recompute txi and txo tx = self.db.get_transaction(tx_hash) if tx is None: continue self.add_transaction(tx, allow_unrelated=True, is_new=False) # if we already had this tx, see if its height changed (e.g. local->unconfirmed) old_height = old_hist.get(tx_hash, None) if old_height is not None and old_height != tx_height: util.trigger_callback('adb_tx_height_changed', self, tx_hash, old_height, tx_height) # Store fees for tx_hash, fee_sat in tx_fees.items(): self.db.add_tx_fee_from_server(tx_hash, fee_sat) @with_lock @profiler def load_local_history(self): self._history_local = {} # type: Dict[str, Set[str]] # address -> set(txid) self._address_history_changed_events = defaultdict(asyncio.Event) # address -> Event for txid in itertools.chain(self.db.list_txi(), self.db.list_txo()): self._add_tx_to_local_history(txid) @with_lock @profiler def check_history(self): hist_addrs_mine = list(filter(lambda k: self.is_mine(k), self.db.get_history())) hist_addrs_not_mine = list(filter(lambda k: not self.is_mine(k), self.db.get_history())) for addr in hist_addrs_not_mine: self.db.remove_addr_history(addr) for addr in hist_addrs_mine: hist = self.db.get_addr_history(addr) for tx_hash, tx_height in hist: if self.db.get_txi_addresses(tx_hash) or self.db.get_txo_addresses(tx_hash): continue tx = self.db.get_transaction(tx_hash) if tx is not None: self.add_transaction(tx, allow_unrelated=True) @with_lock def remove_local_transactions_we_dont_have(self): for txid in itertools.chain(self.db.list_txi(), self.db.list_txo()): tx_height = self.get_tx_height(txid).height() if tx_height == TX_HEIGHT_LOCAL and not self.db.get_transaction(txid): self.remove_transaction(txid) @with_lock def clear_history(self): self.db.clear_history() self._history_local.clear() self.invalidate_cache() @with_lock def _get_tx_sort_key(self, tx_hash: str) -> Tuple[int, int]: """Returns a key to be used for sorting txs.""" tx_mined_info = self.get_tx_height(tx_hash) height = self.tx_height_to_sort_height(tx_mined_info.height()) txpos = tx_mined_info.txpos or -1 return height, txpos @classmethod def tx_height_to_sort_height(cls, height: int = None): """Return a height-like value to be used for sorting txs.""" if height is not None: if height > 0: return height if height == TX_HEIGHT_UNCONFIRMED: return TX_HEIGHT_INF if height == TX_HEIGHT_UNCONF_PARENT: return TX_HEIGHT_INF + 1 if height == TX_HEIGHT_FUTURE: return TX_HEIGHT_INF + 2 if height == TX_HEIGHT_LOCAL: return TX_HEIGHT_INF + 3 return TX_HEIGHT_INF + 100 def with_local_height_cached(func): # get local height only once, as it's relatively expensive. # take care that nested calls work as expected def f(self, *args, **kwargs): orig_val = getattr(self.threadlocal_cache, 'local_height', None) self.threadlocal_cache.local_height = orig_val or self.get_local_height() try: return func(self, *args, **kwargs) finally: self.threadlocal_cache.local_height = orig_val return f @with_lock @with_local_height_cached def get_history(self, domain) -> Sequence[HistoryItem]: domain = set(domain) # 1. Get the history of each address in the domain, maintain the # delta of a tx as the sum of its deltas on domain addresses tx_deltas = defaultdict(int) # type: Dict[str, int] for addr in domain: h = self.get_address_history(addr).items() for tx_hash, height in h: tx_deltas[tx_hash] += self.get_tx_delta(tx_hash, addr) # 2. create sorted history history = [] for tx_hash in tx_deltas: delta = tx_deltas[tx_hash] tx_mined_status = self.get_tx_height(tx_hash) fee = self.get_tx_fee(tx_hash) history.append((tx_hash, tx_mined_status, delta, fee)) history.sort(key = lambda x: self._get_tx_sort_key(x[0])) # 3. add balance h2 = [] balance = 0 for tx_hash, tx_mined_status, delta, fee in history: balance += delta h2.append(HistoryItem( txid=tx_hash, tx_mined_status=tx_mined_status, delta=delta, fee=fee, balance=balance)) # sanity check c, u, x = self.get_balance(domain) if balance != c + u + x: self.logger.error(f'sanity check failed! c={c},u={u},x={x} while history balance={balance}') raise Exception("wallet.get_history() failed balance sanity-check") return h2 @with_lock def _add_tx_to_local_history(self, txid): for addr in itertools.chain(self.db.get_txi_addresses(txid), self.db.get_txo_addresses(txid)): cur_hist = self._history_local.get(addr, set()) cur_hist.add(txid) self._history_local[addr] = cur_hist self._mark_address_history_changed(addr) @with_lock def _remove_tx_from_local_history(self, txid): for addr in itertools.chain(self.db.get_txi_addresses(txid), self.db.get_txo_addresses(txid)): cur_hist = self._history_local.get(addr, set()) try: cur_hist.remove(txid) except KeyError: pass else: self._history_local[addr] = cur_hist self._mark_address_history_changed(addr) def _mark_address_history_changed(self, addr: str) -> None: def set_and_clear(): event = self._address_history_changed_events[addr] # history for this address changed, wake up coroutines: event.set() # clear event immediately so that coroutines can wait() for the next change: event.clear() if self.asyncio_loop: self.asyncio_loop.call_soon_threadsafe(set_and_clear) async def wait_for_address_history_to_change(self, addr: str) -> None: """Wait until the server tells us about a new transaction related to addr. Unconfirmed and confirmed transactions are not distinguished, and so e.g. SPV is not taken into account. """ assert self.is_mine(addr), "address needs to be is_mine to be watched" await self._address_history_changed_events[addr].wait() @with_lock def add_unverified_or_unconfirmed_tx(self, tx_hash: str, tx_height: int) -> None: assert tx_height >= TX_HEIGHT_UNCONF_PARENT, f"got {tx_height=} for {tx_hash=}" # forbid local/future txs here if self.db.is_in_verified_tx(tx_hash): if tx_height <= 0: # tx was previously SPV-verified but now in mempool (probably reorg) self.db.remove_verified_tx(tx_hash) self.unconfirmed_tx[tx_hash] = tx_height if self.verifier: self.verifier.remove_spv_proof_for_tx(tx_hash) else: if tx_height > 0: self.unverified_tx[tx_hash] = tx_height else: self.unconfirmed_tx[tx_hash] = tx_height @with_lock def remove_unverified_tx(self, tx_hash: str, tx_height: int) -> None: new_height = self.unverified_tx.get(tx_hash) if new_height == tx_height: self.unverified_tx.pop(tx_hash, None) def add_verified_tx(self, tx_hash: str, info: TxMinedInfo): # Remove from the unverified map and add to the verified map with self.lock: self.unverified_tx.pop(tx_hash, None) self.db.add_verified_tx(tx_hash, info) self.invalidate_cache() util.trigger_callback('adb_added_verified_tx', self, tx_hash) @with_lock def get_unverified_txs(self) -> Dict[str, int]: '''Returns a map from tx hash to transaction height''' return dict(self.unverified_tx) # copy def undo_verifications(self, blockchain: Blockchain, above_height: int) -> Set[str]: '''Used by the verifier when a reorg has happened''' txs = set() with self.lock: for tx_hash in self.db.list_verified_tx(): info = self.db.get_verified_tx(tx_hash) tx_height = info._height if tx_height > above_height: header = blockchain.read_header(tx_height) if not header or hash_header(header) != info.header_hash: self.db.remove_verified_tx(tx_hash) # NOTE: we should add these txns to self.unverified_tx, # but with what height? # If on the new fork after the reorg, the txn is at the # same height, we will not get a status update for the # address. If the txn is not mined or at a diff height, # we should get a status update. Unless we put tx into # unverified_tx, it will turn into local. So we put it # into unverified_tx with the old height, and if we get # a status update, that will overwrite it. self.unverified_tx[tx_hash] = tx_height txs.add(tx_hash) for tx_hash in txs: util.trigger_callback('adb_removed_verified_tx', self, tx_hash) return txs def get_local_height(self) -> int: """ return last known height if we are offline """ cached_local_height = getattr(self.threadlocal_cache, 'local_height', None) if cached_local_height is not None: return cached_local_height return self.network.get_local_height() if self.network else self.db.get('stored_height', 0) def _update_stored_local_height(self) -> None: self.db.put('stored_height', self.get_local_height()) def set_future_tx(self, txid: str, *, wanted_height: int): """Mark a local tx as "future" (encumbered by a timelock). wanted_height is the min (abs) block height at which the tx can get into the mempool (be broadcast). note: tx becomes consensus-valid to be mined in a block at height wanted_height+1 In case of a CSV-locked tx with unconfirmed inputs, the wanted_height is a best-case guess. """ with self.lock: old_height = self.future_tx.get(txid) or None self.future_tx[txid] = wanted_height if old_height != wanted_height: util.trigger_callback('adb_set_future_tx', self, txid) def get_tx_height( self, tx_hash: str, *, force_local_if_missing_tx: bool = True, ) -> TxMinedInfo: if tx_hash is None: # ugly backwards compat... return TxMinedInfo(_height=TX_HEIGHT_LOCAL, conf=0) with self.lock: if verified_tx_mined_info := self.db.get_verified_tx(tx_hash): # mined and spv-ed conf = max(self.get_local_height() - verified_tx_mined_info._height + 1, 0) tx_mined_info = dataclasses.replace(verified_tx_mined_info, conf=conf) elif tx_hash in self.unverified_tx: # mined, no spv height = self.unverified_tx[tx_hash] tx_mined_info = TxMinedInfo(_height=height, conf=0) elif tx_hash in self.unconfirmed_tx: # mempool height = self.unconfirmed_tx[tx_hash] tx_mined_info = TxMinedInfo(_height=height, conf=0) elif wanted_height := self.future_tx.get(tx_hash): # future if wanted_height > self.get_local_height(): tx_mined_info = TxMinedInfo(_height=TX_HEIGHT_FUTURE, conf=0, wanted_height=wanted_height) else: tx_mined_info = TxMinedInfo(_height=TX_HEIGHT_LOCAL, conf=0) else: # local tx_mined_info = TxMinedInfo(_height=TX_HEIGHT_LOCAL, conf=0) if tx_mined_info.height() in (TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE): return tx_mined_info if force_local_if_missing_tx: # It can happen for a txid in any state (unconf/unverified/verified) that we # don't have the raw tx yet, simply due to network timing. # Having only a partial tx is another variant of this. # FIXME in fact even if we have a complete tx saved, the server might have # a different tx if only the witness differs. We should compare wtxids. tx = self.db.get_transaction(tx_hash) if tx is None or isinstance(tx, PartialTransaction): return TxMinedInfo(_height=TX_HEIGHT_LOCAL, conf=0) return tx_mined_info def up_to_date_changed(self) -> None: # fire triggers util.trigger_callback('adb_set_up_to_date', self) def is_up_to_date(self): if not self.synchronizer or not self.verifier: return False return self.synchronizer.is_up_to_date() and self.verifier.is_up_to_date() def reset_netrequest_counters(self) -> None: if self.synchronizer: self.synchronizer.reset_request_counters() if self.verifier: self.verifier.reset_request_counters() def get_history_sync_state_details(self) -> Tuple[int, int]: nsent, nans = 0, 0 if self.synchronizer: n1, n2 = self.synchronizer.num_requests_sent_and_answered() nsent += n1 nans += n2 if self.verifier: n1, n2 = self.verifier.num_requests_sent_and_answered() nsent += n1 nans += n2 return nsent, nans @with_lock def get_tx_delta(self, tx_hash: str, address: str) -> int: """effect of tx on address""" delta = 0 # subtract the value of coins sent from address d = self.db.get_txi_addr(tx_hash, address) for n, v in d: delta -= v # add the value of the coins received at address d = self.db.get_txo_addr(tx_hash, address) for n, (v, cb) in d.items(): delta += v return delta @with_lock def get_tx_fee(self, txid: str) -> Optional[int]: """Returns tx_fee or None. Use server fee only if tx is unconfirmed and not mine. Note: being fast is prioritised over completeness here. We try to avoid deserializing the tx, as that is expensive if we are called for the whole history. We sometimes incorrectly early-exit and return None, e.g. for not-all-ismine-input txs, where we could calculate the fee if we deserialized (but to see if we have all the parent txs available, we would have to deserialize first). More expensive but more complete alternative: wallet.get_tx_info(tx).fee """ # check if stored fee is available fee = self.db.get_tx_fee(txid, trust_server=False) if fee is not None: return fee # delete server-sent fee for confirmed txns confirmed = self.get_tx_height(txid).conf > 0 if confirmed: self.db.add_tx_fee_from_server(txid, None) # if all inputs are ismine, try to calc fee now; # otherwise, return stored value num_all_inputs = self.db.get_num_all_inputs_of_tx(txid) if num_all_inputs is not None: # check if tx is mine num_ismine_inputs = self.db.get_num_ismine_inputs_of_tx(txid) assert num_ismine_inputs <= num_all_inputs, (num_ismine_inputs, num_all_inputs) # trust server if tx is unconfirmed and not mine if num_ismine_inputs < num_all_inputs: return None if confirmed else self.db.get_tx_fee(txid, trust_server=True) # lookup tx and deserialize it. # note that deserializing is expensive, hence above hacks tx = self.db.get_transaction(txid) if not tx: return None # compute fee if possible v_in = v_out = 0 for txin in tx.inputs(): addr = self.get_txin_address(txin) value = self.get_txin_value(txin, address=addr) if value is None: v_in = None elif v_in is not None: v_in += value for txout in tx.outputs(): v_out += txout.value if v_in is not None: fee = v_in - v_out else: fee = None # save result self.db.add_tx_fee_we_calculated(txid, fee) self.db.add_num_inputs_to_tx(txid, len(tx.inputs())) return fee @with_lock def get_addr_io(self, address: str): h = self.get_address_history(address).items() received = {} # type: Dict[str, tuple[int, int, int, bool]] sent = {} # type: Dict[str, tuple[str, int, int]] for tx_hash, height in h: tx_mined_info = self.get_tx_height(tx_hash) txpos = tx_mined_info.txpos if tx_mined_info.txpos is not None else -1 d = self.db.get_txo_addr(tx_hash, address) for n, (v, is_cb) in d.items(): received[tx_hash + ':%d'%n] = (height, txpos, v, is_cb) l = self.db.get_txi_addr(tx_hash, address) for txi, v in l: sent[txi] = tx_hash, height, txpos return received, sent def get_addr_outputs(self, address: str) -> Dict[TxOutpoint, PartialTxInput]: received, sent = self.get_addr_io(address) out = {} for prevout_str, v in received.items(): tx_height, tx_pos, value, is_cb = v prevout = TxOutpoint.from_str(prevout_str) utxo = PartialTxInput(prevout=prevout, is_coinbase_output=is_cb) utxo._trusted_address = address utxo._trusted_value_sats = value utxo.block_height = tx_height utxo.block_txpos = tx_pos if prevout_str in sent: txid, height, pos = sent[prevout_str] utxo.spent_txid = txid utxo.spent_height = height else: utxo.spent_txid = None utxo.spent_height = None out[prevout] = utxo return out def get_addr_utxo(self, address: str) -> Dict[TxOutpoint, PartialTxInput]: out = self.get_addr_outputs(address) for k, v in list(out.items()): if v.spent_height is not None: out.pop(k) return out # return the total amount ever received by an address def get_addr_received(self, address): received, sent = self.get_addr_io(address) return sum([value for height, pos, value, is_cb in received.values()]) @with_lock @with_local_height_cached def get_balance(self, domain, *, excluded_addresses: Set[str] = None, excluded_coins: Set[str] = None) -> Tuple[int, int, int]: """Return the balance of a set of addresses: confirmed and matured, unconfirmed, unmatured Note: intended for display-purposes. would need extreme care for "has enough funds" checks (see #8835) """ if excluded_addresses is None: excluded_addresses = set() assert isinstance(excluded_addresses, set), f"excluded_addresses should be set, not {type(excluded_addresses)}" domain = set(domain) - excluded_addresses if excluded_coins is None: excluded_coins = set() assert isinstance(excluded_coins, set), f"excluded_coins should be set, not {type(excluded_coins)}" cache_key = sha256(','.join(sorted(domain)) + ';' + ','.join(sorted(excluded_coins))) cached_value = self._get_balance_cache.get(cache_key) if cached_value: return cached_value coins = {} for address in domain: coins.update(self.get_addr_outputs(address)) c = u = x = 0 mempool_height = self.get_local_height() + 1 # height of next block for utxo in coins.values(): # type: PartialTxInput if utxo.spent_height is not None: continue if utxo.prevout.to_str() in excluded_coins: continue v = utxo.value_sats() tx_height = utxo.block_height is_cb = utxo.is_coinbase_output() if is_cb and tx_height + COINBASE_MATURITY > mempool_height: x += v elif tx_height > 0: c += v else: txid = utxo.prevout.txid.hex() tx = self.db.get_transaction(txid) assert tx is not None # txid comes from get_addr_io # we look at the outputs that are spent by this transaction # if those outputs are ours and confirmed, we count this coin as confirmed confirmed_spent_amount = 0 for txin in tx.inputs(): if txin.prevout in coins: coin = coins[txin.prevout] if coin.block_height > 0: confirmed_spent_amount += coin.value_sats() # Compare amount, in case tx has confirmed and unconfirmed inputs, or is a coinjoin. # (fixme: tx may have multiple change outputs) if confirmed_spent_amount >= v: c += v else: c += confirmed_spent_amount u += v - confirmed_spent_amount result = c, u, x # cache result. # Cache needs to be invalidated if a transaction is added to/ # removed from history; or on new blocks (maturity...) self._get_balance_cache[cache_key] = result return result @with_lock @with_local_height_cached def get_utxos( self, domain, *, excluded_addresses=None, mature_only: bool = False, confirmed_funding_only: bool = False, confirmed_spending_only: bool = False, nonlocal_only: bool = False, block_height: int = None, ) -> Sequence[PartialTxInput]: if block_height is not None: # caller wants the UTXOs we had at a given height; check other parameters assert confirmed_funding_only assert confirmed_spending_only assert nonlocal_only else: block_height = self.get_local_height() coins = [] domain = set(domain) if excluded_addresses: domain = set(domain) - set(excluded_addresses) mempool_height = block_height + 1 # height of next block cache_key = sha256( ','.join(sorted(domain)) + f";{mature_only};{confirmed_funding_only};{confirmed_spending_only};{nonlocal_only};{block_height}" ) cached = self._get_utxos_cache.get(cache_key) if cached is not None: return copy.deepcopy(cached) for addr in domain: txos = self.get_addr_outputs(addr) for txo in txos.values(): if txo.spent_height is not None: if not confirmed_spending_only: continue if confirmed_spending_only and 0 < txo.spent_height <= block_height: continue if confirmed_funding_only and not (0 < txo.block_height <= block_height): continue if nonlocal_only and txo.block_height in (TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE): continue if (mature_only and txo.is_coinbase_output() and txo.block_height + COINBASE_MATURITY > mempool_height): continue coins.append(txo) continue self._get_utxos_cache[cache_key] = copy.deepcopy(coins) return coins def is_used(self, address: str) -> bool: """Whether any tx ever touched `address`.""" return self.get_address_history_len(address) != 0 def is_used_as_from_address(self, address: str) -> bool: """Whether any tx ever spent from `address`.""" received, sent = self.get_addr_io(address) return len(sent) > 0 def is_empty(self, address: str) -> bool: coins = self.get_addr_utxo(address) return not bool(coins) @with_lock @with_local_height_cached def address_is_old(self, address: str, *, req_conf: int = 3) -> bool: """Returns whether address has any history that is deeply confirmed. Used for reorg-safe(ish) gap limit roll-forward. """ max_conf = -1 h = self.db.get_addr_history(address) needs_spv_check = not self.config.NETWORK_SKIPMERKLECHECK for tx_hash, tx_height in h: if needs_spv_check: tx_age = self.get_tx_height(tx_hash).conf else: if tx_height <= 0: tx_age = 0 else: tx_age = self.get_local_height() - tx_height + 1 max_conf = max(max_conf, tx_age) return max_conf >= req_conf @with_lock def get_spender(self, outpoint: str) -> Optional[str]: """ returns txid spending outpoint. subscribes to addresses as a side effect. """ prev_txid, index = outpoint.split(':') spender_txid = self.db.get_spent_outpoint(prev_txid, int(index)) # discard local spenders tx_mined_status = self.get_tx_height(spender_txid) if tx_mined_status.height() in [TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE]: spender_txid = None if not spender_txid: return None spender_tx = self.get_transaction(spender_txid) for i, o in enumerate(spender_tx.outputs()): if o.address is None: continue if not self.is_mine(o.address): self.add_address(o.address) return spender_txid def get_tx_mined_depth(self, txid: str): if not txid: return TxMinedDepth.FREE tx_mined_depth = self.get_tx_height(txid) height, conf = tx_mined_depth.height(), tx_mined_depth.conf if conf > 20: # FIXME unify with lnutil.REDEEM_AFTER_DOUBLE_SPENT_DELAY ? return TxMinedDepth.DEEP elif conf > 0: return TxMinedDepth.SHALLOW elif height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT): return TxMinedDepth.MEMPOOL elif height in (TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE): return TxMinedDepth.FREE elif height > 0 and conf == 0: # unverified but claimed to be mined return TxMinedDepth.MEMPOOL else: raise NotImplementedError() def is_deeply_mined(self, txid): return self.get_tx_mined_depth(txid) == TxMinedDepth.DEEP ================================================ FILE: electrum/base_crash_reporter.py ================================================ # Electrum - lightweight Bitcoin client # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files # (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, # publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import asyncio import json import locale import traceback import sys import queue from typing import TYPE_CHECKING, NamedTuple, Optional, TypedDict from types import TracebackType from .version import ELECTRUM_VERSION from . import constants from .i18n import _ from .util import make_aiohttp_session, error_text_str_to_safe_str from .logging import describe_os_version, Logger, get_git_version from .crypto import sha256 if TYPE_CHECKING: from .network import ProxySettings _tainted_by_console = False def taint_reports_by_console_usage(): global _tainted_by_console _tainted_by_console = True class CrashReportResponse(NamedTuple): status: Optional[str] text: str url: Optional[str] class BaseCrashReporter(Logger): report_server = "https://crashhub.electrum.org" issue_template = """

Traceback

{traceback}

Additional information

  • Electrum version: {app_version}
  • Python version: {python_version}
  • Operating system: {os}
  • Wallet type: {wallet_type}
  • Locale: {locale}
""" CRASH_MESSAGE = _('Something went wrong while executing Electrum.') CRASH_TITLE = _('Sorry!') REQUEST_HELP_MESSAGE = _('To help us diagnose and fix the problem, you can send us a bug report that contains ' 'useful debug information:') DESCRIBE_ERROR_MESSAGE = _("Please briefly describe what led to the error (optional):") ASK_CONFIRM_SEND = _("Do you want to send this report?") USER_COMMENT_PLACEHOLDER = _("Do not enter sensitive/private information here. " "The report will be visible on the public issue tracker.") exc_args: tuple[type[BaseException], BaseException, TracebackType | None] def __init__( self, exctype: type[BaseException], excvalue: BaseException, tb: TracebackType | None, ): Logger.__init__(self) self.exc_args = (exctype, excvalue, tb) def send_report(self, asyncio_loop, proxy: 'ProxySettings', *, timeout=None) -> CrashReportResponse: # FIXME the caller needs to catch generic "Exception", as this method does not have a well-defined API... if (constants.net.GENESIS[-4:] not in [ "e26f", # mainnet "4943", # testnet 3 "f043", # testnet 4 "1ef6", # signet ] and ".electrum.org" in BaseCrashReporter.report_server): # Gah! Some kind of altcoin wants to send us crash reports. raise Exception(_("Missing report URL.")) report = self.get_traceback_info(*self.exc_args) report.update(self.get_additional_info()) report = json.dumps(report) coro = self.do_post(proxy, BaseCrashReporter.report_server + "/crash.json", data=report) response = asyncio.run_coroutine_threadsafe(coro, asyncio_loop).result(timeout) self.logger.info( f"Crash report sent. Got response [DO NOT TRUST THIS MESSAGE]: {error_text_str_to_safe_str(response)}") response = json.loads(response) assert isinstance(response, dict), type(response) # sanitize URL if location := response.get("location"): assert isinstance(location, str) base_issues_url = constants.GIT_REPO_ISSUES_URL if not base_issues_url.endswith("/"): base_issues_url = base_issues_url + "/" if not location.startswith(base_issues_url): location = None ret = CrashReportResponse( status=response.get("status"), url=location, text=_("Thanks for reporting this issue!"), ) return ret async def do_post(self, proxy: 'ProxySettings', url, data) -> str: async with make_aiohttp_session(proxy) as session: async with session.post(url, data=data, raise_for_status=True) as resp: return await resp.text() @classmethod def get_traceback_info( cls, exctype: type[BaseException], excvalue: BaseException, tb: TracebackType | None, ) -> TypedDict('TBInfo', {'exc_string': str, 'stack': str, 'id': dict[str, str]}): exc_string = str(excvalue) stack = traceback.extract_tb(tb) readable_trace = cls._get_traceback_str_to_send(exctype, excvalue, tb) _id = { "file": stack[-1].filename if len(stack) else '', "name": stack[-1].name if len(stack) else '', "type": exctype.__name__ } # note: this is the "id" the crash reporter server uses to group together reports. return { "exc_string": exc_string, "stack": readable_trace, "id": _id, } @classmethod def get_traceback_groupid_hash( cls, exctype: type[BaseException], excvalue: BaseException, tb: TracebackType | None, ) -> bytes: tb_info = cls.get_traceback_info(exctype, excvalue, tb) _id = tb_info["id"] return sha256(str(_id)) def get_additional_info(self): app_version = (get_git_version() or ELECTRUM_VERSION) if _tainted_by_console: app_version += "-consoletaint" args = { "app_version": app_version, "python_version": sys.version, "os": describe_os_version(), "wallet_type": "unknown", "locale": locale.getlocale()[0] or "?", "description": self.get_user_description() } try: args["wallet_type"] = self.get_wallet_type() except Exception: # Maybe the wallet isn't loaded yet pass return args @classmethod def _get_traceback_str_to_send( cls, exctype: type[BaseException], excvalue: BaseException, tb: TracebackType | None, ) -> str: # make sure that traceback sent to crash reporter contains # e.__context__ and e.__cause__, i.e. if there was a chain of # exceptions, we want the full traceback for the whole chain. return "".join(traceback.format_exception(exctype, excvalue, tb)) def _get_traceback_str_to_display(self) -> str: # overridden in Qt subclass return self._get_traceback_str_to_send(*self.exc_args) def get_report_string(self): info = self.get_additional_info() info["traceback"] = self._get_traceback_str_to_display() return self.issue_template.format(**info) def get_user_description(self): raise NotImplementedError def get_wallet_type(self) -> str: raise NotImplementedError class EarlyExceptionsQueue: """Helper singleton for explicitly sending exceptions to crash reporter. Typically the GUIs set up an "exception hook" that catches all otherwise uncaught exceptions (which unroll the stack of a thread completely). This class provides methods to report *any* exception, and queueing logic that delays processing until the exception hook is set up. """ _is_exc_hook_ready = False _exc_queue = queue.Queue() @classmethod def set_hook_as_ready(cls): """Flush the queue and disable it for future exceptions.""" if cls._is_exc_hook_ready: return cls._is_exc_hook_ready = True while cls._exc_queue.qsize() > 0: e = cls._exc_queue.get() cls._send_exception_to_crash_reporter(e) @classmethod def send_exception_to_crash_reporter(cls, e: BaseException): if cls._is_exc_hook_ready: cls._send_exception_to_crash_reporter(e) else: cls._exc_queue.put(e) @staticmethod def _send_exception_to_crash_reporter(e: BaseException): assert EarlyExceptionsQueue._is_exc_hook_ready sys.excepthook(type(e), e, e.__traceback__) send_exception_to_crash_reporter = EarlyExceptionsQueue.send_exception_to_crash_reporter def trigger_crash(): # note: do not change the type of the exception, the message, # or the name of this method. All reports generated through this # method will be grouped together by the crash reporter, and thus # don't spam the issue tracker. class TestingException(Exception): pass def crash_test(): raise TestingException("triggered crash for testing purposes") import threading t = threading.Thread(target=crash_test) t.start() ================================================ FILE: electrum/bip21.py ================================================ import urllib import urllib.parse import re from decimal import Decimal from typing import Optional from . import bitcoin from .util import format_satoshis_plain from .bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC from .lnaddr import lndecode, LnDecodeException # note: when checking against these, use .lower() to support case-insensitivity BITCOIN_BIP21_URI_SCHEME = 'bitcoin' LIGHTNING_URI_SCHEME = 'lightning' class InvalidBitcoinURI(Exception): pass def parse_bip21_URI(uri: str) -> dict: """Raises InvalidBitcoinURI on malformed URI.""" if not isinstance(uri, str): raise InvalidBitcoinURI(f"expected string, not {repr(uri)}") if ':' not in uri: if not bitcoin.is_address(uri): raise InvalidBitcoinURI("Not a bitcoin address") return {'address': uri} u = urllib.parse.urlparse(uri) if u.scheme.lower() != BITCOIN_BIP21_URI_SCHEME: raise InvalidBitcoinURI("Not a bitcoin URI") address = u.path # python for android fails to parse query if address.find('?') > 0: address, query = u.path.split('?') pq = urllib.parse.parse_qs(query) else: pq = urllib.parse.parse_qs(u.query) for k, v in pq.items(): if len(v) != 1: raise InvalidBitcoinURI(f'Duplicate Key: {repr(k)}') if k.startswith('req-'): # we have no support for any req-* query parameters raise InvalidBitcoinURI(f'Unsupported Key: {repr(k)}') out = {k: v[0] for k, v in pq.items()} if address: if not bitcoin.is_address(address): raise InvalidBitcoinURI(f"Invalid bitcoin address: {address}") out['address'] = address if 'amount' in out: am = out['amount'] try: m = re.match(r'([0-9.]+)X([0-9])', am) if m: k = int(m.group(2)) - 8 amount = Decimal(m.group(1)) * pow(Decimal(10), k) else: amount = Decimal(am) * COIN if amount > TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN or amount <= 0: raise InvalidBitcoinURI(f"amount is out-of-bounds: {amount!r} BTC") out['amount'] = int(amount) except Exception as e: raise InvalidBitcoinURI(f"failed to parse 'amount' field: {repr(e)}") from e if 'message' in out: out['message'] = out['message'] out['memo'] = out['message'] if 'time' in out: try: out['time'] = int(out['time']) except Exception as e: raise InvalidBitcoinURI(f"failed to parse 'time' field: {repr(e)}") from e if 'exp' in out: try: out['exp'] = int(out['exp']) except Exception as e: raise InvalidBitcoinURI(f"failed to parse 'exp' field: {repr(e)}") from e if 'sig' in out: try: out['sig'] = bitcoin.base_decode(out['sig'], base=58).hex() except Exception as e: raise InvalidBitcoinURI(f"failed to parse 'sig' field: {repr(e)}") from e if 'lightning' in out: try: lnaddr = lndecode(out['lightning']) except LnDecodeException as e: raise InvalidBitcoinURI(f"Failed to decode 'lightning' field: {e!r}") from e amount_sat = out.get('amount') if amount_sat: # allow small leeway due to msat precision if lnaddr.get_amount_sat() is None or abs(amount_sat - int(lnaddr.get_amount_sat())) > 1: raise InvalidBitcoinURI("Inconsistent lightning field in bip21: amount") address = out.get('address') ln_fallback_addr = lnaddr.get_fallback_address() if address and ln_fallback_addr: if ln_fallback_addr != address: raise InvalidBitcoinURI("Inconsistent lightning field in bip21: address") return out def create_bip21_uri(addr, amount_sat: Optional[int], message: Optional[str], *, extra_query_params: Optional[dict] = None) -> str: if not bitcoin.is_address(addr): return "" if extra_query_params is None: extra_query_params = {} query = [] if amount_sat: query.append('amount=%s' % format_satoshis_plain(amount_sat)) if message: query.append('message=%s' % urllib.parse.quote(message)) for k, v in extra_query_params.items(): if not isinstance(k, str) or k != urllib.parse.quote(k): raise Exception(f"illegal key for URI: {repr(k)}") v = urllib.parse.quote(v) query.append(f"{k}={v}") p = urllib.parse.ParseResult( scheme=BITCOIN_BIP21_URI_SCHEME, netloc='', path=addr, params='', query='&'.join(query), fragment='' ) return str(urllib.parse.urlunparse(p)) ================================================ FILE: electrum/bip32.py ================================================ # Copyright (C) 2018 The Electrum developers # Distributed under the MIT software license, see the accompanying # file LICENCE or http://www.opensource.org/licenses/mit-license.php import binascii import hashlib import struct from typing import List, Tuple, NamedTuple, Union, Iterable, Sequence, Optional import electrum_ecc as ecc from .util import bfh, BitcoinException from . import constants from .crypto import hash_160, hmac_oneshot from .bitcoin import EncodeBase58Check, DecodeBase58Check from .logging import get_logger _logger = get_logger(__name__) BIP32_PRIME = 0x80000000 UINT32_MAX = (1 << 32) - 1 BIP32_HARDENED_CHAR = "h" # default "hardened" char we put in str paths def protect_against_invalid_ecpoint(func): def func_wrapper(*args): child_index = args[-1] while True: is_prime = child_index & BIP32_PRIME try: return func(*args[:-1], child_index=child_index) except ecc.InvalidECPointException: _logger.warning('bip32 protect_against_invalid_ecpoint: skipping index') child_index += 1 is_prime2 = child_index & BIP32_PRIME if is_prime != is_prime2: raise OverflowError() return func_wrapper @protect_against_invalid_ecpoint def CKD_priv(parent_privkey: bytes, parent_chaincode: bytes, child_index: int) -> Tuple[bytes, bytes]: """Child private key derivation function (from master private key) If n is hardened (i.e. the 32nd bit is set), the resulting private key's corresponding public key can NOT be determined without the master private key. However, if n is not hardened, the resulting private key's corresponding public key can be determined without the master private key. """ if child_index < 0: raise ValueError('the bip32 index needs to be non-negative') is_hardened_child = bool(child_index & BIP32_PRIME) return _CKD_priv(parent_privkey=parent_privkey, parent_chaincode=parent_chaincode, child_index=int.to_bytes(child_index, length=4, byteorder="big", signed=False), is_hardened_child=is_hardened_child) def _CKD_priv(parent_privkey: bytes, parent_chaincode: bytes, child_index: bytes, is_hardened_child: bool) -> Tuple[bytes, bytes]: try: keypair = ecc.ECPrivkey(parent_privkey) except ecc.InvalidECPointException as e: raise BitcoinException('Impossible xprv (not within curve order)') from e parent_pubkey = keypair.get_public_key_bytes(compressed=True) if is_hardened_child: data = bytes([0]) + parent_privkey + child_index else: data = parent_pubkey + child_index I = hmac_oneshot(parent_chaincode, data, hashlib.sha512) I_left = ecc.string_to_number(I[0:32]) child_privkey = (I_left + ecc.string_to_number(parent_privkey)) % ecc.CURVE_ORDER if I_left >= ecc.CURVE_ORDER or child_privkey == 0: raise ecc.InvalidECPointException() child_privkey = int.to_bytes(child_privkey, length=32, byteorder='big', signed=False) child_chaincode = I[32:] return child_privkey, child_chaincode @protect_against_invalid_ecpoint def CKD_pub(parent_pubkey: bytes, parent_chaincode: bytes, child_index: int) -> Tuple[bytes, bytes]: """Child public key derivation function (from public key only) This function allows us to find the nth public key, as long as n is not hardened. If n is hardened, we need the master private key to find it. """ if child_index < 0: raise ValueError('the bip32 index needs to be non-negative') if child_index & BIP32_PRIME: raise Exception('not possible to derive hardened child from parent pubkey') return _CKD_pub(parent_pubkey=parent_pubkey, parent_chaincode=parent_chaincode, child_index=int.to_bytes(child_index, length=4, byteorder="big", signed=False)) # helper function, callable with arbitrary 'child_index' byte-string. # i.e.: 'child_index' does not need to fit into 32 bits here! (c.f. trustedcoin billing) def _CKD_pub(parent_pubkey: bytes, parent_chaincode: bytes, child_index: bytes) -> Tuple[bytes, bytes]: I = hmac_oneshot(parent_chaincode, parent_pubkey + child_index, hashlib.sha512) pubkey = ecc.ECPrivkey(I[0:32]) + ecc.ECPubkey(parent_pubkey) if pubkey.is_at_infinity(): raise ecc.InvalidECPointException() child_pubkey = pubkey.get_public_key_bytes(compressed=True) child_chaincode = I[32:] return child_pubkey, child_chaincode def xprv_header(xtype: str, *, net=None) -> bytes: if net is None: net = constants.net return net.XPRV_HEADERS[xtype].to_bytes(length=4, byteorder="big") def xpub_header(xtype: str, *, net=None) -> bytes: if net is None: net = constants.net return net.XPUB_HEADERS[xtype].to_bytes(length=4, byteorder="big") class InvalidMasterKeyVersionBytes(BitcoinException): pass class BIP32Node(NamedTuple): xtype: str eckey: Union[ecc.ECPubkey, ecc.ECPrivkey] chaincode: bytes depth: int = 0 fingerprint: bytes = b'\x00'*4 # as in serialized format, this is the *parent's* fingerprint child_number: bytes = b'\x00'*4 @classmethod def from_xkey( cls, xkey: str, *, net=None, allow_custom_headers: bool = True, # to also accept ypub/zpub ) -> 'BIP32Node': if net is None: net = constants.net xkey = DecodeBase58Check(xkey) if len(xkey) != 78: raise BitcoinException('Invalid length for extended key: {}' .format(len(xkey))) depth = xkey[4] fingerprint = xkey[5:9] child_number = xkey[9:13] chaincode = xkey[13:13 + 32] header = int.from_bytes(xkey[0:4], byteorder='big') if header in net.XPRV_HEADERS_INV: headers_inv = net.XPRV_HEADERS_INV is_private = True elif header in net.XPUB_HEADERS_INV: headers_inv = net.XPUB_HEADERS_INV is_private = False else: raise InvalidMasterKeyVersionBytes(f'Invalid extended key format: {hex(header)}') xtype = headers_inv[header] if not allow_custom_headers and xtype != "standard": raise ValueError(f"only standard xpub/xprv allowed. found custom xtype={xtype}") if is_private: eckey = ecc.ECPrivkey(xkey[13 + 33:]) else: eckey = ecc.ECPubkey(xkey[13 + 32:]) return BIP32Node(xtype=xtype, eckey=eckey, chaincode=chaincode, depth=depth, fingerprint=fingerprint, child_number=child_number) @classmethod def from_rootseed(cls, seed: bytes, *, xtype: str) -> 'BIP32Node': I = hmac_oneshot(b"Bitcoin seed", seed, hashlib.sha512) master_k = I[0:32] master_c = I[32:] return BIP32Node(xtype=xtype, eckey=ecc.ECPrivkey(master_k), chaincode=master_c) @classmethod def from_bytes(cls, b: bytes) -> 'BIP32Node': if len(b) != 78: raise Exception(f"unexpected xkey raw bytes len {len(b)} != 78") xkey = EncodeBase58Check(b) return cls.from_xkey(xkey) def to_xprv(self, *, net=None) -> str: payload = self.to_xprv_bytes(net=net) return EncodeBase58Check(payload) def to_xprv_bytes(self, *, net=None) -> bytes: if not self.is_private(): raise Exception("cannot serialize as xprv; private key missing") payload = (xprv_header(self.xtype, net=net) + bytes([self.depth]) + self.fingerprint + self.child_number + self.chaincode + bytes([0]) + self.eckey.get_secret_bytes()) assert len(payload) == 78, f"unexpected xprv payload len {len(payload)}" return payload def to_xpub(self, *, net=None) -> str: payload = self.to_xpub_bytes(net=net) return EncodeBase58Check(payload) def to_xpub_bytes(self, *, net=None) -> bytes: payload = (xpub_header(self.xtype, net=net) + bytes([self.depth]) + self.fingerprint + self.child_number + self.chaincode + self.eckey.get_public_key_bytes(compressed=True)) assert len(payload) == 78, f"unexpected xpub payload len {len(payload)}" return payload def to_xkey(self, *, net=None) -> str: if self.is_private(): return self.to_xprv(net=net) else: return self.to_xpub(net=net) def to_bytes(self, *, net=None) -> bytes: if self.is_private(): return self.to_xprv_bytes(net=net) else: return self.to_xpub_bytes(net=net) def convert_to_public(self) -> 'BIP32Node': if not self.is_private(): return self pubkey = ecc.ECPubkey(self.eckey.get_public_key_bytes()) return self._replace(eckey=pubkey) def is_private(self) -> bool: return isinstance(self.eckey, ecc.ECPrivkey) def subkey_at_private_derivation(self, path: Union[str, Iterable[int]]) -> 'BIP32Node': if path is None: raise Exception("derivation path must not be None") if isinstance(path, str): path = convert_bip32_strpath_to_intpath(path) if not self.is_private(): raise Exception("cannot do bip32 private derivation; private key missing") if not path: return self depth = self.depth chaincode = self.chaincode privkey = self.eckey.get_secret_bytes() for child_index in path: parent_privkey = privkey privkey, chaincode = CKD_priv(privkey, chaincode, child_index) depth += 1 parent_pubkey = ecc.ECPrivkey(parent_privkey).get_public_key_bytes(compressed=True) fingerprint = hash_160(parent_pubkey)[0:4] child_number = child_index.to_bytes(length=4, byteorder="big") return BIP32Node(xtype=self.xtype, eckey=ecc.ECPrivkey(privkey), chaincode=chaincode, depth=depth, fingerprint=fingerprint, child_number=child_number) def subkey_at_public_derivation(self, path: Union[str, Iterable[int]]) -> 'BIP32Node': if path is None: raise Exception("derivation path must not be None") if isinstance(path, str): path = convert_bip32_strpath_to_intpath(path) if not path: return self.convert_to_public() depth = self.depth chaincode = self.chaincode pubkey = self.eckey.get_public_key_bytes(compressed=True) for child_index in path: parent_pubkey = pubkey pubkey, chaincode = CKD_pub(pubkey, chaincode, child_index) depth += 1 fingerprint = hash_160(parent_pubkey)[0:4] child_number = child_index.to_bytes(length=4, byteorder="big") return BIP32Node(xtype=self.xtype, eckey=ecc.ECPubkey(pubkey), chaincode=chaincode, depth=depth, fingerprint=fingerprint, child_number=child_number) def calc_fingerprint_of_this_node(self) -> bytes: """Returns the fingerprint of this node. Note that self.fingerprint is of the *parent*. """ # TODO cache this return hash_160(self.eckey.get_public_key_bytes(compressed=True))[0:4] def xpub_type(x: str) -> str: assert x is not None return BIP32Node.from_xkey(x).xtype def is_xpub(text: str) -> bool: try: node = BIP32Node.from_xkey(text) return not node.is_private() except Exception: return False def is_xprv(text: str) -> bool: try: node = BIP32Node.from_xkey(text) return node.is_private() except Exception: return False def xpub_from_xprv(xprv: str) -> str: return BIP32Node.from_xkey(xprv).to_xpub() def convert_bip32_strpath_to_intpath(n: str) -> List[int]: """Convert bip32 path str to list of uint32 integers with prime flags m/0/-1/1' -> [0, 0x80000001, 0x80000001] based on code in trezorlib """ if not n: return [] if n.endswith("/"): n = n[:-1] n = n.split('/') # cut leading "m" if present, but do not require it if n[0] == "m": n = n[1:] path = [] for x in n: if x == '': # gracefully allow repeating "/" chars in path. # makes concatenating paths easier continue prime = 0 if x.endswith("'") or x.endswith("h"): # note: some implementations also accept "H", "p", "P" x = x[:-1] prime = BIP32_PRIME if x.startswith('-'): if prime: raise ValueError(f"bip32 path child index is signalling hardened level in multiple ways") prime = BIP32_PRIME try: x_int = int(x) except ValueError as e: raise ValueError(f"failed to parse bip32 path: {(str(e))}") from None child_index = abs(x_int) | prime if child_index > UINT32_MAX: raise ValueError(f"bip32 path child index too large: {child_index} > {UINT32_MAX}") path.append(child_index) return path def convert_bip32_intpath_to_strpath(path: Sequence[int], *, hardened_char=BIP32_HARDENED_CHAR) -> str: assert isinstance(hardened_char, str), hardened_char assert len(hardened_char) == 1, hardened_char s = "m/" for child_index in path: if not isinstance(child_index, int): raise TypeError(f"bip32 path child index must be int: {child_index}") if not (0 <= child_index <= UINT32_MAX): raise ValueError(f"bip32 path child index out of range: {child_index}") prime = "" if child_index & BIP32_PRIME: prime = hardened_char child_index = child_index ^ BIP32_PRIME s += str(child_index) + prime + '/' # cut trailing "/" s = s[:-1] return s def is_bip32_derivation(s: str) -> bool: try: if not (s == 'm' or s.startswith('m/')): return False convert_bip32_strpath_to_intpath(s) except Exception: return False else: return True def normalize_bip32_derivation(s: Optional[str], *, hardened_char=BIP32_HARDENED_CHAR) -> Optional[str]: if s is None: return None if not is_bip32_derivation(s): raise ValueError(f"invalid bip32 derivation: {s}") ints = convert_bip32_strpath_to_intpath(s) return convert_bip32_intpath_to_strpath(ints, hardened_char=hardened_char) def is_all_public_derivation(path: Union[str, Iterable[int]]) -> bool: """Returns whether all levels in path use non-hardened derivation.""" if isinstance(path, str): path = convert_bip32_strpath_to_intpath(path) for child_index in path: if child_index < 0: raise ValueError('the bip32 index needs to be non-negative') if child_index & BIP32_PRIME: return False return True def root_fp_and_der_prefix_from_xkey(xkey: str) -> Tuple[Optional[str], Optional[str]]: """Returns the root bip32 fingerprint and the derivation path from the root to the given xkey, if they can be determined. Otherwise (None, None). """ node = BIP32Node.from_xkey(xkey) derivation_prefix = None root_fingerprint = None assert node.depth >= 0, node.depth if node.depth == 0: derivation_prefix = 'm' root_fingerprint = node.calc_fingerprint_of_this_node().hex().lower() elif node.depth == 1: child_number_int = int.from_bytes(node.child_number, 'big') derivation_prefix = convert_bip32_intpath_to_strpath([child_number_int]) root_fingerprint = node.fingerprint.hex() return root_fingerprint, derivation_prefix def is_xkey_consistent_with_key_origin_info(xkey: str, *, derivation_prefix: str = None, root_fingerprint: str = None) -> bool: bip32node = BIP32Node.from_xkey(xkey) int_path = None if derivation_prefix is not None: int_path = convert_bip32_strpath_to_intpath(derivation_prefix) if int_path is not None and len(int_path) != bip32node.depth: return False if bip32node.depth == 0: if bfh(root_fingerprint) != bip32node.calc_fingerprint_of_this_node(): return False if bip32node.child_number != bytes(4): return False if int_path is not None and bip32node.depth > 0: if int.from_bytes(bip32node.child_number, 'big') != int_path[-1]: return False if bip32node.depth == 1: if bfh(root_fingerprint) != bip32node.fingerprint: return False return True class KeyOriginInfo: """ Object representing the origin of a key. from https://github.com/bitcoin-core/HWI/blob/5f300d3dee7b317a6194680ad293eaa0962a3cc7/hwilib/key.py # Copyright (c) 2020 The HWI developers # Distributed under the MIT software license. """ def __init__(self, fingerprint: bytes, path: Sequence[int]) -> None: """ :param fingerprint: The 4 byte BIP 32 fingerprint of a parent key from which this key is derived from :param path: The derivation path to reach this key from the key at ``fingerprint`` """ self.fingerprint: bytes = fingerprint self.path: Sequence[int] = path @classmethod def deserialize(cls, s: bytes) -> 'KeyOriginInfo': """ Deserialize a serialized KeyOriginInfo. They will be serialized in the same way that PSBTs serialize derivation paths """ fingerprint = s[0:4] s = s[4:] path = list(struct.unpack("<" + "I" * (len(s) // 4), s)) return cls(fingerprint, path) def serialize(self) -> bytes: """ Serializes the KeyOriginInfo in the same way that derivation paths are stored in PSBTs """ r = self.fingerprint r += struct.pack("<" + "I" * len(self.path), *self.path) return r def _path_string(self) -> str: strpath = self.get_derivation_path() if len(strpath) >= 2: assert strpath.startswith("m/") return strpath[1:] # cut leading "m" def to_string(self) -> str: """ Return the KeyOriginInfo as a string in the form ///... This is the same way that KeyOriginInfo is shown in descriptors """ s = binascii.hexlify(self.fingerprint).decode() s += self._path_string() return s @classmethod def from_string(cls, s: str) -> 'KeyOriginInfo': """ Create a KeyOriginInfo from the string :param s: The string to parse """ s = s.lower() entries = s.split("/") fingerprint = binascii.unhexlify(s[0:8]) path: Sequence[int] = [] if len(entries) > 1: path = convert_bip32_strpath_to_intpath(s[9:]) return cls(fingerprint, path) def get_derivation_path(self) -> str: """ Return the string for just the path """ return convert_bip32_intpath_to_strpath(self.path) def get_full_int_list(self) -> List[int]: """ Return a list of ints representing this KeyOriginInfo. The first int is the fingerprint, followed by the path """ xfp = [struct.unpack(" bool: if not isinstance(other, KeyOriginInfo): return False return self.serialize() == other.serialize() def __repr__(self) -> str: return f"" ================================================ FILE: electrum/bip39_recovery.py ================================================ # Copyright (C) 2020 The Electrum developers # Distributed under the MIT software license, see the accompanying # file LICENCE or http://www.opensource.org/licenses/mit-license.php from typing import TYPE_CHECKING, Optional import itertools from . import bitcoin from .constants import BIP39_WALLET_FORMATS from .bip32 import BIP32_PRIME, BIP32Node from .bip32 import convert_bip32_strpath_to_intpath as bip32_str_to_ints from .bip32 import convert_bip32_intpath_to_strpath as bip32_ints_to_str from .util import OldTaskGroup, NetworkOfflineException if TYPE_CHECKING: from .network import Network async def account_discovery(network: Optional['Network'], get_account_xpub): if network is None: raise NetworkOfflineException() async with OldTaskGroup() as group: account_scan_tasks = [] for wallet_format in BIP39_WALLET_FORMATS: account_scan = scan_for_active_accounts(network, get_account_xpub, wallet_format) account_scan_tasks.append(await group.spawn(account_scan)) active_accounts = [] for task in account_scan_tasks: active_accounts.extend(task.result()) return active_accounts async def scan_for_active_accounts(network: 'Network', get_account_xpub, wallet_format): active_accounts = [] account_path = bip32_str_to_ints(wallet_format["derivation_path"]) while True: account_xpub = get_account_xpub(account_path) account_node = BIP32Node.from_xkey(account_xpub) has_history = await account_has_history(network, account_node, wallet_format["script_type"]) if has_history: account = format_account(wallet_format, account_path) active_accounts.append(account) if not has_history or not wallet_format["iterate_accounts"]: break account_path[-1] = account_path[-1] + 1 return active_accounts async def account_has_history(network: 'Network', account_node: BIP32Node, script_type: str) -> bool: # note: scan both receiving and change addresses. some wallets send change across accounts. path_suffixes = itertools.chain( itertools.product((0,), range(20)), # ad-hoc gap limits itertools.product((1,), range(10)), ) async with OldTaskGroup() as group: get_history_tasks = [] for path_suffix in path_suffixes: address_node = account_node.subkey_at_public_derivation(path_suffix) pubkey = address_node.eckey.get_public_key_hex() address = bitcoin.pubkey_to_address(script_type, pubkey) script = bitcoin.address_to_script(address) scripthash = bitcoin.script_to_scripthash(script) get_history = network.get_history_for_scripthash(scripthash) get_history_tasks.append(await group.spawn(get_history)) for task in get_history_tasks: history = task.result() if len(history) > 0: return True return False def format_account(wallet_format, account_path): description = wallet_format["description"] if wallet_format["iterate_accounts"]: account_index = account_path[-1] % BIP32_PRIME description = f'{description} (Account {account_index})' return { "description": description, "derivation_path": bip32_ints_to_str(account_path), "script_type": wallet_format["script_type"], } ================================================ FILE: electrum/bip39_wallet_formats.json ================================================ [ { "description": "Standard BIP44 legacy", "derivation_path": "m/44'/0'/0'", "script_type": "p2pkh", "iterate_accounts": true }, { "description": "Standard BIP49 compatibility segwit", "derivation_path": "m/49'/0'/0'", "script_type": "p2wpkh-p2sh", "iterate_accounts": true }, { "description": "Standard BIP84 native segwit", "derivation_path": "m/84'/0'/0'", "script_type": "p2wpkh", "iterate_accounts": true }, { "description": "Non-standard legacy", "derivation_path": "m/0'", "script_type": "p2pkh", "iterate_accounts": true }, { "description": "Non-standard compatibility segwit", "derivation_path": "m/0'", "script_type": "p2wpkh-p2sh", "iterate_accounts": true }, { "description": "Non-standard native segwit", "derivation_path": "m/0'", "script_type": "p2wpkh", "iterate_accounts": true }, { "description": "Non-standard legacy on BIP84 path", "derivation_path": "m/84'/0'/0'", "script_type": "p2pkh", "iterate_accounts": true }, { "description": "Non-standard compatibility segwit on BIP84 path", "derivation_path": "m/84'/0'/0'", "script_type": "p2wpkh-p2sh", "iterate_accounts": true }, { "description": "Non-standard legacy on BIP49 path", "derivation_path": "m/49'/0'/0'", "script_type": "p2pkh", "iterate_accounts": true }, { "description": "Non-standard native segwit on BIP49 path", "derivation_path": "m/49'/0'/0'", "script_type": "p2wpkh", "iterate_accounts": true }, { "description": "Copay native segwit", "derivation_path": "m/44'/0'/0'", "script_type": "p2wpkh", "iterate_accounts": true }, { "description": "Coolwallet S derivation path using bip44 but with segwit script format", "derivation_path": "m/44'/0'/0'", "script_type": "p2wpkh-p2sh", "iterate_accounts": true }, { "description": "Samourai Bad Bank (toxic change)", "derivation_path": "m/84'/0'/2147483644'", "script_type": "p2wpkh", "iterate_accounts": false }, { "description": "Samourai Whirlpool Pre Mix", "derivation_path": "m/84'/0'/2147483645'", "script_type": "p2wpkh", "iterate_accounts": false }, { "description": "Samourai Whirlpool Post Mix", "derivation_path": "m/84'/0'/2147483646'", "script_type": "p2wpkh", "iterate_accounts": false }, { "description": "Samourai Ricochet legacy", "derivation_path": "m/44'/0'/2147483647'", "script_type": "p2pkh", "iterate_accounts": false }, { "description": "Samourai Ricochet compatibility segwit", "derivation_path": "m/49'/0'/2147483647'", "script_type": "p2wpkh-p2sh", "iterate_accounts": false }, { "description": "Samourai Ricochet native segwit", "derivation_path": "m/84'/0'/2147483647'", "script_type": "p2wpkh", "iterate_accounts": false } ] ================================================ FILE: electrum/bitcoin.py ================================================ # -*- coding: utf-8 -*- # # Electrum - lightweight Bitcoin client # Copyright (C) 2011 thomasv@gitorious # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files # (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, # publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from typing import Tuple, TYPE_CHECKING, Optional, Union, Sequence, Mapping, Any import enum from enum import IntEnum, Enum import electrum_ecc as ecc from electrum_ecc.util import bip340_tagged_hash from .util import bfh, BitcoinException, assert_bytes, to_bytes, inv_dict, is_hex_str, classproperty from . import segwit_addr from . import constants from .crypto import sha256d, sha256, hash_160 if TYPE_CHECKING: from .network import Network from .transaction import OPPushDataGeneric ################################## transactions COINBASE_MATURITY = 100 COIN = 100000000 TOTAL_COIN_SUPPLY_LIMIT_IN_BTC = 21000000 NLOCKTIME_MIN = 0 NLOCKTIME_BLOCKHEIGHT_MAX = 500_000_000 - 1 NLOCKTIME_MAX = 2 ** 32 - 1 # supported types of transaction outputs # TODO kill these with fire TYPE_ADDRESS = 0 TYPE_PUBKEY = 1 TYPE_SCRIPT = 2 class opcodes(IntEnum): # push value OP_0 = 0x00 OP_FALSE = OP_0 OP_PUSHDATA1 = 0x4c OP_PUSHDATA2 = 0x4d OP_PUSHDATA4 = 0x4e OP_1NEGATE = 0x4f OP_RESERVED = 0x50 OP_1 = 0x51 OP_TRUE = OP_1 OP_2 = 0x52 OP_3 = 0x53 OP_4 = 0x54 OP_5 = 0x55 OP_6 = 0x56 OP_7 = 0x57 OP_8 = 0x58 OP_9 = 0x59 OP_10 = 0x5a OP_11 = 0x5b OP_12 = 0x5c OP_13 = 0x5d OP_14 = 0x5e OP_15 = 0x5f OP_16 = 0x60 # control OP_NOP = 0x61 OP_VER = 0x62 OP_IF = 0x63 OP_NOTIF = 0x64 OP_VERIF = 0x65 OP_VERNOTIF = 0x66 OP_ELSE = 0x67 OP_ENDIF = 0x68 OP_VERIFY = 0x69 OP_RETURN = 0x6a # stack ops OP_TOALTSTACK = 0x6b OP_FROMALTSTACK = 0x6c OP_2DROP = 0x6d OP_2DUP = 0x6e OP_3DUP = 0x6f OP_2OVER = 0x70 OP_2ROT = 0x71 OP_2SWAP = 0x72 OP_IFDUP = 0x73 OP_DEPTH = 0x74 OP_DROP = 0x75 OP_DUP = 0x76 OP_NIP = 0x77 OP_OVER = 0x78 OP_PICK = 0x79 OP_ROLL = 0x7a OP_ROT = 0x7b OP_SWAP = 0x7c OP_TUCK = 0x7d # splice ops OP_CAT = 0x7e OP_SUBSTR = 0x7f OP_LEFT = 0x80 OP_RIGHT = 0x81 OP_SIZE = 0x82 # bit logic OP_INVERT = 0x83 OP_AND = 0x84 OP_OR = 0x85 OP_XOR = 0x86 OP_EQUAL = 0x87 OP_EQUALVERIFY = 0x88 OP_RESERVED1 = 0x89 OP_RESERVED2 = 0x8a # numeric OP_1ADD = 0x8b OP_1SUB = 0x8c OP_2MUL = 0x8d OP_2DIV = 0x8e OP_NEGATE = 0x8f OP_ABS = 0x90 OP_NOT = 0x91 OP_0NOTEQUAL = 0x92 OP_ADD = 0x93 OP_SUB = 0x94 OP_MUL = 0x95 OP_DIV = 0x96 OP_MOD = 0x97 OP_LSHIFT = 0x98 OP_RSHIFT = 0x99 OP_BOOLAND = 0x9a OP_BOOLOR = 0x9b OP_NUMEQUAL = 0x9c OP_NUMEQUALVERIFY = 0x9d OP_NUMNOTEQUAL = 0x9e OP_LESSTHAN = 0x9f OP_GREATERTHAN = 0xa0 OP_LESSTHANOREQUAL = 0xa1 OP_GREATERTHANOREQUAL = 0xa2 OP_MIN = 0xa3 OP_MAX = 0xa4 OP_WITHIN = 0xa5 # crypto OP_RIPEMD160 = 0xa6 OP_SHA1 = 0xa7 OP_SHA256 = 0xa8 OP_HASH160 = 0xa9 OP_HASH256 = 0xaa OP_CODESEPARATOR = 0xab OP_CHECKSIG = 0xac OP_CHECKSIGVERIFY = 0xad OP_CHECKMULTISIG = 0xae OP_CHECKMULTISIGVERIFY = 0xaf # expansion OP_NOP1 = 0xb0 OP_CHECKLOCKTIMEVERIFY = 0xb1 OP_NOP2 = OP_CHECKLOCKTIMEVERIFY OP_CHECKSEQUENCEVERIFY = 0xb2 OP_NOP3 = OP_CHECKSEQUENCEVERIFY OP_NOP4 = 0xb3 OP_NOP5 = 0xb4 OP_NOP6 = 0xb5 OP_NOP7 = 0xb6 OP_NOP8 = 0xb7 OP_NOP9 = 0xb8 OP_NOP10 = 0xb9 OP_INVALIDOPCODE = 0xff def hex(self) -> str: return bytes([self]).hex() def script_num_to_bytes(i: int) -> bytes: """See CScriptNum in Bitcoin Core. Encodes an integer as bytes, to be used in script. ported from https://github.com/bitcoin/bitcoin/blob/8cbc5c4be4be22aca228074f087a374a7ec38be8/src/script/script.h#L326 """ if i == 0: return b"" result = bytearray() neg = i < 0 absvalue = abs(i) while absvalue > 0: result.append(absvalue & 0xff) absvalue >>= 8 if result[-1] & 0x80: result.append(0x80 if neg else 0x00) elif neg: result[-1] |= 0x80 return bytes(result) def var_int(i: int) -> bytes: # https://en.bitcoin.it/wiki/Protocol_specification#Variable_length_integer # https://github.com/bitcoin/bitcoin/blob/efe1ee0d8d7f82150789f1f6840f139289628a2b/src/serialize.h#L247 # "CompactSize" assert i >= 0, i if i < 0xfd: return int.to_bytes(i, length=1, byteorder="little", signed=False) elif i <= 0xffff: return b"\xfd" + int.to_bytes(i, length=2, byteorder="little", signed=False) elif i <= 0xffffffff: return b"\xfe" + int.to_bytes(i, length=4, byteorder="little", signed=False) else: return b"\xff" + int.to_bytes(i, length=8, byteorder="little", signed=False) def witness_push(item: bytes) -> bytes: """Returns data in the form it should be present in the witness.""" return var_int(len(item)) + item def _op_push(i: int) -> bytes: if i < opcodes.OP_PUSHDATA1: return int.to_bytes(i, length=1, byteorder="little", signed=False) elif i <= 0xff: return bytes([opcodes.OP_PUSHDATA1]) + int.to_bytes(i, length=1, byteorder="little", signed=False) elif i <= 0xffff: return bytes([opcodes.OP_PUSHDATA2]) + int.to_bytes(i, length=2, byteorder="little", signed=False) else: return bytes([opcodes.OP_PUSHDATA4]) + int.to_bytes(i, length=4, byteorder="little", signed=False) def push_script(data: bytes) -> bytes: """Returns pushed data to the script, automatically choosing canonical opcodes depending on the length of the data. ported from https://github.com/btcsuite/btcd/blob/fdc2bc867bda6b351191b5872d2da8270df00d13/txscript/scriptbuilder.go#L128 """ data_len = len(data) # "small integer" opcodes if data_len == 0 or data_len == 1 and data[0] == 0: return bytes([opcodes.OP_0]) elif data_len == 1 and data[0] <= 16: return bytes([opcodes.OP_1 - 1 + data[0]]) elif data_len == 1 and data[0] == 0x81: return bytes([opcodes.OP_1NEGATE]) return _op_push(data_len) + data def make_op_return(x: bytes) -> bytes: return bytes([opcodes.OP_RETURN]) + push_script(x) def add_number_to_script(i: int) -> bytes: return push_script(script_num_to_bytes(i)) def construct_witness(items: Sequence[Union[str, int, bytes]]) -> bytes: """Constructs a witness from the given stack items.""" witness = bytearray() witness += var_int(len(items)) for item in items: if type(item) is int: item = script_num_to_bytes(item) elif isinstance(item, (bytes, bytearray)): pass # use as-is else: assert is_hex_str(item), repr(item) item = bfh(item) witness += witness_push(item) return bytes(witness) def construct_script( items: Sequence[Union[str, int, bytes, opcodes, 'OPPushDataGeneric']], *, values: Optional[Mapping[int, Any]] = None, # can be used to substitute into OPPushDataGeneric ) -> bytes: """Constructs bitcoin script from given items.""" from .transaction import OPPushDataGeneric script = bytearray() values = values or {} for i, item in enumerate(items): if i in values: assert OPPushDataGeneric.is_instance(item), f"tried to substitute into {item=!r}" item = values[i] if isinstance(item, opcodes): script += bytes([item]) elif type(item) is int: script += add_number_to_script(item) elif isinstance(item, (bytes, bytearray)): script += push_script(item) elif isinstance(item, str): assert is_hex_str(item) script += push_script(bfh(item)) else: raise Exception(f'unexpected item for script: {item!r} at idx={i}') return bytes(script) def relayfee(network: 'Network' = None) -> int: """Returns feerate in sat/kbyte.""" from .fee_policy import FEERATE_MIN_RELAY, FEERATE_DEFAULT_RELAY, FEERATE_MAX_RELAY if network and network.relay_fee is not None: fee = network.relay_fee else: fee = FEERATE_DEFAULT_RELAY # sanity safeguards, as network.relay_fee is coming from a server: fee = min(fee, FEERATE_MAX_RELAY) fee = max(fee, FEERATE_MIN_RELAY) return fee # see https://github.com/bitcoin/bitcoin/blob/a62f0ed64f8bbbdfe6467ac5ce92ef5b5222d1bd/src/policy/policy.cpp#L14 # and https://github.com/lightningnetwork/lightning-rfc/blob/7e3dce42cbe4fa4592320db6a4e06c26bb99122b/03-transactions.md#dust-limits DUST_LIMIT_P2PKH = 546 DUST_LIMIT_P2SH = 540 DUST_LIMIT_UNKNOWN_SEGWIT = 354 DUST_LIMIT_P2WSH = 330 DUST_LIMIT_P2WPKH = 294 def dust_threshold(network: 'Network' = None) -> int: """Returns the dust limit in satoshis.""" return DUST_LIMIT_P2PKH def hash_encode(x: bytes) -> str: return x[::-1].hex() def hash_decode(x: str) -> bytes: return bfh(x)[::-1] ############ functions from pywallet ##################### def hash160_to_b58_address(h160: bytes, addrtype: int) -> str: s = bytes([addrtype]) + h160 s = s + sha256d(s)[0:4] return base_encode(s, base=58) def b58_address_to_hash160(addr: str) -> Tuple[int, bytes]: addr = to_bytes(addr, 'ascii') _bytes = DecodeBase58Check(addr) if len(_bytes) != 21: raise Exception(f'expected 21 payload bytes in base58 address. got: {len(_bytes)}') return _bytes[0], _bytes[1:21] def hash160_to_p2pkh(h160: bytes, *, net=None) -> str: if net is None: net = constants.net return hash160_to_b58_address(h160, net.ADDRTYPE_P2PKH) def hash160_to_p2sh(h160: bytes, *, net=None) -> str: if net is None: net = constants.net return hash160_to_b58_address(h160, net.ADDRTYPE_P2SH) def public_key_to_p2pkh(public_key: bytes, *, net=None) -> str: return hash160_to_p2pkh(hash_160(public_key), net=net) def hash_to_segwit_addr(h: bytes, witver: int, *, net=None) -> str: if net is None: net = constants.net addr = segwit_addr.encode_segwit_address(net.SEGWIT_HRP, witver, h) assert addr is not None return addr def public_key_to_p2wpkh(public_key: bytes, *, net=None) -> str: return hash_to_segwit_addr(hash_160(public_key), witver=0, net=net) def script_to_p2wsh(script: bytes, *, net=None) -> str: return hash_to_segwit_addr(sha256(script), witver=0, net=net) def p2wsh_nested_script(witness_script: bytes) -> bytes: wsh = sha256(witness_script) return construct_script([0, wsh]) def pubkey_to_address(txin_type: str, pubkey: str, *, net=None) -> str: from . import descriptor desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey, script_type=txin_type) return desc.expand().address(net=net) # TODO this method is confusingly named def redeem_script_to_address(txin_type: str, scriptcode: bytes, *, net=None) -> str: assert isinstance(scriptcode, bytes) if txin_type == 'p2sh': # given scriptcode is a redeem_script return hash160_to_p2sh(hash_160(scriptcode), net=net) elif txin_type == 'p2wsh': # given scriptcode is a witness_script return script_to_p2wsh(scriptcode, net=net) elif txin_type == 'p2wsh-p2sh': # given scriptcode is a witness_script redeem_script = p2wsh_nested_script(scriptcode) return hash160_to_p2sh(hash_160(redeem_script), net=net) else: raise NotImplementedError(txin_type) def script_to_address(script: bytes, *, net=None) -> Optional[str]: from .transaction import get_address_from_output_script return get_address_from_output_script(script, net=net) def address_to_script(addr: str, *, net=None) -> bytes: if net is None: net = constants.net if not is_address(addr, net=net): raise BitcoinException(f"invalid bitcoin address: {addr}") witver, witprog = segwit_addr.decode_segwit_address(net.SEGWIT_HRP, addr) if witprog is not None: if not (0 <= witver <= 16): raise BitcoinException(f'impossible witness version: {witver}') return construct_script([witver, bytes(witprog)]) addrtype, hash_160_ = b58_address_to_hash160(addr) if addrtype == net.ADDRTYPE_P2PKH: script = pubkeyhash_to_p2pkh_script(hash_160_) elif addrtype == net.ADDRTYPE_P2SH: script = construct_script([opcodes.OP_HASH160, hash_160_, opcodes.OP_EQUAL]) else: raise BitcoinException(f'unknown address type: {addrtype}') return script class OnchainOutputType(Enum): """Opaque types of scriptPubKeys. In case of p2sh, p2wsh and similar, no knowledge of redeem script, etc. """ P2PKH = enum.auto() P2SH = enum.auto() WITVER0_P2WPKH = enum.auto() WITVER0_P2WSH = enum.auto() WITVER1_P2TR = enum.auto() def address_to_payload(addr: str, *, net=None) -> Tuple[OnchainOutputType, bytes]: """Return (type, pubkey hash / witness program) for an address.""" if net is None: net = constants.net if not is_address(addr, net=net): raise BitcoinException(f"invalid bitcoin address: {addr}") witver, witprog = segwit_addr.decode_segwit_address(net.SEGWIT_HRP, addr) if witprog is not None: if witver == 0: if len(witprog) == 20: return OnchainOutputType.WITVER0_P2WPKH, bytes(witprog) elif len(witprog) == 32: return OnchainOutputType.WITVER0_P2WSH, bytes(witprog) else: raise BitcoinException(f"unexpected length for segwit witver=0 witprog: len={len(witprog)}") elif witver == 1: if len(witprog) == 32: return OnchainOutputType.WITVER1_P2TR, bytes(witprog) else: raise BitcoinException(f"unexpected length for segwit witver=1 witprog: len={len(witprog)}") else: raise BitcoinException(f"not implemented handling for witver={witver}") addrtype, hash_160_ = b58_address_to_hash160(addr) if addrtype == net.ADDRTYPE_P2PKH: return OnchainOutputType.P2PKH, hash_160_ elif addrtype == net.ADDRTYPE_P2SH: return OnchainOutputType.P2SH, hash_160_ raise BitcoinException(f"unknown address type: {addrtype}") def address_to_scripthash(addr: str, *, net=None) -> str: script = address_to_script(addr, net=net) return script_to_scripthash(script) def script_to_scripthash(script: bytes) -> str: h = sha256(script) return h[::-1].hex() def pubkeyhash_to_p2pkh_script(pubkey_hash160: bytes) -> bytes: return construct_script([ opcodes.OP_DUP, opcodes.OP_HASH160, pubkey_hash160, opcodes.OP_EQUALVERIFY, opcodes.OP_CHECKSIG ]) __b58chars = b'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' assert len(__b58chars) == 58 __b58chars_inv = inv_dict(dict(enumerate(__b58chars))) __b43chars = b'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ$*+-./:' assert len(__b43chars) == 43 __b43chars_inv = inv_dict(dict(enumerate(__b43chars))) class BaseDecodeError(BitcoinException): pass def base_encode(v: bytes, *, base: int) -> str: """ encode v, which is a string of bytes, to base58.""" assert_bytes(v) if base not in (58, 43): raise ValueError('not supported base: {}'.format(base)) chars = __b58chars if base == 43: chars = __b43chars origlen = len(v) v = v.lstrip(b'\x00') newlen = len(v) num = int.from_bytes(v, byteorder='big') string = b"" while num: num, idx = divmod(num, base) string = chars[idx:idx + 1] + string result = chars[0:1] * (origlen - newlen) + string return result.decode('ascii') def base_decode(v: Union[bytes, str], *, base: int) -> Optional[bytes]: """ decode v into a string of len bytes. based on the work of David Keijser in https://github.com/keis/base58 """ # assert_bytes(v) v = to_bytes(v, 'ascii') if base not in (58, 43): raise ValueError('not supported base: {}'.format(base)) chars = __b58chars chars_inv = __b58chars_inv if base == 43: chars = __b43chars chars_inv = __b43chars_inv origlen = len(v) v = v.lstrip(chars[0:1]) newlen = len(v) num = 0 try: for char in v: num = num * base + chars_inv[char] except KeyError: raise BaseDecodeError('Forbidden character {} for base {}'.format(char, base)) return num.to_bytes(origlen - newlen + (num.bit_length() + 7) // 8, 'big') class InvalidChecksum(BaseDecodeError): pass def EncodeBase58Check(vchIn: bytes) -> str: hash = sha256d(vchIn) return base_encode(vchIn + hash[0:4], base=58) def DecodeBase58Check(psz: Union[bytes, str]) -> bytes: vchRet = base_decode(psz, base=58) payload = vchRet[0:-4] csum_found = vchRet[-4:] csum_calculated = sha256d(payload)[0:4] if csum_calculated != csum_found: raise InvalidChecksum(f'calculated {csum_calculated.hex()}, found {csum_found.hex()}') else: return payload # backwards compat # extended WIF for segwit (used in 3.0.x; but still used internally) # the keys in this dict should be a superset of what Imported Wallets can import WIF_SCRIPT_TYPES = { 'p2pkh': 0, 'p2wpkh': 1, 'p2wpkh-p2sh': 2, 'p2sh': 5, 'p2wsh': 6, 'p2wsh-p2sh': 7 } WIF_SCRIPT_TYPES_INV = inv_dict(WIF_SCRIPT_TYPES) def is_segwit_script_type(txin_type: str) -> bool: return txin_type in ('p2wpkh', 'p2wpkh-p2sh', 'p2wsh', 'p2wsh-p2sh') def serialize_privkey(secret: bytes, compressed: bool, txin_type: str, *, internal_use: bool = False) -> str: # we only export secrets inside curve range secret = ecc.ECPrivkey.normalize_secret_bytes(secret) if internal_use: prefix = bytes([(WIF_SCRIPT_TYPES[txin_type] + constants.net.WIF_PREFIX) & 255]) else: prefix = bytes([constants.net.WIF_PREFIX]) suffix = b'\01' if compressed else b'' vchIn = prefix + secret + suffix base58_wif = EncodeBase58Check(vchIn) if internal_use: return base58_wif else: return '{}:{}'.format(txin_type, base58_wif) def deserialize_privkey(key: str) -> Tuple[str, bytes, bool]: if is_minikey(key): return 'p2pkh', minikey_to_private_key(key), False txin_type = None if ':' in key: txin_type, key = key.split(sep=':', maxsplit=1) if txin_type not in WIF_SCRIPT_TYPES: raise BitcoinException('unknown script type: {}'.format(txin_type)) try: vch = DecodeBase58Check(key) except Exception as e: neutered_privkey = str(key)[:3] + '..' + str(key)[-2:] raise BaseDecodeError(f"cannot deserialize privkey {neutered_privkey}") from e if txin_type is None: # keys exported in version 3.0.x encoded script type in first byte prefix_value = vch[0] - constants.net.WIF_PREFIX try: txin_type = WIF_SCRIPT_TYPES_INV[prefix_value] except KeyError as e: raise BitcoinException('invalid prefix ({}) for WIF key (1)'.format(vch[0])) from None else: # all other keys must have a fixed first byte if vch[0] != constants.net.WIF_PREFIX: raise BitcoinException('invalid prefix ({}) for WIF key (2)'.format(vch[0])) if len(vch) not in [33, 34]: raise BitcoinException('invalid vch len for WIF key: {}'.format(len(vch))) compressed = False if len(vch) == 34: if vch[33] == 0x01: compressed = True else: raise BitcoinException(f'invalid WIF key. length suggests compressed pubkey, ' f'but last byte is {vch[33]} != 0x01') if is_segwit_script_type(txin_type) and not compressed: raise BitcoinException('only compressed public keys can be used in segwit scripts') secret_bytes = vch[1:33] # we accept secrets outside curve range; cast into range here: secret_bytes = ecc.ECPrivkey.normalize_secret_bytes(secret_bytes) return txin_type, secret_bytes, compressed def is_compressed_privkey(sec: str) -> bool: return deserialize_privkey(sec)[2] def address_from_private_key(sec: str) -> str: txin_type, privkey, compressed = deserialize_privkey(sec) public_key = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed) return pubkey_to_address(txin_type, public_key) def is_segwit_address(addr: str, *, net=None) -> bool: if net is None: net = constants.net try: witver, witprog = segwit_addr.decode_segwit_address(net.SEGWIT_HRP, addr) except Exception as e: return False return witprog is not None def is_taproot_address(addr: str, *, net=None) -> bool: if net is None: net = constants.net try: witver, witprog = segwit_addr.decode_segwit_address(net.SEGWIT_HRP, addr) except Exception as e: return False return witver == 1 def is_b58_address(addr: str, *, net=None) -> bool: if net is None: net = constants.net try: # test length, checksum, encoding: addrtype, h = b58_address_to_hash160(addr) except Exception as e: return False if addrtype not in [net.ADDRTYPE_P2PKH, net.ADDRTYPE_P2SH]: return False return True def is_address(addr: str, *, net=None) -> bool: return is_segwit_address(addr, net=net) \ or is_b58_address(addr, net=net) def is_private_key(key: str, *, raise_on_error=False) -> bool: try: deserialize_privkey(key) return True except BaseException as e: if raise_on_error: raise return False ########### end pywallet functions ####################### def is_minikey(text: str) -> bool: # Minikeys are typically 22 or 30 characters, but this routine # permits any length of 20 or more provided the minikey is valid. # A valid minikey must begin with an 'S', be in base58, and when # suffixed with '?' have its SHA256 hash begin with a zero byte. # They are widely used in Casascius physical bitcoins. return (len(text) >= 20 and text[0] == 'S' and all(ord(c) in __b58chars for c in text) and sha256(text + '?')[0] == 0x00) def minikey_to_private_key(text: str) -> bytes: return sha256(text) def _get_dummy_address(purpose: str) -> str: return redeem_script_to_address('p2wsh', sha256(bytes(purpose, "utf8"))) _dummy_addr_funcs = set() class DummyAddress: """dummy address for fee estimation of funding tx Use e.g. as: DummyAddress.CHANNEL """ def purpose(func): _dummy_addr_funcs.add(func) return classproperty(func) @purpose def CHANNEL(self) -> str: return _get_dummy_address("channel") @purpose def SWAP(self) -> str: return _get_dummy_address("swap") @classmethod def is_dummy_address(cls, addr: str) -> bool: return addr in (f(cls) for f in _dummy_addr_funcs) class DummyAddressUsedInTxException(Exception): pass def taproot_tweak_pubkey(pubkey32: bytes, h: bytes) -> Tuple[int, bytes]: assert isinstance(pubkey32, bytes), type(pubkey32) assert isinstance(h, bytes), type(h) assert len(pubkey32) == 32, len(pubkey32) int_from_bytes = lambda x: int.from_bytes(x, byteorder="big", signed=False) tweak = int_from_bytes(bip340_tagged_hash(b"TapTweak", pubkey32 + h)) if tweak >= ecc.CURVE_ORDER: raise ValueError P = ecc.ECPubkey(b"\x02" + pubkey32) Q = P + (ecc.GENERATOR * tweak) return 0 if Q.has_even_y() else 1, Q.get_public_key_bytes(compressed=True)[1:] def taproot_tweak_seckey(seckey0: bytes, h: bytes) -> bytes: assert isinstance(seckey0, bytes), type(seckey0) assert isinstance(h, bytes), type(h) assert len(seckey0) == 32, len(seckey0) int_from_bytes = lambda x: int.from_bytes(x, byteorder="big", signed=False) P = ecc.ECPrivkey(seckey0) seckey = P.secret_scalar if P.has_even_y() else ecc.CURVE_ORDER - P.secret_scalar pubkey32 = P.get_public_key_bytes(compressed=True)[1:] tweak = int_from_bytes(bip340_tagged_hash(b"TapTweak", pubkey32 + h)) if tweak >= ecc.CURVE_ORDER: raise ValueError return int.to_bytes((seckey + tweak) % ecc.CURVE_ORDER, length=32, byteorder="big", signed=False) # a TapTree is either: # - a (leaf_version, script) tuple (leaf_version is 0xc0 for BIP-0342 scripts) # - a list of two elements, each with the same structure as TapTree itself TapTreeLeaf = Tuple[int, bytes] TapTree = Union[TapTreeLeaf, Sequence['TapTree']] def taproot_tree_helper(script_tree: TapTree): if isinstance(script_tree, tuple): leaf_version, script = script_tree h = bip340_tagged_hash(b"TapLeaf", bytes([leaf_version]) + witness_push(script)) return [((leaf_version, script), bytes())], h left, left_h = taproot_tree_helper(script_tree[0]) right, right_h = taproot_tree_helper(script_tree[1]) ret = [(l, c + right_h) for l, c in left] + [(l, c + left_h) for l, c in right] if right_h < left_h: left_h, right_h = right_h, left_h return ret, bip340_tagged_hash(b"TapBranch", left_h + right_h) def taproot_output_script(internal_pubkey: bytes, *, script_tree: Optional[TapTree]) -> bytes: """Given an internal public key and a tree of scripts, compute the output script.""" assert isinstance(internal_pubkey, bytes), type(internal_pubkey) assert len(internal_pubkey) == 32, len(internal_pubkey) if script_tree is None: merkle_root = bytes() else: _, merkle_root = taproot_tree_helper(script_tree) _, output_pubkey = taproot_tweak_pubkey(internal_pubkey, merkle_root) return construct_script([1, output_pubkey]) def control_block_for_taproot_script_spend( *, internal_pubkey: bytes, script_tree: TapTree, script_num: int, ) -> Tuple[bytes, bytes]: """Constructs the control block necessary for spending a taproot UTXO using a script. script_num indicates which script to use, which indexes into (flattened) script_tree. """ assert isinstance(internal_pubkey, bytes), type(internal_pubkey) assert len(internal_pubkey) == 32, len(internal_pubkey) info, merkle_root = taproot_tree_helper(script_tree) (leaf_version, leaf_script), merkle_path = info[script_num] output_pubkey_y_parity, _ = taproot_tweak_pubkey(internal_pubkey, merkle_root) pubkey_data = bytes([output_pubkey_y_parity + leaf_version]) + internal_pubkey control_block = pubkey_data + merkle_path return (leaf_script, control_block) # user message signing def usermessage_magic(message: bytes) -> bytes: length = var_int(len(message)) return b"\x18Bitcoin Signed Message:\n" + length + message def ecdsa_sign_usermessage(ec_privkey, message: Union[bytes, str], *, is_compressed: bool) -> bytes: message = to_bytes(message, 'utf8') msg32 = sha256d(usermessage_magic(message)) return ec_privkey.ecdsa_sign_recoverable(msg32, is_compressed=is_compressed) def verify_usermessage_with_address(address: str, sig65: bytes, message: bytes, *, net=None) -> bool: from electrum_ecc import ECPubkey assert_bytes(sig65, message) if net is None: net = constants.net h = sha256d(usermessage_magic(message)) try: public_key, compressed, txin_type_guess = ECPubkey.from_ecdsa_sig65(sig65, h) except Exception as e: return False # check public key using the address pubkey_hex = public_key.get_public_key_hex(compressed) txin_types = (txin_type_guess,) if txin_type_guess else ('p2pkh', 'p2wpkh', 'p2wpkh-p2sh') for txin_type in txin_types: addr = pubkey_to_address(txin_type, pubkey_hex, net=net) if address == addr: break else: return False # check message # note: `$ bitcoin-cli verifymessage` does NOT enforce the low-S rule for ecdsa sigs return public_key.ecdsa_verify(sig65[1:], h, enforce_low_s=False) ================================================ FILE: electrum/blockchain.py ================================================ # Electrum - lightweight Bitcoin client # Copyright (C) 2012 thomasv@ecdsa.org # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files # (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, # publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import os import threading import time from typing import Optional, Dict, Mapping, Sequence, TYPE_CHECKING from . import util from .bitcoin import hash_encode from .crypto import sha256d from . import constants from .util import bfh, with_lock from .logging import get_logger, Logger if TYPE_CHECKING: from .simple_config import SimpleConfig _logger = get_logger(__name__) HEADER_SIZE = 80 # bytes CHUNK_SIZE = 2016 # num headers in a difficulty retarget period # see https://github.com/bitcoin/bitcoin/blob/feedb9c84e72e4fff489810a2bbeec09bcda5763/src/chainparams.cpp#L76 MAX_TARGET = 0x00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff # compact: 0x1d00ffff class MissingHeader(Exception): pass class InvalidHeader(Exception): pass def serialize_header(header_dict: dict) -> bytes: s = ( int.to_bytes(header_dict['version'], length=4, byteorder="little", signed=False) + bfh(header_dict['prev_block_hash'])[::-1] + bfh(header_dict['merkle_root'])[::-1] + int.to_bytes(int(header_dict['timestamp']), length=4, byteorder="little", signed=False) + int.to_bytes(int(header_dict['bits']), length=4, byteorder="little", signed=False) + int.to_bytes(int(header_dict['nonce']), length=4, byteorder="little", signed=False)) return s def deserialize_header(s: bytes, height: int) -> dict: if not s: raise InvalidHeader('Invalid header: {}'.format(s)) if len(s) != HEADER_SIZE: raise InvalidHeader('Invalid header length: {}'.format(len(s))) h = {} h['version'] = int.from_bytes(s[0:4], byteorder='little') h['prev_block_hash'] = hash_encode(s[4:36]) h['merkle_root'] = hash_encode(s[36:68]) h['timestamp'] = int.from_bytes(s[68:72], byteorder='little') h['bits'] = int.from_bytes(s[72:76], byteorder='little') h['nonce'] = int.from_bytes(s[76:80], byteorder='little') h['block_height'] = height return h def hash_header(header: dict) -> str: if header is None: return '0' * 64 if header.get('prev_block_hash') is None: header['prev_block_hash'] = '00'*32 return hash_raw_header(serialize_header(header)) def hash_raw_header(header: bytes) -> str: assert isinstance(header, bytes) return hash_encode(sha256d(header)) pow_hash_header = hash_header # key: blockhash hex at forkpoint # the chain at some key is the best chain that includes the given hash blockchains = {} # type: Dict[str, Blockchain] blockchains_lock = threading.RLock() # lock order: take this last; so after Blockchain.lock def read_blockchains(config: 'SimpleConfig'): best_chain = Blockchain(config=config, forkpoint=0, parent=None, forkpoint_hash=constants.net.GENESIS, prev_hash=None) blockchains[constants.net.GENESIS] = best_chain # consistency checks if best_chain.height() > constants.net.max_checkpoint(): header_after_cp = best_chain.read_header(constants.net.max_checkpoint()+1) if not header_after_cp or not best_chain.can_connect(header_after_cp, check_height=False): _logger.info("[blockchain] deleting best chain. cannot connect header after last cp to last cp.") os.unlink(best_chain.path()) best_chain.update_size() # forks fdir = os.path.join(util.get_headers_dir(config), 'forks') util.make_dir(fdir) # files are named as: fork2_{forkpoint}_{prev_hash}_{first_hash} l = filter(lambda x: x.startswith('fork2_') and '.' not in x, os.listdir(fdir)) l = sorted(l, key=lambda x: int(x.split('_')[1])) # sort by forkpoint def delete_chain(filename, reason): _logger.info(f"[blockchain] deleting chain {filename}: {reason}") os.unlink(os.path.join(fdir, filename)) def instantiate_chain(filename): __, forkpoint, prev_hash, first_hash = filename.split('_') forkpoint = int(forkpoint) prev_hash = (64-len(prev_hash)) * "0" + prev_hash # left-pad with zeroes first_hash = (64-len(first_hash)) * "0" + first_hash # forks below the max checkpoint are not allowed if forkpoint <= constants.net.max_checkpoint(): delete_chain(filename, "deleting fork below max checkpoint") return # find parent (sorting by forkpoint guarantees it's already instantiated) for parent in blockchains.values(): if parent.check_hash(forkpoint - 1, prev_hash): break else: delete_chain(filename, "cannot find parent for chain") return b = Blockchain(config=config, forkpoint=forkpoint, parent=parent, forkpoint_hash=first_hash, prev_hash=prev_hash) # consistency checks h = b.read_header(b.forkpoint) if first_hash != hash_header(h): delete_chain(filename, "incorrect first hash for chain") return if not b.parent.can_connect(h, check_height=False): delete_chain(filename, "cannot connect chain to parent") return chain_id = b.get_id() assert first_hash == chain_id, (first_hash, chain_id) blockchains[chain_id] = b for filename in l: instantiate_chain(filename) def get_best_chain() -> 'Blockchain': return blockchains[constants.net.GENESIS] # block hash -> chain work; up to and including that block _CHAINWORK_CACHE = { "0000000000000000000000000000000000000000000000000000000000000000": 0, # virtual block at height -1 } # type: Dict[str, int] def init_headers_file_for_best_chain(): b = get_best_chain() filename = b.path() length = HEADER_SIZE * len(constants.net.CHECKPOINTS) * CHUNK_SIZE if not os.path.exists(filename) or os.path.getsize(filename) < length: with open(filename, 'wb') as f: if length > 0: f.seek(length - 1) f.write(b'\x00') util.ensure_sparse_file(filename) with b.lock: b.update_size() class Blockchain(Logger): """ Manages blockchain headers and their verification """ def __init__(self, config: 'SimpleConfig', forkpoint: int, parent: Optional['Blockchain'], forkpoint_hash: str, prev_hash: Optional[str]): assert isinstance(forkpoint_hash, str) and len(forkpoint_hash) == 64, forkpoint_hash assert (prev_hash is None) or (isinstance(prev_hash, str) and len(prev_hash) == 64), prev_hash # assert (parent is None) == (forkpoint == 0) if 0 < forkpoint <= constants.net.max_checkpoint(): raise Exception(f"cannot fork below max checkpoint. forkpoint: {forkpoint}") Logger.__init__(self) self.config = config self.forkpoint = forkpoint # height of first header self.parent = parent self._forkpoint_hash = forkpoint_hash # blockhash at forkpoint. "first hash" self._prev_hash = prev_hash # blockhash immediately before forkpoint self.lock = threading.RLock() self.update_size() @property def checkpoints(self): return constants.net.CHECKPOINTS def get_max_child(self) -> Optional[int]: children = self.get_direct_children() return max([x.forkpoint for x in children]) if children else None def get_max_forkpoint(self) -> int: """Returns the max height where there is a fork related to this chain. """ mc = self.get_max_child() return mc if mc is not None else self.forkpoint def get_direct_children(self) -> Sequence['Blockchain']: with blockchains_lock: return list(filter(lambda y: y.parent==self, blockchains.values())) def get_parent_heights(self) -> Mapping['Blockchain', int]: """Returns map: (parent chain -> height of last common block)""" with self.lock, blockchains_lock: result = {self: self.height()} chain = self while True: parent = chain.parent if parent is None: break result[parent] = chain.forkpoint - 1 chain = parent return result def get_height_of_last_common_block_with_chain(self, other_chain: 'Blockchain') -> int: last_common_block_height = 0 our_parents = self.get_parent_heights() their_parents = other_chain.get_parent_heights() for chain in our_parents: if chain in their_parents: h = min(our_parents[chain], their_parents[chain]) last_common_block_height = max(last_common_block_height, h) return last_common_block_height @with_lock def get_branch_size(self) -> int: return self.height() - self.get_max_forkpoint() + 1 def get_name(self) -> str: return self.get_hash(self.get_max_forkpoint()).lstrip('0')[0:10] def check_header(self, header: dict) -> bool: header_hash = hash_header(header) height = header.get('block_height') return self.check_hash(height, header_hash) def check_hash(self, height: int, header_hash: str) -> bool: """Returns whether the hash of the block at given height is the given hash. """ assert isinstance(header_hash, str) and len(header_hash) == 64, header_hash # hex try: return header_hash == self.get_hash(height) except Exception: return False def fork(parent, header: dict) -> 'Blockchain': if not parent.can_connect(header, check_height=False): raise Exception("forking header does not connect to parent chain") forkpoint = header.get('block_height') self = Blockchain(config=parent.config, forkpoint=forkpoint, parent=parent, forkpoint_hash=hash_header(header), prev_hash=parent.get_hash(forkpoint-1)) self.assert_headers_file_available(parent.path()) open(self.path(), 'w+').close() self.save_header(header) # put into global dict. note that in some cases # save_header might have already put it there but that's OK chain_id = self.get_id() with blockchains_lock: blockchains[chain_id] = self return self @with_lock def height(self) -> int: return self.forkpoint + self.size() - 1 @with_lock def size(self) -> int: return self._size @with_lock def update_size(self) -> None: p = self.path() self._size = os.path.getsize(p)//HEADER_SIZE if os.path.exists(p) else 0 @classmethod def verify_header(cls, header: dict, prev_hash: str, target: int, expected_header_hash: str=None) -> None: _hash = hash_header(header) if expected_header_hash and expected_header_hash != _hash: raise InvalidHeader("hash mismatches with expected: {} vs {}".format(expected_header_hash, _hash)) if prev_hash != header.get('prev_block_hash'): raise InvalidHeader("prev hash mismatch: %s vs %s" % (prev_hash, header.get('prev_block_hash'))) if constants.net.TESTNET: return bits = cls.target_to_bits(target) if bits != header.get('bits'): raise InvalidHeader("bits mismatch: %s vs %s" % (bits, header.get('bits'))) _pow_hash = pow_hash_header(header) pow_hash_as_num = int.from_bytes(bfh(_pow_hash), byteorder='big') if pow_hash_as_num > target: raise InvalidHeader(f"insufficient proof of work: {pow_hash_as_num} vs target {target}") def verify_chunk(self, index: int, data: bytes) -> None: num = len(data) // HEADER_SIZE start_height = index * CHUNK_SIZE prev_hash = self.get_hash(start_height - 1) target = self.get_target(index-1) for i in range(num): height = start_height + i try: expected_header_hash = self.get_hash(height) except MissingHeader: expected_header_hash = None raw_header = data[i*HEADER_SIZE : (i+1)*HEADER_SIZE] header = deserialize_header(raw_header, index*CHUNK_SIZE + i) self.verify_header(header, prev_hash, target, expected_header_hash) prev_hash = hash_header(header) @with_lock def path(self): d = util.get_headers_dir(self.config) if self.parent is None: filename = 'blockchain_headers' else: assert self.forkpoint > 0, self.forkpoint prev_hash = self._prev_hash.lstrip('0') first_hash = self._forkpoint_hash.lstrip('0') basename = f'fork2_{self.forkpoint}_{prev_hash}_{first_hash}' filename = os.path.join('forks', basename) return os.path.join(d, filename) @with_lock def save_chunk(self, index: int, chunk: bytes): assert index >= 0, index chunk_within_checkpoint_region = index < len(self.checkpoints) # chunks in checkpoint region are the responsibility of the 'main chain' if chunk_within_checkpoint_region and self.parent is not None: main_chain = get_best_chain() main_chain.save_chunk(index, chunk) return delta_height = (index * CHUNK_SIZE - self.forkpoint) delta_bytes = delta_height * HEADER_SIZE # if this chunk contains our forkpoint, only save the part after forkpoint # (the part before is the responsibility of the parent) if delta_bytes < 0: chunk = chunk[-delta_bytes:] delta_bytes = 0 truncate = not chunk_within_checkpoint_region self.write(chunk, delta_bytes, truncate) self.swap_with_parent() def swap_with_parent(self) -> None: with self.lock, blockchains_lock: # do the swap; possibly multiple ones cnt = 0 while True: old_parent = self.parent if not self._swap_with_parent(): break # make sure we are making progress cnt += 1 if cnt > len(blockchains): raise Exception(f'swapping fork with parent too many times: {cnt}') # we might have become the parent of some of our former siblings for old_sibling in old_parent.get_direct_children(): if self.check_hash(old_sibling.forkpoint - 1, old_sibling._prev_hash): old_sibling.parent = self def _swap_with_parent(self) -> bool: """Check if this chain became stronger than its parent, and swap the underlying files if so. The Blockchain instances will keep 'containing' the same headers, but their ids change and so they will be stored in different files.""" if self.parent is None: return False if self.parent.get_chainwork() >= self.get_chainwork(): return False self.logger.info(f"swapping {self.forkpoint} {self.parent.forkpoint}") parent_branch_size = self.parent.height() - self.forkpoint + 1 forkpoint = self.forkpoint # type: Optional[int] parent = self.parent # type: Optional[Blockchain] child_old_id = self.get_id() parent_old_id = parent.get_id() # swap files # child takes parent's name # parent's new name will be something new (not child's old name) self.assert_headers_file_available(self.path()) child_old_name = self.path() with open(self.path(), 'rb') as f: my_data = f.read() self.assert_headers_file_available(parent.path()) assert forkpoint > parent.forkpoint, (f"forkpoint of parent chain ({parent.forkpoint}) " f"should be at lower height than children's ({forkpoint})") with open(parent.path(), 'rb') as f: f.seek((forkpoint - parent.forkpoint)*HEADER_SIZE) parent_data = f.read(parent_branch_size*HEADER_SIZE) self.write(parent_data, 0) parent.write(my_data, (forkpoint - parent.forkpoint)*HEADER_SIZE) # swap parameters self.parent, parent.parent = parent.parent, self # type: Optional[Blockchain], Optional[Blockchain] self.forkpoint, parent.forkpoint = parent.forkpoint, self.forkpoint self._forkpoint_hash, parent._forkpoint_hash = parent._forkpoint_hash, hash_raw_header(parent_data[:HEADER_SIZE]) self._prev_hash, parent._prev_hash = parent._prev_hash, self._prev_hash # parent's new name os.replace(child_old_name, parent.path()) self.update_size() parent.update_size() # update pointers blockchains.pop(child_old_id, None) blockchains.pop(parent_old_id, None) blockchains[self.get_id()] = self blockchains[parent.get_id()] = parent return True def get_id(self) -> str: return self._forkpoint_hash def assert_headers_file_available(self, path): if os.path.exists(path): return elif not os.path.exists(util.get_headers_dir(self.config)): raise FileNotFoundError('Electrum headers_dir does not exist. Was it deleted while running?') else: raise FileNotFoundError('Cannot find headers file but headers_dir is there. Should be at {}'.format(path)) @with_lock def write(self, data: bytes, offset: int, truncate: bool = True, *, fsync: bool = True) -> None: filename = self.path() self.assert_headers_file_available(filename) with open(filename, 'rb+') as f: if truncate and offset != self._size * HEADER_SIZE: f.seek(offset) f.truncate() f.seek(offset) f.write(data) if fsync: f.flush() os.fsync(f.fileno()) self.update_size() @with_lock def save_header(self, header: dict) -> None: delta = header.get('block_height') - self.forkpoint data = serialize_header(header) # headers are only _appended_ to the end: assert delta == self.size(), (delta, self.size()) assert len(data) == HEADER_SIZE # note: we don't fsync, to improve perf. losing headers at end of file is ok. self.write(data, delta*HEADER_SIZE, fsync=False) self.swap_with_parent() @with_lock def read_header(self, height: int) -> Optional[dict]: if height < 0: return if height < self.forkpoint: return self.parent.read_header(height) if height > self.height(): return delta = height - self.forkpoint name = self.path() self.assert_headers_file_available(name) with open(name, 'rb') as f: f.seek(delta * HEADER_SIZE) h = f.read(HEADER_SIZE) if len(h) < HEADER_SIZE: raise Exception('Expected to read a full header. This was only {} bytes'.format(len(h))) if h == bytes([0])*HEADER_SIZE: return None return deserialize_header(h, height) def header_at_tip(self) -> Optional[dict]: """Return latest header.""" height = self.height() return self.read_header(height) def is_tip_stale(self) -> bool: STALE_DELAY = 8 * 60 * 60 # in seconds header = self.header_at_tip() if not header: return True # note: We check the timestamp only in the latest header. # The Bitcoin consensus has a lot of leeway here: # - needs to be greater than the median of the timestamps of the past 11 blocks, and # - up to at most 2 hours into the future compared to local clock # so there is ~2 hours of leeway in either direction if header['timestamp'] + STALE_DELAY < time.time(): return True return False def get_hash(self, height: int) -> str: def is_height_checkpoint(): within_cp_range = height <= constants.net.max_checkpoint() at_chunk_boundary = (height+1) % CHUNK_SIZE == 0 return within_cp_range and at_chunk_boundary if height == -1: return '0000000000000000000000000000000000000000000000000000000000000000' elif height == 0: return constants.net.GENESIS elif is_height_checkpoint(): index = height // CHUNK_SIZE h, t = self.checkpoints[index] return h else: header = self.read_header(height) if header is None: raise MissingHeader(height) return hash_header(header) def get_target(self, index: int) -> int: # compute target from chunk x, used in chunk x+1 if constants.net.TESTNET: return 0 if index == -1: return MAX_TARGET if index < len(self.checkpoints): h, t = self.checkpoints[index] return t # new target first = self.read_header(index * CHUNK_SIZE) last = self.read_header((index+1) * CHUNK_SIZE - 1) if not first or not last: raise MissingHeader() bits = last.get('bits') target = self.bits_to_target(bits) nActualTimespan = last.get('timestamp') - first.get('timestamp') nTargetTimespan = 14 * 24 * 60 * 60 nActualTimespan = max(nActualTimespan, nTargetTimespan // 4) nActualTimespan = min(nActualTimespan, nTargetTimespan * 4) new_target = min(MAX_TARGET, (target * nActualTimespan) // nTargetTimespan) # not any target can be represented in 32 bits: new_target = self.bits_to_target(self.target_to_bits(new_target)) return new_target @classmethod def bits_to_target(cls, bits: int) -> int: # arith_uint256::SetCompact in Bitcoin Core if not (0 <= bits < (1 << 32)): raise InvalidHeader(f"bits should be uint32. got {bits!r}") bitsN = (bits >> 24) & 0xff bitsBase = bits & 0x7fffff if bitsN <= 3: target = bitsBase >> (8 * (3-bitsN)) else: target = bitsBase << (8 * (bitsN-3)) if target != 0 and bits & 0x800000 != 0: # Bit number 24 (0x800000) represents the sign of N raise InvalidHeader("target cannot be negative") if (target != 0 and (bitsN > 34 or (bitsN > 33 and bitsBase > 0xff) or (bitsN > 32 and bitsBase > 0xffff))): raise InvalidHeader("target has overflown") return target @classmethod def target_to_bits(cls, target: int) -> int: # arith_uint256::GetCompact in Bitcoin Core # see https://github.com/bitcoin/bitcoin/blob/7fcf53f7b4524572d1d0c9a5fdc388e87eb02416/src/arith_uint256.cpp#L223 c = target.to_bytes(length=32, byteorder='big') bitsN = len(c) while bitsN > 0 and c[0] == 0: c = c[1:] bitsN -= 1 if len(c) < 3: c += b'\x00' bitsBase = int.from_bytes(c[:3], byteorder='big') if bitsBase >= 0x800000: bitsN += 1 bitsBase >>= 8 return bitsN << 24 | bitsBase def chainwork_of_header_at_height(self, height: int) -> int: """work done by single header at given height""" chunk_idx = height // CHUNK_SIZE - 1 target = self.get_target(chunk_idx) work = ((2 ** 256 - target - 1) // (target + 1)) + 1 return work @with_lock def get_chainwork(self, height=None) -> int: if height is None: height = max(0, self.height()) if constants.net.TESTNET: # On testnet/regtest, difficulty works somewhat different. # It's out of scope to properly implement that. return height last_retarget = height // CHUNK_SIZE * CHUNK_SIZE - 1 cached_height = last_retarget while _CHAINWORK_CACHE.get(self.get_hash(cached_height)) is None: if cached_height <= -1: break cached_height -= CHUNK_SIZE assert cached_height >= -1, cached_height running_total = _CHAINWORK_CACHE[self.get_hash(cached_height)] while cached_height < last_retarget: cached_height += CHUNK_SIZE work_in_single_header = self.chainwork_of_header_at_height(cached_height) work_in_chunk = CHUNK_SIZE * work_in_single_header running_total += work_in_chunk _CHAINWORK_CACHE[self.get_hash(cached_height)] = running_total cached_height += CHUNK_SIZE work_in_single_header = self.chainwork_of_header_at_height(cached_height) work_in_last_partial_chunk = (height % CHUNK_SIZE + 1) * work_in_single_header return running_total + work_in_last_partial_chunk def can_connect(self, header: dict, *, check_height: bool = True) -> bool: if header is None: return False height = header['block_height'] if check_height and self.height() != height - 1: return False if height == 0: return hash_header(header) == constants.net.GENESIS try: prev_hash = self.get_hash(height - 1) except Exception: return False if prev_hash != header.get('prev_block_hash'): return False try: target = self.get_target(height // CHUNK_SIZE - 1) except MissingHeader: return False try: self.verify_header(header, prev_hash, target) except BaseException as e: return False return True def connect_chunk(self, idx: int, data: bytes) -> bool: assert idx >= 0, idx try: self.verify_chunk(idx, data) self.save_chunk(idx, data) return True except BaseException as e: self.logger.info(f'verify_chunk idx {idx} failed: {repr(e)}') return False def get_checkpoints(self): # for each chunk, store the hash of the last block and the target after the chunk cp = [] n = self.height() // CHUNK_SIZE for index in range(n): h = self.get_hash((index+1) * CHUNK_SIZE -1) target = self.get_target(index) cp.append((h, target)) return cp def check_header(header: dict) -> Optional[Blockchain]: """Returns any Blockchain that contains header, or None.""" if type(header) is not dict: return None with blockchains_lock: chains = list(blockchains.values()) for b in chains: if b.check_header(header): return b return None def can_connect(header: dict) -> Optional[Blockchain]: """Returns the Blockchain that has a tip that directly links up with header, or None. """ with blockchains_lock: chains = list(blockchains.values()) for b in chains: if b.can_connect(header): return b return None def get_chains_that_contain_header(height: int, header_hash: str) -> Sequence[Blockchain]: """Returns a list of Blockchains that contain header, best chain first.""" with blockchains_lock: chains = list(blockchains.values()) chains = [chain for chain in chains if chain.check_hash(height=height, header_hash=header_hash)] chains = sorted(chains, key=lambda x: x.get_chainwork(), reverse=True) return chains ================================================ FILE: electrum/chains/mainnet/checkpoints.json ================================================ [ [ "00000000693067b0e6b440bc51450b9f3850561b07f6d3c021c54fbd6abb9763", 26959535291011309493156476344723991336010898738574164086137773096960 ], [ "00000000f037ad09d0b05ee66b8c1da83030abaf909d2b1bf519c3c7d2cd3fdf", 26959535291011309493156476344723991336010898738574164086137773096960 ], [ "000000006ce8b5f16fcedde13acbc9641baa1c67734f177d770a4069c06c9de8", 26959535291011309493156476344723991336010898738574164086137773096960 ], [ "00000000563298de120522b5ae17da21aaae02eee2d7fcb5be65d9224dbd601c", 26959535291011309493156476344723991336010898738574164086137773096960 ], [ "000000009b0a4b2833b4a0aa61171ee75b8eb301ac45a18713795a72e461a946", 26959535291011309493156476344723991336010898738574164086137773096960 ], [ "00000000fa8a7363e8f6fdc88ec55edf264c9c7b31268c26e497a4587c750584", 26959535291011309493156476344723991336010898738574164086137773096960 ], [ "000000008ac55b5cd76a5c176f2457f0e9df5ff1c719d939f1022712b1ba2092", 26959535291011309493156476344723991336010898738574164086137773096960 ], [ "000000007f0c796631f00f542c0b402d638d3518bc208f8c9e5d29d2f169c084", 26959535291011309493156476344723991336010898738574164086137773096960 ], [ "00000000ffb062296c9d4eb5f87bbf905d30669d26eab6bced341bd3f1dba5fd", 26959535291011309493156476344723991336010898738574164086137773096960 ], [ "0000000074c108842c3ec2252bba62db4050bf0dddfee3ddaa5f847076b8822f", 26959535291011309493156476344723991336010898738574164086137773096960 ], [ "0000000067dc2f84a73fbf5d3c70678ce4a1496ef3a62c557bc79cbdd1d49f22", 26959535291011309493156476344723991336010898738574164086137773096960 ], [ "00000000dbf06f47c0624262ecb197bccf6bdaaabc2d973708ac401ac8955acc", 26959535291011309493156476344723991336010898738574164086137773096960 ], [ "000000009260fe30ec89ef367122f429dcc59f61735760f2b2288f2e854f04ac", 26959535291011309493156476344723991336010898738574164086137773096960 ], [ "00000000f9f1a700898c4e0671af6efd441eaf339ba075a5c5c7b0949473c80b", 26959535291011309493156476344723991336010898738574164086137773096960 ], [ "000000005107662c86452e7365f32f8ffdc70d8d87aa6f78630a79f7d77fbfe6", 26959535291011309493156476344723991336010898738574164086137773096960 ], [ "00000000984f962134a7291e3693075ae03e521f0ee33378ec30a334d860034b", 22791060871177364286867400663010583169263383106957897897309909286912 ], [ "000000005e36047e39452a7beaaa6721048ac408a3e75bb60a8b0008713653ce", 20657664212610420653213483117824978239553266057163961604478437687296 ], [ "00000000128d789579ffbec00203a371cbb39cee27df35d951fd66e62ed59258", 20055820920770189543295303139304627292355830414308479769458683936768 ], [ "000000008dde642fb80481bb5e1671cb04c6716de5b7f783aa3388456d5c8a85", 14823939180767414932263578623363531361763221729526512593941781544960 ], [ "000000008135b689ad1557d4e148a8b9e58e2c4a67240fc87962abb69710231a", 10665477591887247494381404907447500979192021944764506987270680608768 ], [ "00000000308496ef3e4f9fa542a772df637b4aaf1dcce404424611feacfc09e7", 7129927859545590787920041835044506526699926406309469412482969763840 ], [ "000000001a2e0c63d7d012003c9173acfd04ccd6372027718979228c461b5ed5", 5949911473257063494842414979623989957440207170696926280907451531264 ], [ "000000002e0c0ac26ccde91b51ab018576b3a126b413e9f6f787b36637f1b174", 5905492491837656485645884063467495540781288435542782321354050895872 ], [ "00000000103226f85fe2b68795f087dcec345e523363f18017e60b5c94175355", 4430143390146946405787502162943966061454423600514874825749833973760 ], [ "000000001ae6f66fd4de47f8d6f357e798943bbfc4f39ebf14b0975fab059173", 3447600406241317909690675945127070282093452846402311540118831235072 ], [ "000000000a3f22690162744d3bc0b674c92e661a25afb3d2ac8b39b27ac14373", 2351604382534916182160036119666703740669209516522695514729880748032 ], [ "0000000006dc436c3c515a97446af858c1203a501c85d26c4a30afa380aba4a1", 2098151686442211199940455690614286210348997571531298297574806519808 ], [ "000000000943fe1680ffcc498ce50790ff8e842a8af2c157664e4fbc1cb7cb46", 2275790652544821279950241890112140030244814501479017131553197129728 ], [ "000000000847b2144376c1fb057ea1d5a027d5a6004277ed4c72422e93df04e9", 1622203955679450683159610732218403647246163922223729367236739072000 ], [ "00000000094505954deb1d31382b86d0510fd280a34143400b1856a4d52b4c93", 1551048739079662593758612650769536967206480773659027300489594142720 ], [ "000000000109272cecb3f7e98ac12cf149fa8a1b2aaab248e1b006b0dc595a3a", 1389323280429349294447518501872137680563441219958739463959193059328 ], [ "0000000009e6aa0fe39b790625ffeb18a2d6ff5060a5bd14e699e83c54109977", 1147152896345386682952518188670047452875537662186691235300769792000 ], [ "0000000000d14af55c4eae0121184919baba2deb8bf89c3af6b8e4c4f35c8e4e", 594007861936424273334637371358095438347537381057796937154824241152 ], [ "0000000003dfbfa2b33707e691ab2ab7cda7503be2c2cce43d1b21cd1cc757fb", 148501965484106068333659342839523859586884345264449234288706060288 ], [ "0000000000c169d181d66d242901f70d006f3e088c1ae9cacb88b94b8266e9c3", 110393429764504113949181711819653188468070301266890302199533928448 ], [ "000000000009f7d1439d6a2fc1a456db8e843674275bf0133fc7b43b5f45b96e", 76554528428498296726819074079132986384157750623812250673757552640 ], [ "000000000011b8a8fad7973548b50f6d4b2ba1690f7487c374e43248c576354f", 52678642966898219212816601311127992435882858542187514726849708032 ], [ "000000000077e856b6cc475d9cf784119811214c9cac8d7b674ec24faa7c2c0c", 43246870766561725070861386869077695524372774526710079316876591104 ], [ "00000000004cbb474f2cbf3a65f690efa09804512af3351ba3a0888c806c6625", 37817516728945957090904676150631917288430706594442690521085247488 ], [ "0000000000235b1ec6656d8e91f3dde3b6ab9ad7e75b332e4da9355ce60d860e", 29373101246077110899697012205905070265841442578602225419818106880 ], [ "00000000002a153a2c95a8e5493db93086b0e3fe590b636a5871ace57523ef93", 20444488966645742314409346972440253478913291170842138088329707520 ], [ "00000000000e9550e084908cf91a4e8b74f9f1315d1bc4020709f9e7f261bb18", 19563849255781403323327768731100757126732627316116500830377476096 ], [ "00000000002c2cfef3bb85b463d3fcd39b73a6d3d5ae11c1e2a8113e3794f28d", 12545026348036226200394850922278603223904369245268262607334146048 ], [ "00000000000fa92b757ee29674aa97e98a49ba3ad340d2baa94155d71648dfe1", 8719867261221084516486306056196045840260667577454435863762042880 ], [ "0000000000030571601dbc8e13d00d45004eee6ea8b6ab3cdfb38d2546fee21c", 5942996718418989293499865695368015163438891473576991811912597504 ], [ "00000000000bb6adef42e63082b20fd2b1dc1b324c51973512a4c31f29a9986e", 3926013280397599483741094494745234959951218212740030386090803200 ], [ "000000000000765094788a98dbb8adac30d248b7129b59b1441ee2b7ef9e332f", 3337321571246095014985518819479127172783474909736415373333364736 ], [ "00000000000431a0aa9625f82975709f3c6f4f64d04c559512af051599872084", 2200419182034594781720344474937177839165432393990533906392154112 ], [ "00000000000292b850b8f8578e6b4d03cbb4a78ada44afbb4d2f80a16490e8f9", 1861311314983800126815643622927230076368334845814253369901973504 ], [ "0000000000025afe84e27423011af25f777e5a94545dbd00fd04bebe9050f7dd", 1653206561150525499452195696179626311675293455763937233695932416 ], [ "0000000000000e389cccae2a40437be574fd806909e24136711e7f8bce671d65", 1462200632444444190489436459820840230299714881944341127503020032 ], [ "0000000000030510bf6bc1649726cf2e6e4010c64a2c8fd3fde5dc92535ca40e", 1224744150896501443874292381730317417444978877835711165914677248 ], [ "00000000000082648057f14fc835779c6ce46a407bafb2e5c2ac1d20d9f4e822", 1036989760889350435547200084292752907272941324136347429599444992 ], [ "000000000000f38accd6b22959010471a6d9f159d43bf2a9d4c53c220201254e", 739430030225080220618328322475016688484025266646974337550123008 ], [ "0000000000004ed7a73133678b5eb883cd8882bf14dfb26c104ae0c3f94cf4ee", 484975157177710342494716926626447514974484083994735770500857856 ], [ "00000000000037bb3ff4cf649a1757d4028ecc10f893529b4a2214792c981f96", 353833947722011807976659613996792948209273674048993161457434624 ], [ "0000000000008008f46559fe7f181e9dc0648f213472a1e576e8bf506b88f22f", 390843739553851677760235428436025349398613161749553108945469440 ], [ "000000000000691d0c2444db713bf6c088844cc95a37cdc55cc269bb0a31d8c8", 327394795212563108599383268946242257264650552916910648089116672 ], [ "00000000000071153b0afcc64a425f8442c29749610797119e732dd4b723f675", 291935447509363748964474894494542149680088347011133317125767168 ], [ "000000000000a384acb522e4e5935ad2bc31366ecf1f16f1f11023e967ef033d", 245823858161213192073337185391658632187400443916100519594033152 ], [ "0000000000002e532093d43e901292121fb7c6583caf2d13b666fe7e194b4a97", 171262555713783851185422181139260521316022447660158187451973632 ], [ "00000000000033e435c4bbddc7eb255146aa7f18e61a832983af3a9ee5dd144d", 110438984653392107399822606842181601255647711092336854093004800 ], [ "00000000000028ff4b0bd45f0e3e713f91fa1821d28a276a1a1f32f786662f13", 61993465896324436412959469550829248888675813063783317791309824 ], [ "0000000000001ef9c75318e116a607af4de68fb4f67c788677ee6779fb5fa0d5", 47525089675259291211422247200069659468817014361857087365971968 ], [ "0000000000000e6e98694ccb8247aad63aaa1e2bec5a7be14329407e4cea6223", 30742228348699538311994447367921718297595975288392383715082240 ], [ "000000000000000a2153574b2523a6d1844c3cb82d085e2575846dd8c5d4ebb4", 19547336162709893274575855467812492508787617050928192350584832 ], [ "00000000000002a92c1b1ffb2a8388979cf30798e312335ae2a1b922927ee83d", 17248274092338559882155796390905381469049315669915374897332224 ], [ "00000000000004d54b1422ce733922e7672a4e2ecc86dcf96c0de06565cddaa6", 15943936487596784557029840069157210316687734428242467413295104 ], [ "00000000000009dd91ae96cbbf67af42340b0bc715b3606aa725f630b470262d", 14273467308195657992975774342458504496649432985410431166185472 ], [ "00000000000007d33d78522fa95bdcd4a25072aeac844cbe9b6bc5d0cc885d0a", 14930233597189143322113827544414041000381079823613435714732032 ], [ "00000000000003dd57f5dd1228f68390b586700063225d26bac972bd120546d2", 15164766714763258952996988973449124317842091658872414191747072 ], [ "000000000000076bdeca878b47c392f51fbda543b1e69612cf7d305deb537604", 15357836632983707094928406965317628870031114888441593128288256 ], [ "00000000000008eb1bb7e18d9dfe62210d761cbf114d59ca08e4f638b8563e30", 15958672964717750944291813934170287689797412223641384931819520 ], [ "00000000000001b0d8d885e4d77d7c51e8f1fdaba68f229ac04d191915845f09", 18362361570655080300849714079315004638119732162003921272832000 ], [ "000000000000081baa3a716d5f9ab072c9fc3b798900234c9be23ab02a287c30", 22401652017447755518156310839596703571934659990690572544245760 ], [ "00000000000005b88d0224b9b0d4b65d3de9a61d93609bb91c9297440f1c4657", 22607619418140130980719672680045705126213018528712048676700160 ], [ "000000000000027d6a6870403fa43a650b7d9a6e61243f375a79ea935ad9ef1f", 24717289559589094364468373797949472355802981654048927838633984 ], [ "0000000000000810a3490b86e4f302f6557f9621c5c8620c2b09ec8f0cf72794", 23340814324747679919001773364939281849550099124416593832968192 ], [ "000000000000073833bca8d0ea909fde717e251576b7b3ccaaa58ad5d39eed60", 23242391331131109072962566885467580392541369223033474166816768 ], [ "000000000000031b7fd2ed1f28ff74e969aa891297706c38bd2e1d3bc48183c4", 21554562042243053719921017803645315870071034703425342074257408 ], [ "0000000000000b0738bcba382983811d40b531f2e68cd57126092755f1be4ba6", 20615546854515052444405957679617344022137222968655050411343872 ], [ "000000000000000664cbfd5e3fa497c07614c33a0934b83e01fbe980634a9aa4", 19540887421473929614259883543522244007742949396702043752628224 ], [ "000000000000021eb520df39289a70e40c59822a8c47924dc4940e7d0c1455c4", 19588382523276445241758125434587686389961661359576757951266816 ], [ "0000000000000275e0c41b11bc250fe887c5e60c8ebaaa449f5c28c67133d496", 18009299117968233362105684657812007807160912568078774269116416 ], [ "000000000000097fb0fdbeee0cee7e8f4e1a4ef8fad49f3d549624b0d47abed0", 17993483763986497389087426516491816616385967180337839494660096 ], [ "000000000000053f199ae19d34365277e534f978ea2f6c69cd4757a4fc099af5", 16574638092431222848464934504874974361824393751455373256032256 ], [ "0000000000000217b2e7b4f61682d24b9357d62ad29f27ed45ea2a32dc1f32f6", 17085559845791583266730740536950670241169412424878408752693248 ], [ "000000000000039c1d77acd4702393f48ca61983c64fc0209ade141c694b2359", 17870687961287995446644888885900316642120964851955511819501568 ], [ "0000000000000ae53f0c78330f6c2fbece2752909bc3742823e4fab29c5fd2b0", 15554707140145502641228553657813466188995512591033787398225920 ], [ "00000000000004b4d72b8631a85ec7d226dc696f1913ba1bf735b7c8dec207b8", 16944226977030767532657500340718760127019357828074148225613824 ], [ "00000000000006e06735bffb7d2f215dcadd8311fc33f4a46661fdca3dc0560e", 17028747171100603034973679895960153979114298528140818252824576 ], [ "000000000000055fc0110d4a38ffb338eabc30c8b0aef355d4643d21b5b6a860", 15614535766060906942258863525753414259523988166363835227176960 ], [ "000000000000081b69cb4de006c14084c4861f0e4a140c37200117a738733fe8", 15392654931672180089790308609774483894682932641297604569726976 ], [ "00000000000009920770f2d40b5b6a8aba33d969b855c91b0f56e3db9c27e41a", 14444739009842829731785903206212823051010663269705670545375232 ], [ "0000000000000791dd1cb7a684a54c72ccde51f459fff0fc3e6e051641b1e941", 13237058963854547748734324548161076199478283141947127217782784 ], [ "000000000000019da474a1a598b5cf28534b7fd9b214eed0f36c67c203a9b449", 12305424274651356593961118223415860240572779254789271782948864 ], [ "000000000000074333e888bac730f9772b65e4cc9d07edb122c6e3c6606bc8ab", 11046080738989403765716562970384822165842244193743674858799104 ], [ "000000000000067080669115c445f378f3dec19787558d0e03263b9dec5d7720", 10007073282210984973971337419529346944295676968729147521105920 ], [ "0000000000000304760bf583f4ac241c5ffe77312fa213634eba252c720530f1", 9412783771427520201810837309176674245361798887059324066070528 ], [ "000000000000041fb61665c8a31b8b5c3ae8fe81903ea81530c979d5094e6f9d", 8825801199382903987726989797449454220615414953524072026210304 ], [ "000000000000022fc7f2a5c87b2bab742d71c4eb662d572df33f18193d6abf0e", 8774971387283464186072960143252932765613148614319486309236736 ], [ "000000000000013c6d43ba38bc5f24e699515b9d78602694112fefdc64606640", 8158785580212107593904235970576336449063725988071903546310656 ], [ "00000000000001665176b9a810fddf27cca60dfcfd80bf113289fcc8ffed0284", 8002789794116287035234223109988652176644807295346590313611264 ], [ "00000000000002dc6ef80f56a00f1091471d942ce9bfb656ebdab4ea0b77eb0b", 7839560629067579481152758851432818444879208153964570478641152 ], [ "00000000000002a1fa5546ec48ca88b9e5710e2c6d895bb3675004fdacd6ab13", 7999430563890709006856701613305138698914315019190763857641472 ], [ "00000000000000f517517c11e649b98feca7da84ae44fb643de5a86798fe3c31", 9047927233058169382412882048952728634925849476849852060008448 ], [ "0000000000000299cab92a923348acf9251f656bcbacdb641fd0a66d895a6e8f", 8296391419817537486273948666838217011279219811331013552898048 ], [ "000000000000027508b977f72c3a0f06f1f36e311ad079536630661880934501", 9081029136740872581753422344739175313292014241889017867010048 ], [ "00000000000001925959229452cc6fbfef0104ebed7ccd6f584f2439c5dd1f1b", 8230751570811169734692743946971314968326461977249645504495616 ], [ "00000000000003b34ca89509da5f558af468c194afaa8d458bbeb07c50cc7c74", 7384127474250891166670391848516180960454656786677558849568768 ], [ "0000000000000076559e314ab0c86cc552e34fd79488415d3d17f6ea3c01adb3", 6172230000534146257480611019445716458048957888854766248787968 ], [ "000000000000003a58043252cdc30ed2f37fb17e6ef1658324b1478f16c1463b", 5561365017980676031428107027647386014985059524839404952616960 ], [ "000000000000011babf767e60240658195b693711c217d7da0d9215ccab45333", 4026319404534786334009451711043898716884778820756489262596096 ], [ "000000000000027579d28fb480ccad8e2516d1219d4c1919e3fd4fc0c882955d", 3513558656525386849113615662535622466519417660386833443323904 ], [ "0000000000000074546fe07f80ba15fc81897ec56a5535de727df9fda9dab500", 3004083578955603829930099910053556479043735076695139267117056 ], [ "00000000000000b6c55833b80c07894f4c4d3bb686e5ddbc1b1d162e22752ca3", 2675541054922611112919804040984964595022815308724929898217472 ], [ "00000000000001326f2f970753122e35bfdf3358d046ddf5ea22e57f5d82b00d", 2409843108029446766213067266805752590003732794677225687351296 ], [ "00000000000000641084745613912464ff73c974bafd0bf6dd306295f019d306", 2218268905456883731807407021635746739577921454491297946533888 ], [ "000000000000011ae105ddb1a5bbac6931a6578d95c201525f3a945276a64559", 1727551573307299192250197436766000536509732237655131060961280 ], [ "00000000000000d9b66fee19af89eaaf3f3933d1acd2617924c107f0abbe0a41", 1394031503757574068227953656553224448260418805016069352194048 ], [ "0000000000000011956d42670c2f75eeb344ac0657a806775998e2c58fa4b157", 1263610003247723462826224891154624535497729630761756072607744 ], [ "00000000000000959b1ea990368fd16d494e68ee13bd7245ddd9cdfba3330100", 1030450001678223668360152541055867895065240185756254103142400 ], [ "0000000000000091f86b1e423e24fe358c72db181cfcc2738c85f2f51871a960", 862513010327976103705811440432628413487564277790886242287616 ], [ "0000000000000055e146e473b49fe656a1f2f4b8c33e72b80acc18f84d9fcc26", 720982641204331278205950312227594303241470815982254303477760 ], [ "000000000000004f6a191a3261274735292bc30a1f79f23a143e4ee7dd2f64c1", 530591525189316709998942710962548491505413142398652303540224 ], [ "000000000000005327c8e714272803c60277333362e74ec88b9ffab5410c2358", 410030579894253754102159787320079652501746816512444002729984 ], [ "0000000000000002e2a62b8705564c38d6a746fc8e971a450a69989152b5ee97", 310118479516817784682897231521434079438159381558537557639168 ], [ "00000000000000202bf3ff30109538bfd9b5075c6438ab5ef64ebe2cf9b61404", 239366800071949252578530950352093786414793290792735831228416 ], [ "000000000000001c997105893f5991cb45765ff856b6e503f8466cb22cdd330a", 181156297885756721946540202079438048595571151633323613224960 ], [ "0000000000000010c13ce182a3d8fc6748b75640447eb360d7739a5fe984ffc1", 142431093377788751676361246670241704468765375727695350988800 ], [ "000000000000000bbb49db68b79ecc8393376d78272d237bb612288af64c1de8", 100696259189502783924473792493100546893980348528488767029248 ], [ "0000000000000001bbfd0973c367d30eef2416d9e94bdddea53bccf541a4858f", 68962778243821519216393853205209897734463141354237780295680 ], [ "0000000000000004ee5b6ace996ab746f1e6dd952cdbc74c0b4f8b9ac51c7335", 52765641310467331636297188681879886184148735229489015947264 ], [ "0000000000000002f2f23b515085d0c9f37a2824304ccb7ca1546a48548d0dac", 44233472386696495417387091608220539804351405166731810832384 ], [ "00000000000000045590c3fdeca1753d148a87614a70fa0897a17f90bb321654", 38110290672195532365762668664552282566878756832852091863040 ], [ "0000000000000002b704edc0bf1435fe2116040b547adb1bc2d196eb81779834", 29679649578007061283718812081441644170496168236939550392320 ], [ "00000000000000038cc59dc6dd68ae0fbe2ded8a3de65dbd9a2f9a36d26772df", 22829202948393929850749706076701368331072452018388575715328 ], [ "0000000000000000a979bc50075e7cdf0da5274f7314910b2d798b1aeaf6543f", 19005913916847449503306572434028937600915626422125897711616 ], [ "0000000000000001dd8e548c8cf5b77cde6e5631cd542e39f42c41952e5e7085", 15065005852539512185984435657022720640916062598235628240896 ], [ "0000000000000002513542a461de351a5a94f96b4bcd3e324a48d2d71b403fe0", 12288698618318346282960995223961541766142764336009759948800 ], [ "000000000000000150cc07163e78d599a7e56c0d1040641bffb382705ac17df0", 10284386012808371892335572105827331142617405906583881252864 ], [ "00000000000000009051d83d276dad5c547612f67c2907acf6a143039bddb1bb", 8614444778121073626993210829679478604092861119379437256704 ], [ "00000000000000000b83d3947d2790ab0bcbbb61eba1eb8d8f0f0eb3e9d461e0", 7065379129219572345353864175298106702426244380437224882176 ], [ "00000000000000005a4fbbaeffee6d52fa329dd8c559f90c9b30264c46ad33fd", 6343094824615218102798845742064326605321937397913065881600 ], [ "00000000000000006b6834bae83e895a78c5026a8c8141388040d90506cf3148", 5384518863803604621895699676581808210968416076987222720512 ], [ "0000000000000000bf3c066c9acdb008e7fff3672f1391b35c8877b76b9e295e", 4405349994161605759458363322921957536960017949107037405184 ], [ "00000000000000006bcf448b771c8f4db4e2ca653474e3b29504ec08422b3fba", 3863038134637689339706803268689141874606936642244315185152 ], [ "000000000000000098686ab04cc22fec77e4fa2d76d5a3cc0eb8cbf4ed800cdc", 3369574570478873127315415525946742317481702644901195284480 ], [ "000000000000000036cc637d80982595b1fa30f877efe8904965e6fd70aeae1a", 3045099693687311168583241534842989903432036285033490677760 ], [ "00000000000000000ee9b585e0a707347d7c80f3a905f48fa32d448917335366", 2578448441038522347123624842639328775756428679710156783616 ], [ "00000000000000000401800189014bad6a3ca1af029e19b362d6ef3c5425a8dc", 2293149852232440455888971398133692017055281498246925516800 ], [ "00000000000000001b44d4645ac00773be676f3de8a8bff1a5fdd1fb04d2b3b2", 2002553378451099534811946324256852041059202347552707969024 ], [ "00000000000000003ff2a53152ee98910d7383c0177459ad258c4b2d2c4d4610", 1602972750958019380418919163663316163747908621623690788864 ], [ "00000000000000001bb242c9463b511b9e6a99a6d48bd783acb070ca27861c2b", 1555090122338762644529309082074529684497336694348804259840 ], [ "000000000000000019d43247356b848a7ef8b1c786d8c833b76e382608cb59e9", 1438882362326364789097016808333128944459434864174551793664 ], [ "00000000000000003711b624fbde8c77d4c7e25334cfa8bc176b7248ca67b24b", 1366448002777625511026173062127977611952455397852592472064 ], [ "0000000000000000092c1f996e0b6d07fd0e73dfe6409a5c2adc1206e997c3a2", 1130631509982695295834811811892052032638591596239280668672 ], [ "000000000000000020ce180d66df9d3c28aee9fcec7896071ec67091a9753283", 982897592923314645728937741958820396011314229953349812224 ], [ "000000000000000018d37d53ae02e13634eefb8d9246253e99c1bdf65ac293ea", 903780639904017349860452775965599807564731663176966340608 ], [ "00000000000000001607d1a21507dea1c0e5f398daf94d35fb7e0a3238f96a0f", 777796486219054632155478957346406689849105796561635377152 ], [ "00000000000000001acae244523061f650ddab9c3271d13c0cd86071ae6e8a5f", 770217816864616291160628694313702426464491250746461782016 ], [ "0000000000000000104430189dba1219b0e3dd90824e8c2271609aca5b71250f", 749174812297985386116525053725808178560617045558724395008 ], [ "00000000000000001aa260733b6d8f8faa2092af35e55973278bb17f8eaeca6b", 680733321990486529407107157001552378184394215934016880640 ], [ "000000000000000009925ad5866a9cb3a1d83d9399137bccc7b5470b38b1db2b", 668970595596618687654683311252875969389523722950049529856 ], [ "00000000000000001133acacb92e43e24af63a487923361a4a98c87a5550dffe", 673862533877092685902494685124943911912916060357898797056 ], [ "000000000000000018c66b4a76ca69204e24ee069da9368c7a9883adb36c24af", 683252062220249508849116041812776958610205092831121375232 ], [ "000000000000000010b13aed220b96c35ccd5f07125b51308db976eefcd718f9", 663358803453687177159928221638562617962497973903752691712 ], [ "0000000000000000031b14ece1cfda0e23774e473cd2676834f73155e4f46a2b", 613111582105360026820898034285227810088764320248934432768 ], [ "000000000000000010bfa427c8d305d861ab5ee4776d87d6d911f5fb3045c754", 653202279051259096361833571150520065936493508031976308736 ], [ "000000000000000005d1e9e192a43a19e2fbd933ffb27df2623187ad5ce10adc", 606439838822957553646521558653356639834299145437709336576 ], [ "00000000000000000f9e30784bd647e91f6923263a674c9c5c18084fe79a41f8", 577485176368838834686684127480472050622611986764206702592 ], [ "00000000000000000036d3e1c36e4b959a3e4ad6376ce9ae65961e60350c86e8", 568436119447114618883887501211268589217582000336195813376 ], [ "00000000000000000b3ec9df7aebc319bb12491ba651337f9b3541e78446eca8", 577075114085443079269506210404847846798089003835028668416 ], [ "000000000000000012d24ce222e3c81d4c148f2bce88f752c0dba184c3bc6844", 545227566982404669720599751103563308707559049533419683840 ], [ "000000000000000000c4ccbdd98c267bd16bda12b63b648c47af3ac51c1cc574", 566251116039239425785056264238964437451875594947144974336 ], [ "00000000000000000056bfec1dca8e82710f411af64b1d3b04a2d2364a81993f", 565860883410058976058672534759150528155363303710710038528 ], [ "00000000000000001275d1cadce690546f74f77f6d4a6190e2137a8a819946f6", 552364745922238091561919045022000637317595931246011088896 ], [ "000000000000000003816ae80c6413b84cbee2f639ba497ab5872ec9711eb256", 566500670366816952120145379831520408210047884740723212288 ], [ "00000000000000000d92953224570f521b09553194da1ca3c4b31a09a238f4f6", 542528489142608155505707877213460200687386787807972294656 ], [ "000000000000000006721943f23cfacf20c17c2ad6ea4e902af36b01f92e3c06", 545717322027080804612101478705745866012577831152301113344 ], [ "0000000000000000031d9af2fe38cc02410361fb213181fdb667c74e210d54c4", 527827980769521817826567786138322798799309668948178370560 ], [ "0000000000000000142e8a13ef6994961655c8e86aece3f0abebd2ee05473e75", 515692606534173891771672037645739723025219384908133171200 ], [ "00000000000000000c7a8db37a746d6637ef6a6eab28735608fd715ee2f394e7", 511567664312971151375333957573881285830542480898837708800 ], [ "000000000000000007854877c66c71a49af40d20f2d6f817becfe4d66d5e5a81", 496889230460615059653870414954457230681194245244172894208 ], [ "000000000000000005ce1d2d10aeb9def4d38233e859d98a4a168ea3fa36687a", 473325989086544548323169648982069700877697035484407005184 ], [ "000000000000000007c71decfe74855ad99dc2aa4a2e713165db5a8d6da5f32a", 454358737757395076722955683517864397151243915416267915264 ], [ "000000000000000008ce4f34161be6760569877c685e37ebebce3546ea42a767", 443316987659242217350916733941384923365365929826941140992 ], [ "0000000000000000086233f4843682eb47bacb58930a5577fbfd5c9ebd57ddf9", 442802913227320896234856097023585967110900073490544590848 ], [ "000000000000000010a904eee4fc763c6b88d378884f368fd652f63c1af71580", 433057199397126884276233483897801969646324654385408245760 ], [ "00000000000000000c114754749d622d4fa2f78c84d7147c345b2b99a8e83d2e", 409419129139225030716120689261979366152221060879441985536 ], [ "000000000000000000a5039e32cc9a89aeffbde1391e8bc9ae9724127904f01d", 370716507988397359530778284103407727265240291588416995328 ], [ "000000000000000003b0b73d9b3259c318cca48a6335b5d64545583f7f3773fa", 340818253309165415058055171484606858815006633875327680512 ], [ "00000000000000000198bcc5bd65fd0ccd1c7e3b49e0170ea80296cbfee05042", 288495652867775987986282369150900282132304927019642126336 ], [ "00000000000000000a60f379d3dc1413491f360809a97cbb02c81442c613dce7", 259524902203633530447121351815377152077137395840706412544 ], [ "0000000000000000038973a5f8ba8cdc7e371dcc8f4b24337ef695f24b962907", 237834253647442358407456603145452341381064939329604812800 ], [ "000000000000000004b8ec471974913d052a3af7dc2a8c6f01c2ac2f3d1f7b19", 224600391397450328424792273873642383828872941895338164224 ], [ "0000000000000000075d572eef1c4210adc7abf4e40986d7f0a80003853bfec4", 187067719845325692996306936867878122094522982476155977728 ], [ "0000000000000000074f9edbfc07648dc74392ba8248f0983ffea63431b3bc20", 164898540577033087399552264895286015147022701908103004160 ], [ "000000000000000003c4a4d9c62b3a7f4893afe14eef8a6a377229d23ad4b1ea", 170169861298531990750482624090969781281789404909188153344 ], [ "00000000000000000404b6939e6c35a5448386e5d58f318c82ce2fefb7d73e47", 162900609378736249874251099581569547607832255884553093120 ], [ "0000000000000000034656c96781091b5fbc799c881ea85b41cba0b88128eff7", 161578008857017275969393492955354620126364423170461532160 ], [ "0000000000000000045645e2acd740a88d2b3a09369e9f0f80d5376e4b6c5189", 150883090635422687830679296233896712896447026244773478400 ], [ "00000000000000000381e6a138308c6547d6fe3eb3437250ffefdebbf71eefd1", 150899178845446426410002882396535253739927398750206558208 ], [ "0000000000000000012100ddbb2102e65fb1ebbf104ead754a4110abffc4b8bc", 138784382553152119468195441786396823230753870240366460928 ], [ "0000000000000000046f56e59b9b1293b5e7c1587aa6d29c4f3f79b98cf22ee6", 135262935280049154152065372885142255350817451144176992256 ], [ "000000000000000001bd1c291e91f4476f93454d4542d2ed7e44fc86902c93bb", 137505556928474480767543871928291413858290772017802117120 ], [ "000000000000000001c37a483375ff6fd6ed7c5b79d80167b027a8fdb0721dcd", 128713911367130082233924624261304605948946745676720504832 ], [ "0000000000000000051804b4c2da5298c4573386bf1d4242bf0e26a49ec32e42", 126333978716874242627475052620752087219210710628817698816 ], [ "0000000000000000034bff7888f1f7294311f0199322f77c1457018c875bd9e1", 126278605342839049377710151409810132688161986656629424128 ], [ "00000000000000000506b43c9283ccbc40f583e0c734e4a8af2ce6a4262c6221", 133533639774706835230353390473157702360903922769486413824 ], [ "000000000000000003937068e19a0750a33978050f019d2b60f430e3da707db9", 124022888639743237872084547350559836284832548627419234304 ], [ "000000000000000002e2f6ec3c9eb965aa706c788da7dede201b6b4b8fae3971", 122123731568103772089607259872577666017242529148853813248 ], [ "000000000000000000b3076636b13562bb4315f895bcb324e0c962763c2196b1", 119378259820331825692479928211144812308894309500762193920 ], [ "00000000000000000025b8961d1d0cfba33b0205ec10b3ce541618e352b0bbd5", 111759931157462873316041289986819959868258380300102402048 ], [ "00000000000000000421d58b78b9f063a4b20e181d55c9c79082f9e4b8b30925", 104283029085035157753191385936387396702868516379761311744 ], [ "0000000000000000027fd968d41741f31c73c4a3b304472da0165245278e2ea3", 106299667504289830835845558415962632664710558339861315584 ], [ "00000000000000000364a23184b8a2c009d13172094421c22e4d9bc85dcf90a5", 105881374043672627773432318187360570734220873198601240576 ], [ "0000000000000000042a2ed4a504424060407825d774a54f2e148fa769ee72ff", 95668727978371040303278646201741713440261619517174579200 ], [ "0000000000000000025f769f13f2806fed19d9948b1a7ef19048177789afc5d3", 94012390634764280055243391736606357298689315295029362688 ], [ "000000000000000000b3ff31d54e9e83515ee18360c7dc59e30697d083c745ff", 86923102180582917240747796162767475850640519180006195200 ], [ "0000000000000000021ecdcb2368ce66c23efd8bd8ab6a88a8bb70571c6e67f0", 84861566431029438820446406485131195674434646972185968640 ], [ "000000000000000001972cb33b862b27c1dc3f3a723f7d1cfd69aebe0409126c", 80022382513656536844370512820784980102919810105407963136 ], [ "000000000000000000cb26d2b1018d80670ccc41d89c7da92175bd6b00f27a3e", 68605739707508652902977299640495787127103841947617329152 ], [ "00000000000000000276deb4022f66cacd929c690cd6b4f7e740836b614b21f4", 63859343606086615291372321518809062931940920926127783936 ], [ "000000000000000000587912ced677698c86eec8b1d70144dccb1c6b0bad0f17", 61163258921643354765656928775243357859392914550528409600 ], [ "0000000000000000009f989a246ac4221ebdced8ccebae9b8d5c83b69bb5e7c8", 58509826700983959310706392369835644790490546910263246848 ], [ "000000000000000000038bed8b89c4e82c13076dd64dc5f7a349c39d3921d607", 56672777602924507578641088682504585686103825941044133888 ], [ "00000000000000000122f47d580700a3a5b4b6cb46669a36e4fa974c720ab6cd", 53958359841942568206719748916397287559357255547625668608 ], [ "00000000000000000172ad9ea56a90bdfed0f364a902500e9ff4d74f000ced99", 51764751112426770751506128647798102319231116027761786880 ], [ "00000000000000000201d7429db233c7055e9699c5bfb57b167ca8d0c710dc71", 51649140486907347007064544362790913467244253139882213376 ], [ "000000000000000000c0549b2a8adbefbf6c909f61fdc4d6087c44a549cf8201", 48144529712666433692552181910809237167694270386587828224 ], [ "0000000000000000015b6789cdc5dc13766f58b38f16d5b35bf79ce4b040f7fd", 45240046586752885057924289339576851866807485277820420096 ], [ "0000000000000000013a31b29f845d97465bff53f901027f8ab4b1a2f59118a8", 39718797393257298660757754408019939605415460564426031104 ], [ "00000000000000000088cdeaa7389a7de9f09e3a28b3647630fea3bd1b107134", 37880625861940376795251270290737354395669643839013912576 ], [ "000000000000000001389446206ebcd378c32cd00b4920a8a1ba7b540ca7d699", 38043004539854389433075372490391464304285496568268718080 ], [ "000000000000000000f41e2b7f056b6edef47477d0d0f5833d5d4a047151f2dc", 33509870757351677175294676059494700127350769223450230784 ], [ "0000000000000000010e0373719b7538e713e47d8d7189826dce4264d85a79b8", 31340207270661909233492904963194738468218672502370467840 ], [ "00000000000000000053e2d10bd703ad5b7787614965711d6170b69b133aa366", 29201223626342991605750065618903157022235193117232857088 ], [ "000000000000000000cbeff0b533f8e1189cf09dfbebf57a8ebe349362811b80", 30353962581764818649842367179120467226026534727449575424 ], [ "000000000000000000d0ad638ad61e7c4c3113618b8b26b2044347c00c042278", 29217311836366730185073651781541697865715565622665936896 ], [ "000000000000000000a7bda943639876a2d7a8caf4cac45678fb237d59c28ba1", 24433127148609864747615599184820261456796420809345204224 ], [ "000000000000000000fb6c6a307c8363e923873499ba6299597769c10a438e61", 23988269434232535193761088780698748366141469438183997440 ], [ "0000000000000000006f408147ffbcaa0fb1dcf1f199c527ffdaf159d86e5cd9", 22526487188587264742197108840494583820145762956159746048 ], [ "000000000000000000e3be3cf7343d7792c0d47d3c39ddb9ceaf19961e9eeab4", 18556440756915402760741928101946749165024073301499052032 ], [ "000000000000000000b3fb09d6def197657e20f9c1d5e9680cfcac1e1f9aa269", 19758940920085072387393228723348383373068660102939017216 ], [ "000000000000000000bfe71f044145e1b42fdfb3a523ee2a215e80fa6afc2a98", 20014481558369106100835306608979160026489460596213284864 ], [ "000000000000000000cee3bff56ee49c0f96d1cbd17fa17dc6f84b3f48aed765", 16946123176864917983795071264823963343174695083267063808 ], [ "00000000000000000089ef13654974b8896b0b0909dd9ae8e350b8a8a7807ce3", 14392961660539521116256653268419249019684881662910398464 ], [ "0000000000000000003105a067417c318dab31e25ae1583fa2b27be226945fdd", 13960450711994363030255127593764523087979983609872252928 ], [ "000000000000000000720da39f66f29337b9a29223e1ce05fd5ee57bb72a9223", 12101157559014734955774763823279522156034099347349045248 ], [ "0000000000000000006a8957cbd52c2038861514f106f7f9f76392d5cb83fd4c", 10356793971791534424976101420669664288187918308140384256 ], [ "0000000000000000006b68e55432541794388c94fe9e805652038e7b3cac0681", 9378292318569022964986206758839123913433917663832178688 ], [ "00000000000000000001c9deea9f0302eadb1250df1ad53da802dfb40d47face", 8964447668935855171055978546867850348456065181232922624 ], [ "00000000000000000013aaa8778111530a626a3fe57e4e6f4a878c92669b04d1", 8192878571041388924351625416816775770172128369752145920 ], [ "0000000000000000002f67aa98789b98304a32e54bffbb34c8693eb0acac4c30", 7786052052270684126234611299412205796254663675224260608 ], [ "0000000000000000002e5f072398ee27b25b6cdcf69051bcdbbece417093c979", 7678459224733657715202292429397298472913633233275453440 ], [ "00000000000000000028d7447c20ade2053bbaf49e8a16eb5fb1bc74335d0d18", 7021961458254440109762706424650140438182306270565892096 ], [ "00000000000000000042d89446b9043387be2d4c09aa9e9524176c5754616510", 6702918573828378664524678433037841287557455508299317248 ], [ "00000000000000000018ec4d369bab2c13174834a02138decea7c85685d46bd6", 6505870154073602347674948421782035713149324747260035072 ], [ "0000000000000000000d4a6c2237c6c46b963b17f60d9c850c4915518deb6678", 6259542822111302646229226565336702507884435252736688128 ], [ "00000000000000000031adb986da21237ce06b57ae5390b7f0f890ab8e21b66a", 5456617206587901877414813377199700077413780408546361344 ], [ "000000000000000000031df41201cd3789559333cd9529f99834a805014c9b13", 5309609141393698345581459330931267317315649121846034432 ], [ "00000000000000000020c68bfc8de14bc9dd2d6cf45161a67e0c6455cf28cfd8", 5026314587016750785722693470327208449351582469580652544 ], [ "00000000000000000009dce52e227d46a6bdf38a8c1f2e88c6044893289c2bf0", 5205879062684137510961952799929229129995569309608312832 ], [ "0000000000000000002eca92f4e44dcf144115851689ace0ff4ce271792f16fe", 4531442825108320403104334767545311437480985430866264064 ], [ "00000000000000000000943de85f4495f053ff55f27d135edc61c27990c2eec5", 4219470685603665866184576203153693664105230070242607104 ], [ "0000000000000000001d9d48d93793aaa85b5f6d17c176d4ef905c7e7112b1cf", 4007526641161212986792514236082843733160766044725313536 ], [ "0000000000000000001877e616b546d1ba5cf9e8b8edd9eba480a4fbb9f02bce", 3840827764407250199942201944063224491938810378873470976 ], [ "00000000000000000025eb2c783f2f29d68ab4260f4b0248450c0038debc7ba4", 3769176185135465353474348091454476000617158630021529600 ], [ "0000000000000000000c61b8a7779dcc46e88ca343b9a3fcc6763917fe3b87e2", 3616317728887026217259424694800679959591344645351669760 ], [ "00000000000000000003dba9fedba6a0b92b640167eeda0d41485a3c85ac4ac6", 3753318892370425056811838111019504329853891761930240000 ], [ "0000000000000000001ac75bed7eb6169255893f99de28f24e3e0e57b6f7db7b", 3752507758961706405692235065937346792777982719368888320 ], [ "0000000000000000000e5796e9c5cdc8a8a2de84fd17287d7dfe89074de31766", 4052052750044136275098507698196378011637603685579620352 ], [ "00000000000000000015fe695e8d2e5ed3a7de81d3818ef43a444e1ee7b3ace2", 4774638159061819979596346127394133648234752261950013440 ], [ "00000000000000000015a08d0a60237487070fe0d956d5fb5fd9d21ad6d7b2d3", 5279534360700703025330663904443631645337169341976674304 ], [ "00000000000000000008f4f64baaa9b28d4476f2a000c459df492d5664320b12", 4798269179035823348880781507454323228379569035237392384 ], [ "00000000000000000028a69d9498c46b2b073752133e3e9e585965e7dab55065", 4581847093576588582947343450056030606262879232408420352 ], [ "00000000000000000014dbca1d9ea7256a3993253c033a50d8b3064a2cbd056b", 4636475101776743072223960781733299832971578678999777280 ], [ "00000000000000000019046cf62aa17f6e526636c71c09161c8e730b64d755ae", 4447653474738502407900799312400854215681091162244907008 ], [ "00000000000000000017e5c36734296b27065045f181e028c0d91cebb336d50c", 4440088742263677654396177039706714734771352055402463232 ], [ "0000000000000000002296c06935b34f3ed946d98781ff471a99101796e8611b", 4442250303185290059812200289574302117357423179633524736 ], [ "0000000000000000001ccf7aa37a7f07e4d709eef9c6c4abd0b808686b14c314", 4226119056551884143559484765457720035561644907380604928 ], [ "0000000000000000000de3e7a7711130dbac9fb0a14e5ad6ab72d080182f3321", 4217024131862773934699503234743726606330326039165665280 ], [ "0000000000000000000e6829c1245de98ce5a35c177a75f67e9c1678cb6e24aa", 4243570847603252455305754966045185171099356397876281344 ], [ "00000000000000000001b2505c11119fcf29be733ec379f686518bf1090a522a", 4022508494445492072607020209303018350395259009223360512 ], [ "0000000000000000000a4adf6c5192128535d4dcb56cfb5753755f8d392b26bf", 4021030916290150529756716283937142188262386861422411776 ], [ "0000000000000000000485ab94f5ea60203aacfc9740b3e42700d7e7012f76d7", 3614033401827878015998272335407144409231622422786998272 ], [ "0000000000000000000cbc6dfb3f2afbd6ed1427e30ed1f3167898ac4aa4c673", 3638558860803927897868648370584956354584468626790678528 ], [ "0000000000000000001d9865df58f5f300552699fefc09aa840ba25ac044a534", 3397669776434136486181562425402160438435718857259745280 ], [ "000000000000000000115eb6c10b7a98bf23a46002baec8fbbbb2cf0583439a6", 2974300520630483197933400799376074857018768662277914624 ], [ "000000000000000000113978c5b95531173923ba81ed4d1df3b09db37ae0f0cf", 2990922178751847556822131306978557143801315583089180672 ], [ "000000000000000000096b8d24db6471fb5871e9ae8bd1d7384fbee9c80a6052", 2699909434228155498652331786772923585210445951064342528 ], [ "00000000000000000016e0dd8fe86bf34feaa611b4c52180b6822b5ad31b68ff", 2647377219375933524160418539145769508351933111739613184 ], [ "00000000000000000011e20e47a868d12a2bf3de814ebd067e83514aa2725745", 2502742632840755378666227277045667991877723059489079296 ], [ "0000000000000000000c48f6bed594da7bb5e75731b4e78501670e834d426e87", 2267299103571658911252368261549572946260211294613274624 ], [ "0000000000000000000f7871dc40f51b1ecd6343a6d9fd614d0e2235a7d9e3fd", 2112846149036891759953684644743283440459952687539027968 ], [ "0000000000000000001558c0f33a360d105b52a749103eb2abd4a66a68d52664", 2072520395859657486634608572838975759381606196813234176 ], [ "0000000000000000000676463abf3771ea01e0f8c948d1c93658a1d82d95df5a", 1969073848467738847181233556694484530967339635488849920 ], [ "0000000000000000000e24396612da4ec125ee6c0b4507e854c5cfed1884cd30", 2119459443945814095658556318611324621123895782295994368 ], [ "00000000000000000002fb021eeb13e47021920faf6e5daa3c40bc552c4d248e", 2078088717097888226752964612051624797686495299801972736 ], [ "000000000000000000067b904af747b653ba448a79779f7846bf1ea5537b8a4d", 2093644940525638357414324633411056914147713045789409280 ], [ "000000000000000000080ae07ccf2f1b6d1d089f5dcbc1fac50a6b93d005f1e0", 2082043540528505650049623783208955059537684253263265792 ], [ "00000000000000000008f9ddf24dbec1459689fc399329e9738b2795860e4361", 1953761695813422977307213550702116033770404430236090368 ], [ "0000000000000000000aacba541ebb7b56b0831e4ae33faf20ff1e528bb9a657", 1824503568004603261415443256727022530945994444270206976 ], [ "00000000000000000010fe23dd08a4b6465c4850984bb538e9dfcb93995a23cc", 1743137387349479903250289511035208906392689711805104128 ], [ "0000000000000000001166c174a9d34b0743953e724162fe44388e38d078204c", 1734095076719313606895363312975193263350078457161711616 ], [ "00000000000000000006da92c61b6b63ea910be27cab5fd951137105314f2969", 1740794600224838465872409004248364704712181251938713600 ], [ "000000000000000000043f26353c41c2343a277ad72f115171fb49d3be52dbbc", 1628687194130096895725758951785196783123433634364653568 ], [ "0000000000000000000bc6800858a1b3be08fb26b55d4b989c95e06ad50a350c", 1937788944419033539314165479165359776648584743473905664 ], [ "0000000000000000000c799dc0e36302db7fbb471711f140dc308508ef19e343", 1832085838499075985755083973639154607251969422303166464 ], [ "0000000000000000000de98650125747f239134cf7e2b7362033e325a8003a14", 1689336589076054705025375464973257095873115523033071616 ], [ "0000000000000000001138f586983520b0de3645c0873164f4b214b90cf3aedc", 1674005436900453533413418811078063286996924790657253376 ], [ "0000000000000000000e87ecbff47d9ab75e78d92328d5951351f9702597dace", 1780912820169571750977100152906426673601736600243404800 ], [ "00000000000000000007c4dac98234149700771e9d1756956660b63cca88c36b", 1963213226902041926479236780515292236058519345991516160 ], [ "00000000000000000003030a3de58b57be352e2ca79016cefe19777e02ba0520", 1707948812427463753688699391317898960128433823967870976 ], [ "0000000000000000000cfd1300625612513c6cd1413245fcdaf1eeb766e33a93", 1708005810991319658902509335026374895166200405337047040 ], [ "0000000000000000000830b0a5ac4b78b5eb99209ebb4790be1fae1428c7f77c", 1554226608711362053849117616927967595838003183165112320 ], [ "0000000000000000000ed5cf2e86791b44abce69e178e58613e64ed47e1c02a3", 1600203988720154928752887338080389143353359165034594304 ], [ "0000000000000000000aac5c93f7945c60d82828990448cde97d3d7128830a6d", 1590739304116800001454600275103718494518067345886281728 ], [ "000000000000000000049a66ca322371799e1cb51d85c8937764ba6a2abb8ed9", 1535456543183121267670627692621392373016562041515671552 ], [ "0000000000000000000657c7aa925caa49d18e0c02cab9992be315012d8fab06", 1554222224206450061140363005873469446988944215367483392 ], [ "000000000000000000061250f1186194229157967d10a01a2b36ab19d4304da5", 1395807138732878832030429199485686097922398375169228800 ], [ "0000000000000000000d2e17e6d3179b4182518bd678f20bbda8b29e5e494d54", 1397005570075490172423356221048513449998516239854469120 ], [ "00000000000000000005e2dea23567cb4fe092a354e7d1b50b59571715de22f6", 1348156339349342073285316259199804406349536350538039296 ], [ "00000000000000000005e17383e25f65b531d50060b99ed66f673ea251949e4b", 1605902383604108119230963505243149930846997646019657728 ], [ "000000000000000000090386439b3e1c7dc56d2e450694e910b366895f05b9ef", 1532070243889425565609149754863988745260019245813596160 ], [ "000000000000000000046f183ba323cfceb2d11660376c59fb55e8521c4d32a5", 1407282849589201081744164532792174352192736757496676352 ], [ "00000000000000000006d248288fe5c88d55836f0ffcd9acae8333c824106a54", 1443989924712404039437768281050676516514415159246061568 ], [ "0000000000000000000a047b7c5b3b06db1bd4b858e757c7214d192cc491e2e9", 1449469094350757594478113895488529861555105250349678592 ], [ "0000000000000000000ea5abc8d23ce15f85afbdf574da6c82c67bef5df0d752", 1308244191135472445492091830103155433365752488721907712 ], [ "00000000000000000008ad1e1340f31a30d1fee7d0e3e56a0cf7dd571dc653aa", 1294666840924668357381979598007221164113148875397660672 ], [ "00000000000000000004f29390852281bae27d3662f648020bb47cced0d883b8", 1257769770588612382309009370720465882998915202417688576 ], [ "00000000000000000008aa78bb2eb233395c99d1276ab90a7fe728882b5c2907", 1240994654795328278613867476210548386499304408689410048 ], [ "0000000000000000000b842cbf7dfe7345b48deb00b76298f58ae0f58ce821ae", 1256955714176619069383569918268642913356966847991250944 ], [ "000000000000000000065952ab35814a9a021f9e5138623779c93c6c56ad6cf4", 1232968087803106959787092839109270560155354027163385856 ], [ "00000000000000000000b136fc67072ab98643c2346ff07c4076d94c35d9481c", 1165190949371886336955396954992052935118810155482873856 ], [ "00000000000000000005a2db60197fa9012b70f75e4745b362afc16a052d6ee6", 1143226041264440196997713775641159917616401145294487552 ], [ "000000000000000000035d5c18a83cb7e0ad0880a3b7936d277becaad9d5a00f", 1308153578033957929511163201643527023818533820904243200 ], [ "0000000000000000000d882df633c1ec8ef9ded1dbf09f02114cf9c999d1dde1", 1076379879376199359324913638762382564863378102643851264 ], [ "00000000000000000006248c28751a176336f5c070f901dc86df190c391d761d", 1280876111474813957445809627925710317539675495922139136 ], [ "00000000000000000001464428893b618817bff3128a6e17a2c043de53ca4673", 1352521844740049480301990665795127943729248621043908608 ], [ "00000000000000000009cbc816ab1d430e7a9cc24ffdb6702870112c84a9657c", 1877009475827353279654828838027187714710153476809162752 ], [ "0000000000000000000b964e653343c6ae97d73db7f526cbc3187ff7829b0c42", 1971793703014811657512010614168169533666919325951328256 ], [ "00000000000000000003c1a7a8f2e45be0b6ba0d2fac227ff2a43cf8b7eaec29", 1859734526474102007161661283304481249417820354151186432 ], [ "00000000000000000000019999808926fea81b376f1060e7411bc3a5d2853594", 1733053026051896673114684085689466553557063777258569728 ], [ "000000000000000000098d881262beb65d58d46176ae565dee9bb050f7b3d516", 1530484514612921535942898756820491578183692559004467200 ], [ "0000000000000000000019cb70a38e25e348efd12435797631ec48088e4abe63", 1463986190114365453164631096931900700789347628299059200 ], [ "0000000000000000000515117969caee77eadd03b30d8d50d044269172d844d8", 1419099090327021431837841324664685500406654972106637312 ], [ "0000000000000000000e1c751629fb7e8112c37eda91f9f6d8e223cd2ee50c05", 1355224161267474319797749279050820351032592440315871232 ], [ "000000000000000000029da63650d127e160033c93393da77302320bd8ee4958", 1342441867947378242875139851503883739742681654295003136 ], [ "00000000000000000006c0cd9b33f4d579e31ba1e6bbf4c5297324fa78fbf151", 1244706868954148772026104835685647745369230477348569088 ], [ "000000000000000000013712fc242ee6dd28476d0e9c931c75f83e6974c6bccc", 1188998811044006745492934980917001185509005296607952896 ], [ "00000000000000000001871f43a65da0527d3455aea40b0af27e0ef31b3d6b98", 1207017664730659447571468211219560238858343288930304000 ], [ "0000000000000000000a8c54b4dfa4ba0152a0c4d5bcbf999025cbcbf0efc039", 1114247386799443053935571112778061457902663314832359424 ], [ "00000000000000000004680d21d3c5e94dc03d197a3bb5653b1f9d7d5015a2a9", 1110710552837102268873518195482888052995095958078357504 ], [ "00000000000000000000135a8473d7d3a3b091c928246c65ce2a396dd2a5ca9a", 1106174051754827146215413957762136710502083943464960000 ], [ "00000000000000000002d5d3caf5cd22bd5cc211e4fb7e283c1713dabaa96df6", 1011873581609325297224157601300783981224824207994519552 ], [ "000000000000000000001fc83bc1767e7aa5c31bf9dc4aeb505247f6fca1d96d", 1010078857598682948440603476326208385676686722831745024 ], [ "00000000000000000004ddb634472f403757e89fefe4590b70a3841e7b307bf6", 963971403944167623177113627223675088972581362965938176 ], [ "000000000000000000041e39e07eefe69a84140b1020fe46b329a20ce5b74a77", 978555728783092703397868198169350877225727913812295680 ], [ "00000000000000000005f63eed68a9a3979f0db78aec853df62aae1234f43306", 982035564181577583246111171756048347095528689197121536 ], [ "000000000000000000076c23a2f567ea68fa523055dac2a0d94579ad277459d8", 943064623022149056932209915691668660376403247938666496 ], [ "00000000000000000001a5cc47cfd6abaf57dd33f891342f35bd8b8176f8867d", 955133703543227653230735945040239725552721938878562304 ], [ "00000000000000000002c1452eafc55a3a7b2e23b87dc94c53257be3b9fd2d92", 904852201212495269232856372055468724544479235670016000 ], [ "00000000000000000005c7686bd1dd938bf1e1849ae17d71efcd868e897a2ae2", 862674725460762741916416231468109512880228678412271616 ], [ "00000000000000000008b5ffa0ae1b604dd27bf4af84602ea53f7920320a3c96", 901734818220068453308327912307284892863553131555848192 ], [ "000000000000000000050c04aa3e3ca62420b6366e20ebea29ed3042320d2e4b", 890244492347372894565410542152469475763018189902970880 ], [ "0000000000000000000882e7306788b58cb5805c04f432203863a8e8c8290902", 911713951399763858433822672345071673321763838959288320 ], [ "000000000000000000018b957c6b644c23f532ab7d8fbbeca6fa77ad078a6b29", 924766622522766152396299781586060796970310972500606976 ], [ "0000000000000000000528876999d7bdfc25ad64e66ffe7f27a1e371bd9835c5", 973529624652311728262165726029639579921131161797001216 ], [ "00000000000000000001095f6deb27964f80c74f38217a32044c20265e0f40e3", 956871428990014096800480126306339386063092842672160768 ], [ "0000000000000000000073616e48a83e3e27b70ba27cd38c683d0014189b41b9", 950899733299880027476699870079860653644778702301560832 ], [ "000000000000000000016ca393ebd4f388b294134f5633a62d4268b3e4b544dc", 870306687010904716955275873664553942808871958151692288 ], [ "000000000000000000028a4784e2bd24e775437108c0e7a90469bff4a62895d2", 841292956506611632223096322365470292302662385308532736 ], [ "0000000000000000000136eb131bc275961ef87f4a06f56d849ef7de6067a83c", 859664032087861081904916640712713969859737457373741056 ], [ "0000000000000000000009f301f2215237cab791aedc296f102fd7b9dfbff456", 757060771140682373435345150716700036747812369126653952 ], [ "00000000000000000007ac57aa98595dd1daa3db89f17a817b445b083d696e0f", 731886405437657570669286679473162061734238931073892352 ], [ "00000000000000000006e6f83c247026057e769aace3815f8138941b256a3ad2", 733349368576625804490408567990711061036914519549411328 ], [ "00000000000000000001348162a93f4734709f6a142b19aeefd8714f46d0b8f9", 729612308889970685728561745873455525355654300037021696 ], [ "0000000000000000000428fc10bebcc825140bc83b01b8be32488bcd408e1389", 787270009984312136754615316208945606764100494789967872 ], [ "0000000000000000000152525a810f2033976f89ba434694c0fe5bf97730f625", 762342638057996256581733267702136683580848909336969216 ], [ "00000000000000000001fe51e048f4f42e4212098de90827f929403e17b71988", 790751306884434347505776493480475792916920926107336704 ], [ "000000000000000000064d0f3321a86b27d4883ab1ccedd12cf0941fb74f01a7", 717191006474295341826748628480199835971598529354268672 ], [ "000000000000000000053916f5f3b68319baf095c4205ad4edca517cd89b4a51", 685105199528332699160504931662746558558072186305773568 ], [ "00000000000000000006813d0260b4726b64e83025e904165ffaaf06d17227d7", 688509036841676372057001313638142781710850853198364672 ], [ "00000000000000000005c39a2670a916dc4075afaaf8fe30e495c13ba323e141", 626181838016062686207286970262124176054603953970610176 ], [ "00000000000000000005e07bf1518ede202f3a18b1a710e9700046755b3c7550", 619023402996415923713925321951479821824329196375113728 ], [ "00000000000000000005776a4242de5ebfc59d247a114188851e3d24fd173f61", 575524729764536260159429050275345090310309676098519040 ], [ "00000000000000000000eb00afd3cdc013b3033d0419037fa9c6f4243b5a7a79", 562973353703138465897895804931977651737504527419441152 ], [ "0000000000000000000060df647feb8b98f0b5e7ce312c35101bd5c6de001fcc", 553442901526103647968289576137834770166328191306694656 ], [ "000000000000000000044cb8431ecd498363f5ec16e05553b7f1f69b1a3a6a93", 561592234655860762640193322765060764283929671166328832 ], [ "00000000000000000001c951f76bd2d92beda3c1ab615d57558ecf654c900042", 544090752548823200194704196893283275123549878964191232 ], [ "000000000000000000036eb2f0d13b6e4fdb17da672479713a9e08d0f0875dbf", 526200511006255617572972890856003254679941608705622016 ], [ "000000000000000000033c59708025b67a8b8518d3760151eb4980e7a3a21e62", 514982024438103606772841406080073066221062670505738240 ], [ "0000000000000000000062afa9e3b9df5eee17c2edfc89f60778afe3abccb593", 532311049351936122673982497141590033985123062667804672 ], [ "00000000000000000003a3bbc3dac0c578948918c796426cda4c1958fc8454d3", 500073246235691066104245617101534263137552502634840064 ], [ "00000000000000000000dc8c2caadf5e8e8635adc7ff4d90dcfba264a3493e6a", 515199788182065911307653755120147792390991404454641664 ], [ "000000000000000000034e1a8f7c1efee7c36209a1556a377568d6368431dd17", 514581572989474939373253596435908804673676944988962816 ], [ "00000000000000000000967ad630a7aac2aeed84cc4c10ef1bce932cefdb374e", 484696787509332636501824648976526249487752436350189568 ], [ "00000000000000000002f985a10a40897d04888ac10c8ac75867a50b016eb6ef", 497866378763321402697758053004132675777872044494946304 ], [ "000000000000000000027ecc78c2da1cc5c0b0496706baa7e4d7c80812c10bf3", 471981723264553781113452590931894587216745823226298368 ], [ "000000000000000000047381dac259c4a8ce569c8498a2b9d11db6c080500343", 470321457404545875398373204961928889706416683857477632 ], [ "00000000000000000001df9394219ced52cb3789dc1c31408d6e01bec37c0b2a", 441737408381628076124145537003663826407985955181953024 ], [ "0000000000000000000353813d30e99afcc4579d469b57718f4521d570b3dce6", 431604817530012926192239390058441836232711374861500416 ], [ "000000000000000000009af85a7f58a31a21d77ad6dbeb8f52a11de6ec716b79", 416823189970048174077527321660349349771911273121841152 ], [ "00000000000000000000cc85f351633ed7b58e8b827ad0176798c4905084e95c", 396710004437100288117208210992507862855406329465405440 ], [ "0000000000000000000125ab365abaa653d41dd6acb9ecddf9bb9a944d1e0bfd", 400552292241643231889165698417718970914081776120889344 ], [ "0000000000000000000078f4e50a09391670d061deea0b3b87c3fa60cfaaf782", 374406027949793378682501776760424667692437142927048704 ], [ "00000000000000000001595b16989798f8997d89ac778ccc7d0d6c7e88ab4ec9", 368311566122123513513592411007997767500471904222838784 ], [ "000000000000000000026ab1b5e445f9320cf2ec3dc6720b501877d4e0b40eff", 383255420363831995852225088422521761376453814474768384 ], [ "0000000000000000000268358721568565da476fdf07250d18ee210bc2f874c0", 357069695527774208266769667274744118513278471102267392 ], [ "000000000000000000005b3388af9acbfd8c00b5b3ec888a75085011cc69cbb2", 329879919066870090376508314646890389215599502072741888 ], [ "000000000000000000010e2bc83838d1b9479b88341e4d0033dff524d16d5ef5", 339749439623765677783137798322223448447336014535458816 ], [ "0000000000000000000350156217f3a450f8994e7054b631db2f0e07f43f268d", 321145985282180614537323094086577881890135649195917312 ], [ "00000000000000000000c1d14709a7659153a2299083637a01475b7865926b0b", 324317443835188673869825090173572216042789022814175232 ], [ "000000000000000000023aeab989430385cf0c085e9e25580d1813ea3daee028", 312072983117630369221114618645075196904111619969122304 ], [ "00000000000000000002bf1e60049e942ac34b728911adda77d704cc8401e84b", 305996059309608474887223697110640892108382252455428096 ], [ "000000000000000000021342b77cc83903ed85341a53b9ec571fb7b0a503124c", 324234138241860812403487480138107387910668634659225600 ], [ "00000000000000000000a2a87e6a371a8d2e63eb61a051333c7a3251b717c723", 319495949933634025142671133910441198360944101354897408 ], [ "0000000000000000000132f5df736574a143d1e41e1a0cdb9c7d1656a906124c", 322033116776040472608672730780036665683066800249503744 ], [ "0000000000000000000112beeccb1e3ba4e55ee2987685cc397cdd21e4c65fe2", 322192420454509541026756932426802740532209296896688128 ], [ "000000000000000000016e792fc3bd650030e9acd074a910c7905bbe9bb79899", 339134147434449367654574047007649893296060866934865920 ], [ "00000000000000000001a481e800d3a60641bfc442db64a2c2d9d11d3a298705", 328583567114557579488061646200271046177164689907122176 ], [ "00000000000000000001b39765b103785831b8f5a23d9fb42187226d1faf82f3", 297348354121521522320212493955458645481078099598639104 ], [ "0000000000000000000314bc1981218b70bf539ae13ac1e41eefc1ad7a605049", 310338180674118587457206844748640968959780028040609792 ], [ "0000000000000000000073036581ef712215c5f9aebfeb7d2fba84a2f71dd69f", 301319254070149585548971905645948786445483268317904896 ], [ "000000000000000000028030eb24a8d0bd042bd589839edc932c876dda457c9c", 290914823913990887674279873321841567628552684544458752 ], [ "00000000000000000000a98d2c6a1875258ba9611e24b421c88325ec5155e2c8", 304956931645466202912380877194579614881406884417372160 ], [ "00000000000000000001924bab37e9d87715e84aa7bcd0b52405f893dfe7005f", 292880543616200952099263829421844968289989913814761472 ], [ "000000000000000000007c7decd9c85fe0c5273691cf361b548855d91176fdab", 281789207690496729853016065226361096453821041746116608 ], [ "00000000000000000001b4ceba765aa1ecec00c4e3710b7d5115a90060c48cf6", 265227471136262937983931908702020177275080014169112576 ], [ "0000000000000000000026f3661ace0cda87b922d13d35280c9ac61c6ad364cf", 263561359269705708657179707992723614632672251070119936 ], [ "00000000000000000001a2a4d658523967c398ae9c4adda72c02f4072ec398f7", 259426771137696584301581483600969249970065617906040832 ], [ "0000000000000000000087427d61da6ce8b57e2726a9c62be8637cc906bad7e5", 248423125310232216230425940495448355115076101789974528 ], [ "000000000000000000002428ab473fa8570115d4a000e637814b86248d35d370", 245573197117436955539928755071651603226747033331171328 ], [ "000000000000000000006d0b2fecfc61125fe5a7c6387fdca048c4b3cd5ffa84", 244083926948996765466279200227113710829717638069878784 ], [ "00000000000000000001651e40ff05e359a13e1740d9c21f400abdc5f1b287c9", 249381870384321288544767557745710236775970393538166784 ], [ "0000000000000000000268d24974a0f3d8ec73bfc6b0a470c6bc891951b1e3b2", 236140665550103308105842173161300712617887644698804224 ], [ "00000000000000000001a3361e40ce264e71620908f8e8cd555b4182f7813bb7", 243826702660826526552675351696555645018258193942315008 ], [ "000000000000000000009e2b057f15897a5bbe33a9f5a4ca96d426368554f34e", 240389250809824242889060284970006947356027440601235456 ], [ "000000000000000000023d9b58d8793462a5d4fd778b8fe1ba9d8cd8f26816eb", 236991259503029893604236717733941589335327397438816256 ], [ "00000000000000000002065cd1b5c268e8e375d1a176da03d22479d0c02e2da6", 221874948068116364721256005509157074063026087146815488 ], [ "00000000000000000000ca2f64794d814b198eb237974573222e4521545582d5", 218766334085513534214236767869969540080217918627905536 ], [ "000000000000000000008765cffc1a859242108b85e6415ae144ec918a05787e", 226329605058700956815940836879276304706937369537806336 ], [ "000000000000000000006ebc27176d134d4885a125cd7e3a13783a6c9bcd12c9", 221600185760298154972633712760606412855330771828736000 ], [ "00000000000000000000a4d531b0328083c2f19a7f6c31e22e18cb73ff7a4d2c", 212309419851785605121612888279029001699378008653037568 ], [ "0000000000000000000108312d8ba9c66bf9c806b9c7bf2e923d282508941005", 213268164925874677435954505529290883360272300401229824 ], [ "000000000000000000009d9d77c8ae464658da8ad3ba1b2d07eca1937b9cef39", 230505115236555346453248764446346725294094368813088768 ], [ "000000000000000000011a2a7498613bba536d6f0f5649aca4e6a484d6312211", 213504928191122283708703502472190921209456561473191936 ], [ "00000000000000000000a4b1cede1eba74d67df322ca415468f277aa7b508f47", 211248369663083369602997013090476980227107801626836992 ], [ "000000000000000000016e02290e770058422eef8df58d85c037a2d21ca9afb4", 208285905844213629387798143934561074546265226362224640 ], [ "000000000000000000010ee7b1a4f44b8cf580f3fe692f362c43741f10b81c8b", 207862070369387667541519075333073352470565005924761600 ], [ "00000000000000000000b34c3fbb0151989a03d8fe589af6a5621528e6786c29", 198173776015521112096746848576997112333265829097373696 ], [ "000000000000000000019f01622aa9ccac55b505788f3310454eb40145b7d73a", 189398920184986370975851924841368549083251610109345792 ], [ "000000000000000000010919a63dfd4bb86037ad9f2c26cb8e4f3b18d345cb4a", 178729958232470779672965025562539683039763302545620992 ], [ "00000000000000000001d4b4cbfc67c0904765c9b6d6dd00ac3a72fd4efaac90", 183753139359977093002831090332585547778320742695829504 ], [ "0000000000000000000095fdebd0bf892913da1bf6f109a2fbddd7b4d13bb8fb", 172847414142213895427195194110856643885648174060142592 ], [ "000000000000000000014b809628248ecd176df547224411fedafd577041929d", 177049231349540241317030788004915957567158980121198592 ], [ "00000000000000000001c83578eeac30ea13eacc3668da7e37c8aed04992ec95", 180571450295507717349901668451762199644529777549770752 ], [ "0000000000000000000144a6109dda22b210490144247f34903e23ea98281065", 181918954805126809840485465867526612588652547354394624 ], [ "00000000000000000001d39c254559a52d78a36cb5373269db169857d9aa490e", 181841495218348271985820670571392649588610782929616896 ], [ "0000000000000000000159186ff01cb830839027afce1c072816ad1caf6735c2", 184058593202179251712735660462623250929428832597311488 ], [ "00000000000000000000fd7f37b8fb43c92b11c9d2809f115d22a6b5dbeb7836", 190300666695219538076383598383154495706379320488361984 ], [ "00000000000000000001209d8bd5b083d0eec0a0cfb72a9f922a5f46e3b4744f", 214194756963942469886095641713233006794734161633476608 ] ] ================================================ FILE: electrum/chains/mainnet/fallback_lnnodes.json ================================================ { "0294ac3e099def03c12a37e30fe5364b1223fd60069869142ef96580c8439c2e0a": { "host": "8.210.134.135", "port": 26658 }, "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f": { "host": "3.33.236.230", "port": 9735 }, "03cde60a6323f7122d5178255766e38114b4722ede08f7c9e0c5df9b912cc201d6": { "host": "34.65.85.39", "port": 9745 }, "027100442c3b79f606f80f322d98d499eefcb060599efc5d4ecb00209c2cb54190": { "host": "3.230.33.224", "port": 9735 }, "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025": { "host": "34.65.85.39", "port": 9735 }, "02f1a8c87607f415c8f22c00593002775941dea48869ce23096af27b0cfdcc0b69": { "host": "52.13.118.208", "port": 9735 }, "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5": { "host": "213.174.156.79", "port": 9735 }, "033e9ce4e8f0e68f7db49ffb6b9eecc10605f3f3fcb3c630545887749ab515b9c7": { "host": "213.174.156.72", "port": 9735 }, "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226": { "host": "170.75.163.209", "port": 9735 }, "037659a0ac8eb3b8d0a720114efc861d3a940382dcfa1403746b4f8f6b2e8810ba": { "host": "34.78.139.195", "port": 9735 }, "03a93b87bf9f052b8e862d51ebbac4ce5e97b5f4137563cd5128548d7f5978dda9": { "host": "134.209.139.244", "port": 9735 }, "0288be11d147e1525f7f234f304b094d6627d2c70f3313d7ba3696887b261c4447": { "host": "18.219.93.203", "port": 9735 }, "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e": { "host": "164.92.106.32", "port": 9735 }, "02c197ffa4c2aa4105dd4c4b7279ba1b9061b22910ebbfa759b0001bed9ee48a16": { "host": "18.181.210.139", "port": 9735 }, "03c8e5f583585cac1de2b7503a6ccd3c12ba477cfd139cd4905be504c2f48e86bd": { "host": "34.73.189.183", "port": 9735 }, "021c97a90a411ff2b10dc2a8e32de2f29d2fa49d41bfbb52bd416e460db0747d0d": { "host": "54.184.88.251", "port": 9735 }, "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590": { "host": "185.5.53.91", "port": 9735 }, "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3": { "host": "57.129.59.146", "port": 9735 }, "02e4971e61a3f55718ae31e2eed19aaf2e32caf3eb5ef5ff03e01aa3ada8907e78": { "host": "52.38.27.190", "port": 9735 }, "0391904d140fdf88d19423513945a5fcc49c606521b65a85f6d6fe46ebdd1c7665": { "host": "5.75.184.195", "port": 35933 }, "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2": { "host": "45.86.229.190", "port": 9735 }, "027ce055380348d7812d2ae7745701c9f93e70c1adeb2657f053f91df4f2843c71": { "host": "157.90.112.145", "port": 9735 }, "029efe15ef5f0fcc2fdd6b910405e78056b28c9b64e1feff5f13b8dce307e67cad": { "host": "103.126.161.206", "port": 9742 }, "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e": { "host": "3.132.230.42", "port": 9735 }, "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c": { "host": "63.35.146.37", "port": 9735 }, "03037dc08e9ac63b82581f79b662a4d0ceca8a8ca162b1af3551595b8f2d97b70a": { "host": "34.68.41.206", "port": 9735 } } ================================================ FILE: electrum/chains/mainnet/servers.json ================================================ { "5.9.83.108": { "pruning": "-", "s": "50002", "version": "1.6.0" }, "104.248.139.211": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4.2" }, "128.0.190.26": { "pruning": "-", "s": "50002", "version": "1.4.2" }, "142.93.6.38": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4.2" }, "157.245.172.236": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4.2" }, "159.65.53.177": { "pruning": "-", "t": "50001", "version": "1.4.2" }, "167.172.42.31": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4.2" }, "188.230.155.0": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4.2" }, "22mgr2fndslabzvx4sj7ialugn2jv3cfqjb3dnj67a6vnrkp7g4l37ad.onion": { "pruning": "-", "t": "50001", "version": "1.4.2" }, "2AZZARITA.hopto.org": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4.2" }, "2electrumx.hopto.me": { "pruning": "-", "s": "56022", "t": "56021", "version": "1.4.2" }, "2ex.digitaleveryware.com": { "pruning": "-", "s": "50002", "version": "1.4.2" }, "37.205.9.165": { "pruning": "-", "s": "50002", "version": "1.4.2" }, "68.183.188.105": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4.2" }, "73.92.198.54": { "pruning": "-", "s": "50002", "version": "1.4.2" }, "89.248.168.53": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4.2" }, "E-X.not.fyi": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4" }, "VPS.hsmiths.com": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4" }, "alviss.coinjoined.com": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4.2" }, "assuredly.not.fyi": { "pruning": "-", "s": "50002", "version": "1.4.2" }, "bejqtnc64qttdempkczylydg7l3ordwugbdar7yqbndck53ukx7wnwad.onion": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4.5" }, "bitcoin.aranguren.org": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4.2" }, "bitcoin.lu.ke": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4.2" }, "bitcoins.sk": { "pruning": "-", "s": "56002", "t": "56001", "version": "1.4.2" }, "blackie.c3-soft.com": { "pruning": "-", "s": "57002", "t": "57001", "version": "1.4.5" }, "blkhub.net": { "pruning": "-", "s": "50002", "version": "1.4.2" }, "blockstream.info": { "pruning": "-", "s": "700", "t": "110", "version": "1.4" }, "btc.electroncash.dk": { "pruning": "-", "s": "60002", "t": "60001", "version": "1.4.5" }, "btc.litepay.ch": { "pruning": "-", "s": "50002", "version": "1.4.2" }, "btc.ocf.sh": { "pruning": "-", "s": "50002", "version": "1.4.2" }, "btce.iiiiiii.biz": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4.2" }, "de.poiuty.com": { "pruning": "-", "s": "50002", "t": "50004", "version": "1.4.5" }, "e.keff.org": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4" }, "e2.keff.org": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4" }, "eai.coincited.net": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4.2" }, "ecdsa.net": { "pruning": "-", "s": "110", "t": "50001", "version": "1.4" }, "egyh5mutxwcvwhlvjubf6wytwoq5xxvfb2522ocx77puc6ihmffrh6id.onion": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4.2" }, "electrum.bitaroo.net": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4.2" }, "electrum.blockstream.info": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4" }, "electrum.dcn.io": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4.2" }, "electrum.diynodes.com": { "pruning": "-", "s": "50022", "version": "1.4" }, "electrum.emzy.de": { "pruning": "-", "s": "50002", "version": "1.4.2" }, "electrum.hodlister.co": { "pruning": "-", "s": "50002", "version": "1.4" }, "electrum.hsmiths.com": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4" }, "electrum.jhoenicke.de": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.6" }, "electrum.pabu.io": { "pruning": "-", "s": "50002", "version": "1.4.2" }, "electrum.qtornado.com": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4" }, "electrum3.hodlister.co": { "pruning": "-", "s": "50002", "version": "1.4" }, "electrum5.hodlister.co": { "pruning": "-", "s": "50002", "version": "1.4" }, "electrumx.alexridevski.net": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4.2" }, "electrumx.erbium.eu": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4" }, "electrumx.schulzemic.net": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4.2" }, "elx.bitske.com": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4.2" }, "ex.btcmp.com": { "pruning": "-", "s": "50002", "version": "1.4.2" }, "ex03.axalgo.com": { "pruning": "-", "s": "50002", "version": "1.4.2" }, "explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion": { "pruning": "-", "t": "110", "version": "1.4" }, "exs.dyshek.org": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4.2" }, "fortress.qtornado.com": { "pruning": "-", "s": "443", "version": "1.5" }, "fulcrum.grey.pw": { "pruning": "-", "s": "51002", "t": "51001", "version": "1.4.5" }, "fulcrum.sethforprivacy.com": { "pruning": "-", "s": "50002", "version": "1.4" }, "gall.pro": { "pruning": "-", "s": "50002", "version": "1.4.2" }, "guichet.centure.cc": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4.2" }, "hodlers.beer": { "pruning": "-", "s": "50002", "version": "1.4.2" }, "horsey.cryptocowboys.net": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4.2" }, "kareoke.qoppa.org": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4.2" }, "kittycp2gatrqhlwpmbczk5rblw62enrpo2rzwtkfrrr27hq435d4vid.onion": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4.2" }, "node.degga.net": { "pruning": "-", "s": "50002", "version": "1.4.2" }, "node1.btccuracao.com": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4.2" }, "nuzzg3pku3xbctgamzq3pf7ztakkiidnmmier64arqwh3ajdddovatad.onion": { "pruning": "-", "s": "50002", "version": "1.4.2" }, "qly7g5n5t3f3h23xvbp44vs6vpmayurno4basuu5rcvrupli7y2jmgid.onion": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4.2" }, "rzspa374ob3hlyjptkdgz6a62wim2mpanuw6m3shlwn2cxg2smy3p7yd.onion": { "pruning": "-", "s": "50004", "t": "50003", "version": "1.4.2" }, "skbxmit.coinjoined.com": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4.2" }, "smmalis37.ddns.net": { "pruning": "-", "s": "50002", "version": "1.4.2" }, "stavver.dyshek.org": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4.2" }, "tardis.bauerj.eu": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4" }, "ty6cgwaf2pbc244gijtmpfvte3wwfp32wgz57eltjkgtsel2q7jufjyd.onion": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4.2" }, "udfpzbte2hommnvag5f3qlouqkhvp3xybhlus2yvfeqdwlhjroe4bbyd.onion": { "pruning": "-", "s": "60002", "t": "60001", "version": "1.4.5" }, "v7gtzf7nua6hdmb2wtqaqioqmesdb4xrlly4zwr7bvayxv2bpg665pqd.onion": { "pruning": "-", "t": "50001", "version": "1.4.2" }, "v7o2hkemnt677k3jxcbosmjjxw3p5khjyu7jwv7orfy6rwtkizbshwqd.onion": { "pruning": "-", "t": "57001", "version": "1.4.5" }, "venmrle3xuwkgkd42wg7f735l6cghst3sdfa3w3ryib2rochfhld6lid.onion": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4.2" }, "vmd71287.contaboserver.net": { "pruning": "-", "s": "50002", "version": "1.4.2" }, "vmd84592.contaboserver.net": { "pruning": "-", "s": "50002", "version": "1.4.2" }, "wsw6tua3xl24gsmi264zaep6seppjyrkyucpsmuxnjzyt3f3j6swshad.onion": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4.2" }, "xtrum.com": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4.2" } } ================================================ FILE: electrum/chains/mutinynet/fallback_lnnodes.json ================================================ { "02465ed5be53d04fde66c9418ff14a5f2267723810176c9212b722e542dc1afb1b": { "host": "45.79.52.207", "port": 9735 }, "032ae843e4d7d177f151d021ac8044b0636ec72b1ce3ffcde5c04748db2517ab03": { "host": "45.79.201.241", "port": 9735 }, "0220566172d9e324b41ec6f74ca44d377d3faf72ddb310fd263e6d5bcde4882492": { "host": "185.90.61.24", "port": 9735 }, "035a4e767bb1be29ed20219b40f07d9be03656a5f83485821878963c05290a877c": { "host": "54.158.203.78", "port": 9746 } } ================================================ FILE: electrum/chains/mutinynet/servers.json ================================================ { "5.9.83.108": { "pruning": "-", "s": "51234", "version": "1.4" } } ================================================ FILE: electrum/chains/regtest/servers.json ================================================ { "127.0.0.1": { "pruning": "-", "s": "51002", "t": "51001", "version": "1.4" } } ================================================ FILE: electrum/chains/signet/checkpoints.json ================================================ [ [ "000000c0a3841a6ae64c45864ae25314b40fd522bfb299a4b6bd5ef288cae74d", 0 ], [ "000001299957ea59afccbd3f1c4719a466350aea3fda78d419dfde37f9823420", 0 ], [ "000000647c13ba87cb352cbbb464d47f15f1733332c3b910888ff36d858961ce", 0 ], [ "00000096b144d9ab24117213adf6e9e52afb606629f30f10d084ebb4bb80e3c3", 0 ], [ "0000000d41b251c50f6e7e54855b68f4a57004164b9d6014f84c111cd60c2680", 0 ], [ "00000102ea9911f35f8ee910b2bf56946b8163766b8d71b6aae668b2bd1f1501", 0 ], [ "00000108f2a56ca7ccdb6bef0372eead4ba9eab39b2795be544fc8e90b93c01c", 0 ], [ "0000014938ef851ea035d13c02ccdca9e8c3499d33466daab04a1a0a798bbbfc", 0 ], [ "000000739a899ef6a642dc478966abc174a789a88280d83579cc057b95d19aef", 0 ], [ "000000d5237e345c02c467e9d4eda4a207a35af96473daca024c6cd363c7db83", 0 ], [ "00000000bbf300f7561f228cf55f51237beb76f23a4370363ea41e930ebf63a1", 0 ], [ "0000007dcebd40572e399d89055df55c19ea5a67d2a29155e0dd2f35313aeec0", 0 ], [ "0000013d4b88ae84d4b29f64e2ae2466dd0130dfb2f2ce8d9496a469a543fe55", 0 ], [ "0000014ce59f8d976fe17e7cc8d8e5698890995b98073c78bd7c493ba72a2e62", 0 ], [ "000000933087962ab5518c60cc4ca4bdbfeb6dbc7fd3d3d420bc8ef46dc53202", 0 ], [ "000000b622bcdf74d742db0b4d9653034d2549a4e9642962091cad05b1082645", 0 ], [ "000000dfd2d4606684bbeb504959ce225c2ad84d0343c35cdad78d59dc86395c", 0 ], [ "00000120bd3e4e26b6b1418860ead049560f7beec69a4a496cbc780e3f34d8bb", 0 ], [ "000000db40817d098b504b8a524b2df500b6f6c9b9abd954c4116dda7620e0dc", 0 ], [ "000000dbc1b9e4c662ac78c95fc46e689b54d0c2c284dde285dd54dd9b24b123", 0 ], [ "00000041d0bd45cf26d854ff7188e773b87466fd33bfdbf7397ecdade8724318", 0 ], [ "0000002327e4edf0f436fbb87392a68c2bfeb6284d215704a017e741fdc1154a", 0 ], [ "000000f198a4c393044bd9b3c4b340bebe811ce721b5d6788f931282f5eac3fd", 0 ], [ "00000106876fcfbb809096bf96df6dfe1de43e408bee2f88500954d38bf3cb72", 0 ], [ "0000003e477f554425d1b570dc23535f5365e0424a1b4a4b81dc373db1421430", 0 ], [ "0000002cd0191c4d8cc646fe277a6d9be51d5d5e116ed75491291bf18ab7fe68", 0 ], [ "000000940edf438dc29af2fe8e77d831eb3e2434f372d68598da354310289d4a", 0 ], [ "00000103744843bf10a3f0ddd264fae8c7199fadd7187883975c3dc9976b6a61", 0 ], [ "00000141abbf269a681f83cf6b243e505d5e29f483f18f366f3cb9419bace51d", 0 ], [ "000000d0a1eb4b089678e0ebd64f211e0b9cb68f2f1fd1b56ab1d5eca03d167d", 0 ], [ "0000005a587db11708cfd476c5a8308e9ddb210dbda90c64ccae2092fe218367", 0 ], [ "0000011dd3d96c272e17eb43ddf33651238871aa525c9b7885e88ee4c5898337", 0 ], [ "000000055aee09f4d9a965638fea3e45130a3f601363d0614e618481bad3b519", 0 ], [ "00000038bcef9e43f8a75438a925403aeb1b77ed9013c1c719b57c77e8137d2e", 0 ], [ "0000003ce9a4b46fbb431c198aedf59ef322c0791f03e3bf153548fea29dc375", 0 ], [ "000000715d041a78f798c6ef7cce244a0a7751acc839446bfe52d84087696162", 0 ], [ "0000005bcfde62a40e676a73f6fd481b4d5d524891b7b5a7e5874da0af640885", 0 ], [ "0000003a0ebac1a5bf4ec8e7d9f94d672f9ce217eecbcfe1837b0b4d68bb7efb", 0 ], [ "00000002b09a09076aaa8cdeddf164c14bd69c0d3c7c700cc2a1e70d5782fba3", 0 ], [ "0000010e5f6260be27d95a9c6b77b3427f675ac6a79ffa848d2065122e0a49fb", 0 ], [ "00000027c6cadacbf945fde93ca6fa9b2c404216b7b54d46d12acd6bd2084403", 0 ], [ "0000011d5a1230c91fcb07e1ffed7c74a77824f49632d7a85984afc5fdb84210", 0 ], [ "000001420feee79c0518790bae4fcf3d11379f46b7c92568d5d49ce061c14aaf", 0 ], [ "0000012beab6aa1841d9ca386986bf4a1227e4a1c9c4f6bada49dfc5c45a7b4c", 0 ], [ "000001425fa8c62dfd856ae0fee3b36add930a5826778f62c54c5e7a089cb2cd", 0 ], [ "00000047aa330a7bfd668afa6fceec2d71b0a01d4c3e940f01fea16cf8a5495a", 0 ], [ "000001138c3f76d7b699e3f610946b35ab8d0fb670d7b277e848cf93e2963478", 0 ], [ "0000011579b1ffe43056593c6d2541b20ecad58654a70b926b80f02bf7e3afa2", 0 ], [ "0000002a0af0dd33f39c900d2b08ca7b53ec7ad5cf8cdac6c0a594c824528554", 0 ], [ "000000174c981f1f0f785a066db5ab640fd81db7131d3bc6c0ae5f1c881a5869", 0 ], [ "00000010b2e80adbf6ebef457bfe030f028fd7c054eb91967067ecbe32391e6a", 0 ], [ "00000056b45f9fba1d07427704b8929aa5a6273864c3c28fbf0561a970927eba", 0 ], [ "0000015fee8d0e568e47beb80e459c1e55f0939bcfb67c8d71d6aea4bbef07da", 0 ], [ "00000026f4c92697d195eb8380852e8e2f72f00bd237ed7b6a34ab27d46df667", 0 ], [ "0000006997e8fb35db20f6cba346e8a4f5ad3e53b2816b04b5f15ee7d300c507", 0 ], [ "00000153eeef8fe2c11990a87b680c86595bd1eefe62e912bd4d2d0181dd275c", 0 ], [ "0000011255d8a68c30b8dc8f9c0d5bd83484dbd1a67c19f9773e942b869184d4", 0 ], [ "00000127cd6eb4b0bb794bafce0990f25ad88da0338aaa643b08f73679ee7c40", 0 ], [ "000001351829786299ce95aa0e4fff6ca6fb0579176476ebb23cd9c5b18dc38b", 0 ], [ "0000003cd9b6c744cb5c8fdc012c200c9fbb72c8e82a535286ad29e9daae0b7b", 0 ], [ "000001195e3dc8e195350793514679ce2b2951c8e69e2ab759627b7d4dae0174", 0 ], [ "000001469586fbf43e84517bd8aebd65f556a3f8caae21676fde3d8a37d548c5", 0 ], [ "000000f0298f92b43e87c972155819d6e6fa80a99a309a7a63a447748fdbcae2", 0 ], [ "000000793ff1190b63052655624f2e3e771257ee5e4a95d0fe8bf8900119f4a7", 0 ], [ "0000002b535d083651a6a816526ab64e18a3473117e11bf9fdb37409fd2b8476", 0 ], [ "00000056df0365d1e9ac806b3124919922d8d2fa528ec230562b384ef957e049", 0 ], [ "000000e4d73bc1ff88044907fdabcd18fb4feeae8d89214e00a835497eff9d44", 0 ], [ "000001164ada2f23afdc6e7eb7a549d0240ec00795c97a6b7b0da459ba144236", 0 ], [ "0000003a02c60020ea02296f487408b092ea86fcace4b71fe65ddf8e9d1227af", 0 ], [ "00000011c1bc4de2a5565d4f6b52e2d20830ae03cc33da840752a9b082f22970", 0 ], [ "000000590d9132c7ec8cf2fdc08d841aa30f1a6c98baaa41bb2f2ced082ffc56", 0 ], [ "000000aad06d12ca4f1614a04529b9b0db229edd3b999c8f2387bf4499e7e823", 0 ], [ "000000f21e5a641ff87de60c94f9f95c28291989fa52980862a96b1da9420c20", 0 ], [ "000000389253c73a42abfce6f70dd8b4b47c1e94ff70f3650136315f1c7c3d51", 0 ], [ "00000052d02504f8361173e62831ffb1ed06502e39bfe56e7b2e8534527acf54", 0 ], [ "0000006e903dbc8f9c6a155c31ac0eabc64a0f538ec1956c12bf0296365ea10f", 0 ], [ "0000012d6d51f5edeb1bc181aa56058ba399c56a233196a609d17ea9fe1260f4", 0 ], [ "00000078fe34ae6dcfebd6c54750fc5b23c9ccfccb85410a67dc873950089470", 0 ], [ "0000008ba099907877add94606ade4d8c4fce72e78e4c9a7211511e191cb3090", 0 ], [ "000000ba16aeb089d8272fda74e376bc74063e202b41c1d910466b28d0e22aa1", 0 ], [ "000000e9305b2194cc90a8f36ac85d6e335f9c40eebe31a63111b1a2cd62ef60", 0 ], [ "000000b20594e8e7b7c883ebcd4fb5038ac6e5490d8089643e63fcbbed54159c", 0 ], [ "000000ebaf4db2459b51830938659a587f72bd56fc713f8c15bbd877eb6e4cdf", 0 ], [ "00000121189334c3922651f449fb752026ac9d3c83ca36f1b379629acb8a4bc8", 0 ], [ "0000011331e953cf2ebd0231d00cf8ce3b0925d4642d32359922d87dde28e6a5", 0 ], [ "000001088db354ec53ca636d01718cbd5948d240d37b2bde660cf46e64b7bd13", 0 ], [ "000000a163f70345b7c90e1053b582aa415f8211ced4f9693edf493032d8b969", 0 ], [ "000000116824a8e4a6c2d6d0e32b8df377f701091d6a4ff2457790052fec8c2f", 0 ], [ "00000071aed5672329b4ceaa9fe0dc56a73a9aaa6ae368e83fd49d2fc613d182", 0 ], [ "0000006980fba2dab31f8a62e96484952512fefd1ef1a7d1ac6b2b70520298d4", 0 ], [ "0000010a5c600b597167962263ee3f34a9844f11cf62eae87254140fd1ca4c4b", 0 ], [ "00000032c5455af9f5ecbd52120902a2476b525bdb1bdc95304c72a75298576c", 0 ], [ "00000145fda2984b2467e98dca41b15a210817ce8507ed6470bfabaf6e1d58a0", 0 ], [ "000000fc300d0bb620acb44dcb5fd4a75746de32a250724e26346798781b762c", 0 ], [ "0000013835a5ae1a46e514eca524b79ea5acbf16bbd16d4913d5711fbfd0b43a", 0 ], [ "000001105408d97b67a382a8682793c6fa17e8a9e14431ff46931852d98809a9", 0 ], [ "000000b246b4b35ffc56055ff91448ba0c395a7697369680460aab62560c71c8", 0 ], [ "0000001af62d9a537a82ca68c50cab0726fe74a209358fa7ae02aa12fb68dc03", 0 ], [ "000000357adc640f81f63c32cc2bf924953a9087465478eace40ad368bbb610b", 0 ], [ "00000074d93559ca744568b640e31497e54448f5e81eb458d5e6309a866fbdc0", 0 ], [ "0000000e5f9f2f28a8e84fba06965b939eb33d10f367b285e0b765f73432293e", 0 ], [ "000000bc8fbad304cc9f7a2129b3ada77205993d6797aaa3c412d47378cfe1d3", 0 ], [ "0000001bfaddc170487792c89290b630bf2987cf465bceee6228c98339d5b51c", 0 ], [ "0000005d328174d05468a64f3a1c9b876607b229803896be461d4bf71a604ad6", 0 ], [ "000000767328d1681b20d958b8e3ace547f35d780e3fbca8f6917896d7a040fb", 0 ], [ "0000013f4af09699526f35b5d991b6acc392ad3b71d97955acbc061896f613da", 0 ], [ "000000a5bee294beb00596090e6f193fe72463b22feda343c108ed89f81cb395", 0 ], [ "0000008c2e331069893d102b28aae90710ece9fd4926d005de976f97e44b66e7", 0 ], [ "0000015205ced605eded9c5d62e3cf8ed5229f9a72e7beb4bc8d63655b36abf3", 0 ], [ "0000013ae7e94521be9703ffe12b1214d47fcf56fb1268a6652db590ec06bbc2", 0 ], [ "000000d18fb8d06bda4847c4a629e17295a2fa1092e14ffbff0d97a2eae72993", 0 ], [ "0000010f9a0dae27ef69bef205f2e6d900e120b29e684a2a08286abc95579840", 0 ], [ "0000006e29c2511c1ad31fe21d0064594c557e5fac6f73f7961f2b53cf36b983", 0 ], [ "000000dffd144d2e1eac01a786558c04cb65a8ed0002397db7b43011c223174e", 0 ], [ "0000008365805832101e989fbf04bdca8c364b8afcb794b2166db74a4a92cf8f", 0 ], [ "0000004a9fde1918b96cb079d4b48a98098231d34f162e8e898919e0573ada05", 0 ], [ "00000063f777d9004ebcf1685a07dc2e36c3ed40758d5ed5b7dc82ca2165f8da", 0 ], [ "00000084364ae6e59f9223629293d017a1718f587e08b45478e79ca3681ba813", 0 ], [ "000000e826cbbeb467e73577566db478f432295952489e154a3de4db0012dc91", 0 ], [ "000000d2f92728dbccd1782fde6a555a89f47ba4102d3bc0e0d07ac6c99cb468", 0 ], [ "00000066d082b9414191fbf2b18746513e10f403eb3d6e837f4a007375f3bcbf", 0 ], [ "0000007b872779a56424e1eae91113b3f43e7da689c5d2e9e86c5b8bbe2f3315", 0 ], [ "000000057927903d3c9ea07cc6344cfa7299541470c7c97caf24b9d9e39bfb38", 0 ], [ "0000000842d0fa799de1397043352af7fe7da168314c9a0a173fa13304d396a8", 0 ], [ "0000000b66f892759e13adc4249d1200a4c9bba92d56830816d5804cd2254d29", 0 ], [ "0000000bd582d47ad9e2457a02af92c63fd1be6bd7aec7f71d7aec5e145e2bed", 0 ], [ "0000000111a8861cafcd74da47754738fef19072b5da46ee663a52e11f042d6b", 0 ], [ "0000000c210b5e309f204bea96516a1d2b147b4855f2afc501445463ca55726f", 0 ], [ "0000000f0532cadd9795ad911a851d75ce9e198bf671a0e1299a39b86f9ff0ea", 0 ], [ "00000006a125ea1b6fecbdbc49a106a8ab3c8eb6e3ed6f49daf33818ba635d59", 0 ], [ "000000021f43833bc6dc117c0a99abd8428b30e042865f2bdfe89258ab5ce450", 0 ], [ "0000000bb33efcb77b81fb0ee743523a02a93f662d2ea8fb083662bbb47e02ca", 0 ], [ "0000000c62a5d5e42d512c83c542bd936bb5f735cb416d555476be79185f6206", 0 ], [ "0000000d896f71fab78384d2c78c8e6a2367315d282464be8233b0bc21d877bd", 0 ], [ "00000000f5d2c35c12db500f5d250ad24e8f363d2937d1c487b5f86731adb552", 0 ], [ "00000000d72f5a316078cd8747e8b8027baa8f661649bf1a8ed722e4f4231d3a", 0 ], [ "000000079f91871f027e6e3fc37dd638759f1e7b8498fd71c7ff397132b20f86", 0 ], [ "00000004cafb2f9b4aa75f426cfcd4bd2774cf14b3e7d89a8b1945e5d8de8195", 0 ], [ "000000034c3e13346b9843061f780d74ce2c18a264610b26f26576f7631c7aa1", 0 ], [ "000000110f528afa3f436cdb42d15287c378559a85d1e1e110a11ffd5309d4e7", 0 ], [ "0000000019963af20c504e4e78fc7c5bbc5fd52efaa19b4ee778f6f213072c93", 0 ], [ "000000046df3b5b7b222fe177bfc016e92d760bf31d8670be3e5b2318a57c27f", 0 ], [ "000000003da79540fc14abc1d14ee5b4cd635e4e903cf116862abc21e82db694", 0 ], [ "000000102cd648540dabef56265de9ebfec7c43bd3192d873613f06e9bac372e", 0 ] ] ================================================ FILE: electrum/chains/signet/fallback_lnnodes.json ================================================ { "02357a375a846279fc1e8413f5e182652a125e5f6a4f4653bffabebb8177a6d8aa": { "host": "34.68.95.152", "port": 9735 }, "0305061295fa30847df41ae6ee809b560e78d65c2a7337a41c725ea3920b65e08a": { "host": "34.124.125.201", "port": 9735 }, "027554f8d4d99a43cf1b49d274f698ee5045273cd377206eba62ea308b4386a4fa": { "host": "35.247.14.99", "port": 9735 }, "0244bb7ba2392ab2d493ad04ad4afcd482ca44a2bfe5b42bcc830bfe00e5b08082": { "host": "34.138.100.228", "port": 9735 }, "03adf6efe5346d455172c750a655b07fb85be4f50f5b555f9f91a853a6b448c3bf": { "host": "34.74.81.232", "port": 9735 }, "03ea42c9408a73dabdcb5655e2923956d132fbb25cb71e7c00a29e10c73e937e64": { "host": "34.138.237.159", "port": 9735 }, "024d899b60d5de58e8d66af042445323a48b6962d6c667c033802421dc49abc232": { "host": "34.75.211.29", "port": 9735 }, "02e8430ba207ce87bd2d4ab36497b9eac10e6d5d86f9fda8aa270c48877e0a8259": { "host": "34.73.252.102", "port": 9735 }, "0265ed138065b84d6b9448f9e0a2fd4ceb63fef08efe1dfc949a63d5d43110e4c0": { "host": "175.45.182.145", "port": 39735 }, "0307238136c48cd35084c4efadc486143a7e8a7acd8ff8ac053fdab4efabc551c4": { "host": "104.244.73.68", "port": 9735 }, "020ee56ff81d12d17d5d3eea5306a8982a5763522ca73e0e220ce282030543c90c": { "host": "84.247.50.180", "port": 44149 }, "0271cf3881e6eadad960f47125434342e57e65b98a78afa99f9b4191c02dd7ab3b": { "host": "signet-eclair.wakiyamap.dev", "port": 9735 } } ================================================ FILE: electrum/chains/signet/servers.json ================================================ { "signet-electrumx.wakiyamap.dev": { "pruning": "-", "s": "50002", "t": "50001", "version": "1.4" }, "electrum.emzy.de": { "pruning": "-", "s": "53002", "version": "1.4" }, "mempool.space": { "pruning": "-", "s": "60602", "version": "1.4" } } ================================================ FILE: electrum/chains/testnet/checkpoints.json ================================================ [ [ "00000000864b744c5025331036aa4a16e9ed1cbb362908c625272150fa059b29", 0 ], [ "000000002e9ccffc999166ccf8d72129e1b2e9c754f6c90ad2f77cab0d9fb4c7", 0 ], [ "0000000009b9f0436a9c733e2c9a9d9c8fe3475d383bdc1beb7bfa995f90be70", 0 ], [ "000000000a9c9c79f246042b9e2819822287f2be7cd6487aecf7afab6a88bed5", 0 ], [ "000000003a7002e1247b0008cba36cd46f57cd7ce56ac9d9dc5644265064df09", 0 ], [ "00000000061e01e82afff6e7aaea4eb841b78cc0eed3af11f6706b14471fa9c8", 0 ], [ "000000003911e011ae2459e44d4581ac69ba703fb26e1421529bd326c538f12d", 0 ], [ "000000000a5984d6c73396fe40de392935f5fc2a8e48eedf38034ce0a3178a60", 0 ], [ "000000000786bdc642fa54c0a791d58b732ed5676516fffaeca04492be97c243", 0 ], [ "000000001359c49f9618f3ee69afbd1b3196f1832acc47557d42256fcc6b7f48", 0 ], [ "00000000270dde98d582af35dff5aed02087dad8529dc5c808c67573d6dabaf4", 0 ], [ "00000000425c160908c215c4adf998771a2d1c472051bc58320696f3a5eb0644", 0 ], [ "0000000006a5976471986377805d4a148d8822bb7f458138c83f167d197817c9", 0 ], [ "000000000318394ea17038ef369f3cccc79b3d7dfda957af6c8cd4a471ffa814", 0 ], [ "000000000ad4f9d0b8e86871478cc849f7bc42fb108ebec50e4a795afc284926", 0 ], [ "000000000207e63e68f2a7a4c067135883d726fd65e3620142fb9bdf50cce1f6", 0 ], [ "00000000003b426d2c12ee66b2eedb4dcc05d5e158685b222240d31e43687762", 0 ], [ "00000000017cf6ee86e3d483f9a978ded72be1fa5af37d287a71c5dfb87cdd83", 0 ], [ "00000000004b1d9fe16fc0c72cfa0395c98a3e460cd2affb8640e28bca295a4a", 0 ], [ "0000000046d191b09f7726e4f8bfaffed6c30734afbf1f95e6bddbe0b07d9e88", 0 ], [ "0000000082cec8200e9ea055c2991bf74560eb7e7140691ea53e7828dbdc9553", 0 ], [ "000000003775b96d6b362d4804afe2d9c3cf3cbb46a45c3ccc377c94e83edd23", 0 ], [ "00000000037835a92404acb2f18768a49d4f93685ead30aad6bb3b073f411e02", 0 ], [ "0000000006cf75d17706d1f62e6b08e6ba5facfde38a8920b7d808a6b6781ff2", 0 ], [ "0000000003dff257cdae43703fcd0ca91fda0970f5fc04258b4608fb1942a6f6", 0 ], [ "0000000000532d97d18867658e08c789f627535652382147e33bf8626d4131bc", 0 ], [ "000000000266dfb79bb11dedd0ae748505863ab3ab731269cd71a2c2fbd159b3", 0 ], [ "00000000349ff0119d5c0dd8ffad8bf41cd6126a88416148b81fa4dcaebc42e1", 0 ], [ "000000003c61939b4799eeea4335218d30de9b1071605126d719dce0f0d14810", 0 ], [ "000000003d9284570ed648d2b12ad24046ac8b9abcf05c4e9813ea110490cf73", 0 ], [ "0000000001360b66e6dc0ccfbd75356034e721ae55c3d5c71a58be5d281c252b", 0 ], [ "000000000c114f42504916bfb2ee26ed8307b3f7f74226c1cfe1f5302ec23d26", 0 ], [ "0000000007acac3fcf97b4ca81821263b704364adaa2736fce0a0722bfed4f8d", 0 ], [ "00000000059768ef7731d27f9c2be48c6e16d7cb56680625f08ff25ead504280", 0 ], [ "000000000351c8908f1f52518ce4bd251b896ca3fbccb69a2607db6624bafcfc", 0 ], [ "0000000068d7ccae048e212e9e2ecb4d944f583b4490df4fbf654b4915597052", 0 ], [ "000000000e2aaa36417187233ff55325473bd5b7a164b358da60c96d1920fd77", 0 ], [ "000000001eb11ef6dbe0647bc87a8d218f6e59c2b9690f17edcf0dbd39cd0308", 0 ], [ "00000000022e7855e24cc3fff67ce093242434a8ffa45882333a0f08a40aad9c", 0 ], [ "000000000210130ff4e3186258c09a8463c1e196f5c5432b4c7b6954e907bf63", 0 ], [ "0000000000e01372ede322bf88ee5ed8a46dd4fd8df832eca16180263fc8b1ef", 0 ], [ "00000000a0701896e26d5d884834b267512e0af52c92edc4bccf1c5c803d3c4f", 0 ], [ "00000000869fc8d9ac1588f3e5bdfd60253e9824083800b7794010e0e9c6b6fe", 0 ], [ "000000001d43b3165ec30736f28f0761600b092686f861db23ec38f2d92b0ec6", 0 ], [ "000000000ef4092da8c2056e5933de0e1530194c3ad941a9b393fbb26f98862e", 0 ], [ "0000000001e3fed39f70023909f962bea146b03bc8e94e5d19d7da93123f4f64", 0 ], [ "0000000000b4b8c877bbe3cde97649845290bb78999ecff4621b9bf2ab16aa2e", 0 ], [ "00000000006095ba3b4742883a0ec427a3fd685ffb65b987ea77ebfedea7da82", 0 ], [ "000000000168f0a76a6068a34fc042553aff4aa63b906028f28c2a4c327328e1", 0 ], [ "0000000000af10f3079b4989ac4ff0baaecab38220510cdae9672d6922e93919", 0 ], [ "0000000000312791ada0f6a4c5eaf2a1cd57cd06f5970a8ab49923817b862c35", 0 ], [ "000000000055f3d4f45c4d199d9c230cb2cfeb68c8e934cfd061bd616358655a", 0 ], [ "000000000036b6129bb5a786bfdd75cb4b932f7dcae9da469d3ba35096f1e821", 0 ], [ "00000000002fbccf271c13e486673251ecd7951ecc12ee73c4390e0ff09e9b59", 0 ], [ "0000000000314e297a81bf002fc40eb391d8883ea45ee4e782385aa0fdba6452", 0 ], [ "00000000d3c473819ec3b3c268f7b555df22772e407bc8f246a47cfc579ec61f", 0 ], [ "0000000075a438fda6bdb391263d0a2a6e8e68edd9dd8f70fe5734eab9351eb8", 0 ], [ "0000000017ebae0a2bec50008b4a4ea8839798cbd9ff228e76aba087d0ff1736", 0 ], [ "000000000800466ba31c0bbc12b125f16d05ed27788de045e25d6f093817d29c", 0 ], [ "00000000002163c41f2264f202e611aeb9ba6c0a3ee95cd8e5e7e571edc64edf", 0 ], [ "0000000000de9882d417786fce8c755cfaad17f40cda744d4badedfe5e414e31", 0 ], [ "00000000002af352cf41f60a5ebf033bf7e4967c0597cee706ba877b795aefb4", 0 ], [ "0000000000009ca0030f1dd0b09cc628f2d4d278c87b20781a1b136dc395debf", 0 ], [ "00000000ffd27370a76d06a0da0e3805f47e35e2cf584d73d2c5ecaa2e525642", 0 ], [ "00000000720da6910aa75099baa020cb8db37e1dc19cdff66152225b7609c23a", 0 ], [ "000000000a5c2cc704bce5e8527ce91bac7430c659624ecd86e6a1bb9b697962", 0 ], [ "00000000084273545134e9a06483c8fab00c2b0628056bb1967f310c74a971bc", 0 ], [ "0000000002f66f4da52804647b1c3e1f89d17bdb05e9cd4ebbd922007c773f21", 0 ], [ "00000000c46146c9d0a67a354b3f82947e52670a3bded6d8513ab34a68ae18bd", 0 ], [ "000000002f61c429d7dbe7bde75796086efe574998766806138710a2d6001eba", 0 ], [ "0000000001daf3e3e78a57df2c2d2ddd14093d10515925e75c818bec3bbd30c2", 0 ], [ "0000000002e133a7427a9aac6ceca969b27507c14111a45512cdf8f52a436de0", 0 ], [ "0000000000f7c4374d458666740de1d0e8c55229a209ced7c38e38708781487c", 0 ], [ "000000000035bb9ea329ba30b83eeb4ea6f57c2fe703b97f9b879f21e22643e0", 0 ], [ "00000000001220503e0aaee266bca85de09ce97b0091f24972d1ad1c8afe8609", 0 ], [ "000000000010a614c60457f8d2ae2bb826d037f52113252888fadda8ed773c9c", 0 ], [ "00000000585a8b882ecff8aa8434feeac4ef199ca669bd81ed473e37f0bb4528", 0 ], [ "000000009504ffdb5fe82ad88218fb5e75a8bc185247e30e22d23b9fd9b7f282", 0 ], [ "000000000ddec7d73bcd653168d82e34cf5746e006bccda8a9c031c3289b9568", 0 ], [ "000000000cb6620ee4e8cb8b6b4d51251e5961f7ae2e83538ab3a4fef3bcc773", 0 ], [ "000000000239224a0841738513c1eda712b73266ea958aa75f44a3985ebfab82", 0 ], [ "00000000002630c7c3586fcc19079300403c54dc293bcfdf8a9981f85a5c31bc", 0 ], [ "000000000028d8c34f44e51fd71f5401094a983f6566e6d08ce86ec5d1bd639c", 0 ], [ "00000000000dca95f1828adc3c37b4625f60aeb35a6614a4358322b7a6bc2f7d", 0 ], [ "00000000d72ec84fda18959ddc474d1a31a3a13b1d94695136c4810af8c01a0b", 0 ], [ "00000000327c29604996eb7f0a208160969ee4408a1cad277a956334f94e0f35", 0 ], [ "000000000e1bd41d009c1910fcfee7bf1cc1adb04b0b7a632ac36c1092f01bb7", 0 ], [ "000000000201a5afed48b9d095b949229e9882ef8bc96767be3097c87264dfb6", 0 ], [ "00000000003f28e8f3f9c80b1269bb0aa3b57501c12458550ef04fd43aca6a33", 0 ], [ "000000000029e09fc14e38a6a0103c8c67383f41af7d76998055682525f4ca89", 0 ], [ "00000000285ce297602995582ba5d32d583d618a6a92643566e25dd36cf2b7ab", 0 ], [ "00000000657045fa54fac52b8480dc84bd4c418940ba63679f4bd6add6a39962", 0 ], [ "0000000017b7bb58be05a47ff7c4ead27db750813d6bcf3f99cbcc35324cf445", 0 ], [ "00000000003a310e39b6df17f17450496b4f5c1593399bfa1ab8b4d39bac9b25", 0 ], [ "00000000000bfbc5294f003548a9636ebbcea3ba42577821266317676fbc363c", 0 ], [ "000000002329351dd70c24da2eea5ac19f65b6053c4611aa4eb93bcc2783c57e", 0 ], [ "000000004ce02f1005aa6fa4d158c6e4fce95ab053d88ae74881dd080c24e057", 0 ], [ "0000000000fdaaa54cdaade8cfb75245de0747c60c0307ad11be9fe154535565", 0 ], [ "0000000003dc49f7472f960eedb4fb2d1ccc8b0530ca6c75ed2bba9718b6f297", 0 ], [ "00000000014ca604d769d4b99fff03ae3ac84d1e8eb991c5dac7c3cd4d9e68ee", 0 ], [ "0000000000190ab8ecef3a3d5583563851672d81a4d4d952b8cf3bd503c655e5", 0 ], [ "00000000001204d263b607987fab11e1c19c94b7e3e674cc73cc2fb7b05fbf07", 0 ], [ "0000000000141e8d7f7ac359a8ae58e35ce6010c25ddd6f1881f41c0b939332e", 0 ], [ "00000000946344dd06ef5ddd13fb74f20c475daf911ff4e3f1dcdf64c330e274", 0 ], [ "00000000ec77a7892e48b85bcbaf404d16d7fc93747d7e9e3ba6195a9b6f1525", 0 ], [ "0000000018a305c04dea8e93e423ce9569872e0ec5af49d23a0e3872b0ad6297", 0 ], [ "00000000055e32c5f8a86c9a712eeb6440bbf9810ae6da12d0cea2493138a885", 0 ], [ "0000000001913fcbe67badbce4234e86e35a1ea867ecd69814b5f5ab039b7d4b", 0 ], [ "00000000002c71fe4403aee704720ceafd21f9f8c9c97a8bfbd25bb46223aa40", 0 ], [ "0000000000343a42da0c811836d0785c272591facd816f0e7fdcfb1109d8f9a8", 0 ], [ "00000000000309b182608b3eea7fafd0d72e3c79a0a3a9cda03cde3947e332e1", 0 ], [ "00000000000204cc04e421c3958a64d7bc024a474ce792d42ab5b48a5a6f3927", 0 ], [ "000000005eaa010e7255bd37e0b00780575074a74d889e17c4dbc578f917348d", 0 ], [ "00000000a0d425f62d9196c069286dc6635ded9d027de40070d397e45bd63e0e", 0 ], [ "000000003355fd37068ce2d5d2a94ef964eeb9b687f21f4a00850a3e6cc4a71f", 0 ], [ "000000000ca9148dabe9424cd8c96860c90d836ab25970a3e91856764e2e640c", 0 ], [ "0000000000bde23f829dde8edef35436be4b8978da21fd2c3a8100ef5334e3cc", 0 ], [ "000000000028bb26f1427fbfabeae65d55a9e59e18230713e40f0f7c9c2dee12", 0 ], [ "00000000002ac05422d254e597ee6b5e0f8be9b3e2f887486442d720c7766919", 0 ], [ "00000000000e36d0b6f187dd9601b1d1dcd987c3e0f6a081ffd039c7c5e32462", 0 ], [ "0000000000048d7b1f2a2a11fda34a5cfeea067ab03e482931e5a0f463f438ba", 0 ], [ "00000000f780ab88c8a4f4247573a749fbb087a4e3fb6a7d29926de8a9ab3462", 0 ], [ "000000000313bbe6a940e6a8c40ba091aa1ebbaad135bbbff3ed8ae07cf574d2", 0 ], [ "000000001d4ab29721aa2722482562670a0d71dc1eb73231c5dafb64756b04e8", 0 ], [ "0000000006588bcbdec38d19962b96cf0352cbf1b90f3379cc6787d018cdb96d", 0 ], [ "000000000022e79539a21ac24f9daa2cbddf2bb4a3125f88a5efc20d13ea856b", 0 ], [ "0000000000dd284b7fee584cc578a10fbe57e8efe6bf6ebacb23c0ac5d46cdf7", 0 ], [ "00000000001451143787f411c93d5506065c3fb597966f2fd7a4a5c078ee6aa2", 0 ], [ "00000000000ca977394af1e414dc1f9d83efa007f7226e11d3a00f59a1fdfad1", 0 ], [ "0000000000011f8caa80580e7a796bbce5b84e60731bf48e03c6ff5c6bba868e", 0 ], [ "000000000001705beb1376af1af08b437acef6befbe7d3b60c5fbaf6bb7f38c9", 0 ], [ "000000000000c838f1f45422d93ca9b5838368a37423efa8439ee24b2bf247a2", 0 ], [ "00000000000111ad857d31d07fdc8b32d17af2522c18bdaccfef449b29d17362", 0 ], [ "000000000000312a7718fc616b0ecfdbf6066f71ec1a4a8c43f50f02f61cc398", 0 ], [ "0000000000007d232b217a59b804ef67091c5720a5460c2c16bf97b97a24801e", 0 ], [ "000000000000177235c33695aced585685b4c500eb76e72caad02e17503900eb", 0 ], [ "00000000000037f5c5890da7a8e2acd2b0669ad7db648ac43140c637a1c81637", 0 ], [ "0000000000002123904063f223bc35135c426a4f9a0b74c1907e837b810f0321", 0 ], [ "0000000000000961db809da357d91a9341170fafef9f24896d8730bd05cf3f96", 0 ], [ "000000000d2e8fcd05eb874e98cfc3a6e239f6974950e6f50b0487513ecab760", 0 ], [ "00000000017e362508c8db23fae0431eaed708d9db13e48fd5d318066bf6733f", 0 ], [ "000000000011b2bc4fe36f90b7ba5a62f974db250bfdc285b70c71148023c7e3", 0 ], [ "000000000001be28570b378dd5dd2eb3aa495c229913b6757fe8900dfa3cce99", 0 ], [ "0000000000242bd0bb16d0a5324e0b4b5a83697dabb3b4a059084557478e50b9", 0 ], [ "0000000000d8ce69d18da32ed52e503d6b5ad48d970b90545f956b2d2af2edf6", 0 ], [ "0000000000366655bf0cb3dd0cd7801e0adbd26b5b441b77a9e3642597effb00", 0 ], [ "00000000000dc7aa00d4607ca8374d40d1187f1c084b620edb45fc39bc8d2db8", 0 ], [ "000000000003baf60d9c6e70a765cf517f66a124509191188e9547ad09edf68b", 0 ], [ "000000000000e0f476893b8fb4d37e855353075fde73dbc1fe181cc956349f19", 0 ], [ "00000000000032ed16b7de758abadf4a4fb2df7a101ff275c51f29e1555a89a5", 0 ], [ "0000000000000a564d03f0f2fe20f6fb5f038d931f732d817641cd7fff3b0acd", 0 ], [ "000000000000011aa4d0fdcea8d4ca85cd5d548e322e2b6abd17f8444be855c5", 0 ], [ "0000000000000610588540267a0eb544531047d4c8af0f21fca7cd3d96205cfc", 0 ], [ "00000000000002770dab5e14843149df8f76b8dc8458ed3ed2ed8a14a6e2e564", 0 ], [ "00000000000006b70ebc9f75bd32f466602cbd4b86c3c2d2379059542bb8bec6", 0 ], [ "00000000000000ef579af389fa7674f98a2371063fa8b218c5ca0ad94e21b896", 0 ], [ "000000000000021b6108dc988f9153383f9501ab9001109aa87902ddd4c8a4d1", 0 ], [ "000000000000022c02ff22bc0af5201f0e1a14a75879c494731e4fbf999218c8", 0 ], [ "000000000000032651c988edc1ccd08e82b888cbb8135e24a958ac0c0b640d5d", 0 ], [ "000000000000015aefdfa0790bed326c38c358c07aac0674f5b2e771258b8df3", 0 ], [ "00000000000000822e1534c86afef911b67d3fa20cf2b12d93d20d64005f54d7", 0 ], [ "00000000000000338b871276768c923b1c603fd6150bd054c2287e532e61de7f", 0 ], [ "00000000000002d0af52c0cae894bf836b61137ace2bd7500abd13a584c02741", 0 ], [ "000000006f8443a458f38d8731821c07a2fda0ecdbb1cf797f541844d468ce0c", 0 ], [ "0000000000b6fbd8b4e227f5514979a61d8b0b918d2adc154e585ca926386704", 0 ], [ "000000000f4f5e49b10278e27d9dee15b92f9d4a257138a206831e0c00188767", 0 ], [ "0000000002c7e9769bd8ae9906fc5682e937b5c31ab5b5b86e4d70af2c15a95c", 0 ], [ "0000000000f68a1db8cd387e0a2f93f45149fe1ee4a230bb386313bdd42058e8", 0 ], [ "0000000000f0f65c360c8f0f9853ad1142f16675dc1175d61afdbef977776b25", 0 ], [ "000000000004f734e634156511cbef7dfefebdf317e7488aa6c2562572d7ecb7", 0 ], [ "0000000000002a46a7a16787e8317dc567ae26816324c2035be0186ba54d5cb8", 0 ], [ "000000000001a593e6f01875b77e270163538d88452779bb557df7c2607c28e0", 0 ], [ "0000000000004f24cfafa10bd50a452535f64be577a6161e51c7c71542f654c4", 0 ], [ "00000000597cce73e84b63f08cfcb9b01f5e7621752d8c8e08fabbd6ab5c0dd5", 0 ], [ "000000007cad379df01247771fff471bc99faea1b86218602f45ab13efc5e9f6", 0 ], [ "000000000d6085aab25892be49c49d6c0a3949befdc3ddce2faa46b104e1e804", 0 ], [ "0000000002be5996786b42d6a229093896aea9966b1854ea261e01e84da1f420", 0 ], [ "00000000002684b72056e270b115d80b12b2f68eac7412355287226aecd9b5e0", 0 ], [ "0000000079ea27efb24366c87856a9e371c56fcbd59d09d3164a5c2fc15fcbca", 0 ], [ "000000001694120525dba4548ca54087544da1fbefa51c38f0208d683418825d", 0 ], [ "000000000693e80d372938f3553151ab9d0a9a6922182591c701df739dc9a502", 0 ], [ "0000000002950d9cb23c8511937811910b712f73d448e6fdc2e39e029b86848b", 0 ], [ "000000000091c40056c6a48f33db17764af89c01f62ae653aa5e494146164cee", 0 ], [ "00000000001f373c47e1a39af4e1ebcd8c88411ec49d6bd520c2781564070971", 0 ], [ "00000000000809ca4b2170c57958709b867095b1972d80a2ee55359fbd0940fe", 0 ], [ "0000000000038e7bd66fc3308447b1370dbdd0661c427c512bdbc641ff360fb2", 0 ], [ "000000009a3325df76e2de1fc1970cc2f241fa8a41da9ad745a0d9666d9ff51d", 0 ], [ "000000003176e92ff837bf43a48a995c1a321b166475f586ffb4b962e0254a4a", 0 ], [ "0000000001ae3292e81ca3859b75bccd5bff825cd9f496efd085160c716ed05e", 0 ], [ "00000000033bdac4f0d36bb912fba28bb5caa54d1b611759a10f79ff3c969cf2", 0 ], [ "00000000004c6db7fa0e2c9f08693abfeb128c5827b511a5c46c623a103b416b", 0 ], [ "00000000003d87f48bb95e9431760d0c5f4f93c77d02fce9dd1673e9f5b01029", 0 ], [ "00000000000e214fc3d8b97571eb75d248ca29f8e25a584c33de8488ceee72b0", 0 ], [ "00000000000133269b7159b828700d02de770a8cbd91f3d166e6bbc95d8e0dfc", 0 ], [ "000000000000cc92e2dd933a08f7fd87f84451627982fb66583587858217c059", 0 ], [ "00000000000030708136c20c4c8216314005b3cb5c551ded33b26cf64d2ff47d", 0 ], [ "00000000c472a1341d479ed02f31b699e448c035049a7092670b38f4ec6121f0", 0 ], [ "000000000a358834d6eed41b9b7161a338aba53828111414cdea7552ed15548a", 0 ], [ "000000000e13e77372daea775c8358916e57ed11835899c14e5140ed9be11089", 0 ], [ "00000000008252cd0931f94b2465bd4f93e4bfeec6697962c5b034cf3d12cf7c", 0 ], [ "00000000019812cd6cde3a43831234be71e68118be24a80161349b8b327acb5b", 0 ], [ "00000000005865499f301adfb59f8380743e4c3b3ab220ca4eb97dc6628df626", 0 ], [ "000000000015f77e1e61329560a4378eb401fa5bf0ef90b0a014a4d7857ca7a8", 0 ], [ "00000000e9cbcbb625e8a463ba8e7f14be46ba9538ffe93338784ccad3d992e8", 0 ], [ "000000000fb27169efcc2873cfaac223ebb91cc5e1e5ad7e9a312d42bedf7c42", 0 ], [ "000000000c9c96d62ebfbf3fa4003f1d46d175140ab084dee17e8125fa40f24a", 0 ], [ "000000000311e3a766b1ab2064b68a344a561eb496d595126808ffb166c71cc1", 0 ], [ "00000000677568c82262ac3a4ca3f909bdfb0b35145ad490fa3fbdc719d06b91", 0 ], [ "000000000ee77ba9ab657e51fd9140f5c9b46731d9341e98188f929c97d04746", 0 ], [ "0000000008a67eb9c91a6d74168f3f385270fa942ea00bdd31924d1b6ea11148", 0 ], [ "00000000017f93c9e0026e90d579e18c83b4a8557f0c00e9b85ab164cf4466c5", 0 ], [ "0000000000994efa379235c03711a8e6b29895d928b5fde96cb01c02374c0602", 0 ], [ "00000000b3be9f23c943d71d7c7dbdf6dd672d77a712f6c83e9796a85e4379f2", 0 ], [ "000000000713e1089b0b2bdcba462b740c9396f822f1c73e090713978a7f1314", 0 ], [ "0000000002fc44d358401a7ac9ce4ddcb17f3cbac08e40242e755e60ab2292ed", 0 ], [ "00000000021ef2c04fd30be7049f73b9a8353ac96a467dd5f0b9c1457be1bc5e", 0 ], [ "000000000023b95b440ccbbdcb914172cf675cd15d6111bd7f5a436a4925d36e", 0 ], [ "00000000001983521dbffd1b742a6d4b5dfda3f46579fbbdd83a2ebf9a039bec", 0 ], [ "0000000000044d53dbea312432e68fa90dc2148946f613216dbdeec86f6a67c1", 0 ], [ "00000000000107667692f12d21a55a72ff1dce828f96872e36c35bfbae475a8d", 0 ], [ "000000000000252d1d0c01744ec25af801ef7c57e2581c95295070b6a8a85bd5", 0 ], [ "000000001c1da54e16dc06158677024d9e74bff39bfaec83434ac33673fcc251", 0 ], [ "00000000b4d0c6ae86bfdf7ba4c205fc3e6b3b6d63836b85e30e9d8bac922301", 0 ], [ "000000002b16179cb022bf678bd847dd6fc1908d0df04abf0c7874981eb33ee7", 0 ], [ "000000000e6783554aae41856424d184dc4fa061f40676efd107e6f933a25641", 0 ], [ "00000000005ae4acbab519895b4b523d97a09e381c9e4b044e642f73b8c0f1b0", 0 ], [ "000000000010372b59c9595d947064804b75ab21868dd075a3842ab7d2df6181", 0 ], [ "00000000002f9f587ea304093be049d3142ac0c92f9c68928a4f82d12b929b69", 0 ], [ "000000000005d4cae51b3c76dc3c61bed0c265c4f228c0c4d1d3d147146c34eb", 0 ], [ "000000000001a5b6c0e0a0b485a490cb52ccdf9b22596656039b51545bb07be5", 0 ], [ "000000000000d723d0976338edf55d08edab995dd6283cbb688855f0dca6e8f5", 0 ], [ "00000000bfebfae90208a82c7fa06c0f61674dbf1e4f9162e370656c38d611bb", 0 ], [ "000000000c91cd144b2a92ab5024c87f70cc1d76a4a7f26a82a98c5aaad62850", 0 ], [ "00000000077c8114eb5cfb69c3924c699d0c70334360dd1daa95db0db4816953", 0 ], [ "000000000348a6443e091db8f68e88a10afad7c6e3e5392247902c4b4feade43", 0 ], [ "0000000000d63b70351e05829ad8a56336521b361b0d50eb7ea1f5b46c25b00a", 0 ], [ "00000000004658603163f0ede572120a1bbfce8d313aa282ae54d2ffd9fe9079", 0 ], [ "0000000000048063b410c793db34856f23acfb19a0ce72f5997fa572773378c8", 0 ], [ "00000000000228fb6e587fa593ff8b4764064bba8bfc2f43ba5b1f12af33d04a", 0 ], [ "00000000000082e3ddb75c0ea2a98922b1556ce10346f9bb0cedd97ccb3fdf62", 0 ], [ "00000000000005571b54d4886b44b81c21dfbefa554cd7c23430e5aeff6b5ae2", 0 ], [ "00000000306a603ca1a0d961e08e103a9f13f3615163c3373d1bd2a67cadc2a7", 0 ], [ "00000000195d93ba7ae19832b622de86ebdadf3c78f1751ef2b2e9b0e3a530d8", 0 ], [ "0000000000476d0d00cbc68bb20b4893f0e608b02a1e029b8c6c73e169c49e69", 0 ], [ "000000000051348044bc10fc05960c244c3ccd3b3b6c145ffd9958a1c8bc0215", 0 ], [ "0000000001e4df369203badca9aedc28c240d592b12d284ce0b0463fc7537c09", 0 ], [ "000000000091cc1ccd448b0ec9185618a84dea96f52477cfb9b9ca2b60cebe83", 0 ], [ "000000000024a50299c0ef0c6dec9c64336b6cf5c1a1b0013e22fd4fcee1d7d1", 0 ], [ "00000000000349248c1df06c3783d1270cd97ce7f605b9036fca0fdc2f0fbb96", 0 ], [ "000000000001afe6793e7427a3d780876d26eb7f2ded92563f991bf7302aea69", 0 ], [ "0000000000007148006e139e24d9fccc307661c9a0cbcd1af983487c2f0780c9", 0 ], [ "0000000000002734722a341984738177a3f6f264291424e4984f2128d921bf29", 0 ], [ "000000000109b02caaa95e49a477757a41a42daed40e92f54fa09e63f5538cd2", 0 ], [ "000000009a11c7ff8b8fa7fbff5a04c25906f701ab5bd67195736f9ccc839ab9", 0 ], [ "000000002b1d77f8e0cd60af1c62ef6d381e8905665b15a7fbc546d0c1a45e18", 0 ], [ "0000000002588cb017de9e2f23cea7edc5082f1b3faec890f9252d556efeac40", 0 ], [ "00000000008b07f177adc24a4b1a64d2dbcfbcc903ba861d493e11d6b33af7dc", 0 ], [ "0000000000bab8db5020aa8e052165275e8eb3e7c843533246bf6e4c8374757e", 0 ], [ "0000000000138488fdca8bfc327e6dbd6c72c5f1dc5868d9c0ea886665b9b56b", 0 ], [ "0000000000094021fc954efbf08be667fef1b817e8715d4093a561fc30264aa7", 0 ], [ "000000000000e8183e64072db79adfc6c09b650c4178001be3fade4050b06005", 0 ], [ "0000000000004c93e8661c75974cd191c68dd66999da4f70d039c0ba4a12b970", 0 ], [ "00000000000021c675b3ec404bb996f5e68f9eeceeac6946e5a6822987824d33", 0 ], [ "0000000000000ad85684d30f25d1ec34638f099df2f33b418a07307c68fe3c2d", 0 ], [ "000000000009c6add76ac42a1942c4ce74d25d1b8975d4e3ac8932185e785a44", 0 ], [ "000000001e7d828d354716881683eb6fb5caec5d91afce298e4e3bcee9574924", 0 ], [ "000000000a0e438ab203d8fd3e56100f2f14759f704bff6c699df0bb4e9aad64", 0 ], [ "000000000b7d5c2895df8bc1fdf5d31e0f663564cb5cff3b18642c44a71b6248", 0 ], [ "000000000193209ecd92fce00a75975446423d94a325ed525c15d5ab921da273", 0 ], [ "000000000020835bdc30ac67efdbc785d15186914bc14e86387f97450df46418", 0 ], [ "00000000000c9078321f0030214c75e170b01ec664d39bab1b1e48460a54eb63", 0 ], [ "00000000000ac68b63d486ade190dc9108eb3730d25e7537649fe21c30e0121f", 0 ], [ "000000000002a94dfc5f4b677b251a7a7647dbb99c0803df8658222227fe3e3f", 0 ], [ "000000000000b076bbef0e50593b1595ffb3d571e7ad95dbdf06dca8824ef7f3", 0 ], [ "000000000000167075c8bcd24233d25cd268271c0e8fcb6f301ee1b6f6ff0341", 0 ], [ "00000000013107aa587bcf12ac445330ff0325d73c5253f7e6a49ed8c50257bb", 0 ], [ "00000000090ff53d49c9ffd51511af8d5cba2038a8e25e3b17186b1bc941f43d", 0 ], [ "000000000d9e704d5607f77f8983cc56069571a3761d5bd5da55f05ec5d8e844", 0 ], [ "0000000002b2b4c0950fb6390f0ae860840e84eb0a82e5e8a9bc37c14bbf43b0", 0 ], [ "0000000000be10137a2434dce1d97850b768ce878c1c80ec905f6e9f21e65fa7", 0 ], [ "00000000005cd966f80183d4c048e63a5c14f649298dfd261d989d9e3c026bf4", 0 ], [ "00000000000e8f30e55006a4082380c4b1a372b7ad919d3a9b0a52fe5ee881d3", 0 ], [ "0000000000018c70a4c27bdba237ad19ebae5d3ca23f1394ccc746d73669a1c4", 0 ], [ "0000000000022acc8432c883953227786f7a6560aeaf0176d232c8affa5b25b4", 0 ], [ "0000000000001854e95b28b4efcb2cfeb08c76d8cf1fb03f2055b3fb758f3a1c", 0 ], [ "000000000000187080c2c39f5a3ea8be72ac4d3ec0d16b21cd34f1541bef23be", 0 ], [ "0000000000001593766a3c63b524f658ec7690df467cc7bbcebbdb56385500d4", 0 ], [ "00000000000012d6966dc51a41f2c617192169ec8418405e164ba83b9f7ecdfe", 0 ], [ "0000000000001d0c7d0a2605e127b00448b71e756ad96625116ab8ca18f74900", 0 ], [ "000000000009cb439ea49282d257595ad1f7602856c16cc26fff423f7783c792", 0 ], [ "0000000000889282b98336c994d7420a639221e0484b511227fd616d78dbd028", 0 ], [ "000000000071a4a2ad6767864bd21239c74c9912a40ca9fd3b209e21b66460d9", 0 ], [ "0000000000f3ed2c3c9a7c3a7291e859cecba8cf9243d23a4892e6be8ea9b70f", 0 ], [ "00000000006a4258ffdff8b7f6f4f685ce18c6eb1d7a1cf501ca9e02fcb7620a", 0 ], [ "00000000004af78f1a109d1267a9c24d69c6a4b30fea49f0efa6c8834cf394f9", 0 ], [ "0000000000193bf3efbb145747198470a81b2cd33c991057676742d5c22a64b2", 0 ], [ "000000000006b436798c7e4a8c3bdbf054a66707feee5a18ce9ca57eb95bb48a", 0 ], [ "0000000000001db50c7caa3a02ea4f173343f958f334a8bf3f8638add9e69b34", 0 ], [ "0000000000003c621629cc0bcec5968d61d2e42c6673de4d46555118ad5001d8", 0 ], [ "0000000000001262bef2918265f6dd4534013a4650444054fb4f5e490c5ed57b", 0 ], [ "0000000000000120ceee972d70cc84430006645997c7337976c673bd75cbef2b", 0 ], [ "00000000ba16134dc0c418a116b97ad5deccd6bf6e3daa028a8a6a80d7823faf", 0 ], [ "00000000a1a00d6d6fe0660e63402a5a7c7248589211594d37fd800456ce84b6", 0 ], [ "00000000394766cec78f962c29aaa715b66e3ad34e1f2323dba45e087cb3b395", 0 ], [ "0000000008b15a3020676f5e084210ecc05f646885eca1cf6a10e9ae9e3995cc", 0 ], [ "0000000002cf7eb98abe784f6e516670a88b9028a6faabfd099a364c2dc5c42b", 0 ], [ "000000000054015fec337a9ee43eea501d2292f031f5bc1f09758d20f5cd3135", 0 ], [ "0000000000068d24d31a9f1192d848155a2f90939627bc456c9a337135a923fa", 0 ], [ "000000000006262bd09358258edcc455f9ba46b7f9d6e69d0f6b9da89488a4a5", 0 ], [ "000000000002327bf77ae67961463ea98a78dab06c24ac7d58b1727c5f856626", 0 ], [ "0000000000006672235c1606fbacd7861b16b267d203b4d687708eeb1fc25e6d", 0 ], [ "000000000000ac0c9a39a47313a8715f125c46d6ea8be8741b99b1db4a8aae47", 0 ], [ "0000000000007e93f6578e7856aae0ecf6341e1312664d9e1d812ff254c37ae6", 0 ], [ "0000000000002a980acdb1443926875e7d4a57859b2b45ce3fa92c7716319f62", 0 ], [ "0000000000683bfd82c63514bc58a80daf699a6bcd040bb2a499540baf52463d", 0 ], [ "00000000373e6262928d7a6cac965b294aef35f90b72c85100ef91501775e06a", 0 ], [ "0000000000f7bc44061b65c62d4d7747138df127dd2a30f583c3ebb66a25c7a4", 0 ], [ "000000000212a71c38d0e13ab7c5646c949d4b7ca23afedbe351a43b7607043b", 0 ], [ "0000000000a836e88f76ee5dcca1e884572f32f4460a3b024280738d76e98ced", 0 ], [ "0000000000413f6c1b1c9841961636bb3290f2410ba0731f3522c4ff3faa2e0e", 0 ], [ "0000000000082336107412226110ab2a53016d4faad4deec048828507a300248", 0 ], [ "000000000000a91e7a3f35a23f01621dd051e314da617714991467131808d3bf", 0 ], [ "000000000000cd6576950f6f238227c3ba7f62405ed1bf3af4878c6dc1b04635", 0 ], [ "0000000000674099e9741e44da03e9531402a2607a19a65660b57470340828db", 0 ], [ "0000000030c4744001ae85f9e6b46ed0664449927b86b8fbf25b22b851d23671", 0 ], [ "00000000002f5095ad1a12eb9eedf88ce1e7268368461b6b4e10051148f436cb", 0 ], [ "000000000057d3e2a77eadb8b9613cb839ab02a96094dd5d0a6d1f09026c3936", 0 ], [ "00000000004e0a28be887d6ed037cd9102cbbda7d6c9e584ba51f2c2dce96232", 0 ], [ "0000000000211346d8099f7ecea72481c4cd45591f5e0d7e347725ac2162f142", 0 ], [ "0000000000199ae9fc06c5acee766db6033b86f76c266cadefe1461c611c2198", 0 ], [ "00000000004c9e5748558d4f5a75bc824171e3b958152dfd6844330f1e907f8c", 0 ], [ "0000000000137addf1521361dad1ee007eb9e6dd4eb8441492ebfaa3c240d556", 0 ], [ "000000000054d4c77bb7964e5327c35760d87b890ea336aec5ecdeb783350738", 0 ], [ "00000000006b7b06d04818e97a4df66164b471912f88d9cd02de4af6c8bbe74f", 0 ], [ "0000000000380fa9858e3e90335c061a3776a26bee1e8b6851de33ec63670782", 0 ], [ "00000000000842598b03fb79ce7386e9f9181a02dcf1effc8f70d3ff7368ccd5", 0 ], [ "000000000003d3475edecd733fc7b82432882d9c9f1350a98ef8921b87db4dec", 0 ], [ "00000000000000e330a8d57a38dbcc0b0a5dc7a4210f231b8082b9be5f9e4bce", 0 ], [ "000000000000218ff87fd50cfba2fd04203a78d2600cb2c4dcb039d803426e19", 0 ], [ "00000000007c96e6e3ed3146260348ac79ea7dc2ec2ae6bf8dc203400a37721d", 0 ], [ "000000005abaa10bf7260470c28ba32f1755b4cfd3734aad580681e39a9605a5", 0 ], [ "00000000005e77c226e6fffccafa56055e68f0ea0a30101e6a243ab9b3e07db0", 0 ], [ "0000000000e989fe27f85b89c1e852d7bc94b09033cc6c8b32fbbbd9383a9ae1", 0 ], [ "000000000091a1e962438583146293ef34156962445ffc5e81e4d0fe327d37ac", 0 ], [ "0000000000477978a6903217e2817d10e99bdfedb4f8bc396b96fd5b0b93b522", 0 ], [ "00000000000bfd9e5f13a9c03c48e8b58a937cf1ae2849160f1ca11f8fcced3c", 0 ], [ "00000000000158dd3c31b6379887b4353ef2898c03b7ce55458fcd57cb6f0639", 0 ], [ "00000000000029d7009eb56b9d38366005576b82a9b59fc845522a34ad36a38a", 0 ], [ "0000000000e6e207a82b8ad7136352204bb8e9ccfcd25885a715d3c65cbee997", 0 ], [ "0000000000fadc4429f50fc534ccac4db5e51a313df25034d6c5c25f7e83448c", 0 ], [ "000000000019c58defcfdab6c6ab9497685e61118effda4c2613bf44be19fcbd", 0 ], [ "000000000006cf444d846093c5045d42ddc0986ca805f261476d0fd2eb474c39", 0 ], [ "0000000000d0856a3d6a1e5b1ac7e388cc029bd8410b3b1489598974fe470568", 0 ], [ "00000000003d9aae63ed532b78082ca5386211e22410fd24ebd5318d1a4cd1da", 0 ], [ "00000000000345003879f86021a6d5e3fe93813246818c145947b7e225691177", 0 ], [ "00000000000175393730cde3e49de7af2b81ae736eee005a9f9c4a1e878c52ec", 0 ], [ "00000000000087a8c621c879aec2a897258632d6aa631b9a38ba4d564e08682a", 0 ], [ "0000000000002ea641b2975935bd9caf337b51ac9f9bb90a54f6ea6ee5d3112b", 0 ], [ "0000000000000c544f9b6a8cbab6d25caf949875622bf75139234850b10affe1", 0 ], [ "0000000000000f66fc4e37232a29f3389c493863a980d58a1d570eddd5268999", 0 ], [ "00000000001213fe2bbb8aacb1fc14983586e09db964151cb507956a81b35f25", 0 ], [ "0000000000ba82c2160602ddc1913bc4c133ad0af8848e014367c84110d00e05", 0 ], [ "0000000000b7a98b364b1cf9521275a915c7a1b3a0f0c052c7d8efb620ec0870", 0 ], [ "000000000047dc62db23540ab4aee43e54812aedb623a2a158aa3244fc784722", 0 ], [ "00000000005291002da10e53c3855882251a6e5a425b5e639ef9be3bd05767ca", 0 ], [ "00000000005ffbcbc0d9b380584bdc78050a6f0c3582b4c9c5103a150cbc71f5", 0 ], [ "00000000000a7a69cc06b0a68b27a8fa5d29727ec3b6db8d32d61cf7489b5ff3", 0 ], [ "000000000007212eb8c49758d98cefaa6098da2b877a6055be341f5f7c0ad301", 0 ], [ "000000000068d1099d8cf3f43f6d164f2925b1d52ede75640cc65ca020e1de1c", 0 ], [ "0000000008d5ddef4468a4414bd08184c2eba0ec536b85a743b1091828a6a884", 0 ], [ "000000000acae40db93b589783b0cde70b98552955cb3c12f08de1b417d9008d", 0 ], [ "000000000066a51eaa3a54036f338719da3d5779180c0bc3787b533410de90e5", 0 ], [ "00000000008b521677a6e897950aac69640e52efb01b7af10bba3820ecd09a89", 0 ], [ "00000000001823f0e399311cab0fcf57403e094feebf99b22030bafd2004da87", 0 ], [ "00000000000bf821c2abf5bcd00ca96439ddf5b0b593be5601145fda5338efdc", 0 ], [ "000000000003f4fd19b2af0141289177014ecc6dce6ea8fb50bab93d4a291095", 0 ], [ "00000000000011842d892a02e55ca594caddc9f3cea1979ddffefc070eda8498", 0 ], [ "000000000000208aa0259d20f51c0e7b8895e18a93aea79af9b3832e710ef134", 0 ], [ "00000000000007218f849e72dee1f7fb6fcf36f3b6745c6468187ed2ed13287f", 0 ], [ "00000000000f79f656cae641c2b74554c6ecd673c0c7550671c4c2af940661b3", 0 ], [ "0000000000199b4d178c05fd1c3154c9a4632eadc7bfc734c4522176c977ce8a", 0 ], [ "00000000085d0682d481635cb2e6de2e4d9884589455a86194f0b222f9acb3c6", 0 ], [ "00000000015972a5a6786a14b009bf582c4bbf7b9854591dd8d26f82b43ddaef", 0 ], [ "000000000064bf72b7bdbfcbe96dbbd0efcaf7aa94c0f92cb4e6662819468fe4", 0 ], [ "00000000003df36b7962bb4ad62266c462382eddc93f4bfeac464b95f7a89ee9", 0 ], [ "000000000006516d3a9f424eb61db5dfb85aeee29708b78c65d24827bd926263", 0 ], [ "000000000001c1709fe1b294712638db356e89155650f6fbecde79ec47a92af7", 0 ], [ "000000000000dfc23251344b593c16c28cd195abcb337519d7bc82175721a033", 0 ], [ "0000000000000aae2dd2bf0b8581d137fcfa3d9c4cadbe3ef3834d7cae4268c0", 0 ], [ "000000000000092a5baff3d9a5ae87689b2afe668e71bac3b342c7d383f0060f", 0 ], [ "00000000000fa906eeff7d2e126698d88b8cda01d32ea2c039c26984daaa17a3", 0 ], [ "00000000002d4315e5bdc2bcfdb245b914130764a50943a2b2e02ea3acf5c47b", 0 ], [ "0000000000fc2bc9bb83e04cbe922d64719295bfef6320027725402306bcf1a0", 0 ], [ "000000000142690e7c334b97612746d6db208e6153bdfa8479d86d1b575feacd", 0 ], [ "0000000000629a7820e8cdbbed18dcfe16c992152badc745ca73b9b34e53fb0d", 0 ], [ "000000000023c2e9dbf3fe03248e40f4ec3fb2dc81ac573d5a6a4f490c701877", 0 ], [ "000000000013658a43b6d1c4be95fa36e32d3edf80716de3a8f7e98858016adb", 0 ], [ "000000000007c847295d8c4b6da9d8a64b57c3a2307e64387bf8882b9d35d6de", 0 ], [ "0000000000032bf90b823332af80bd2ea18f411f081c7dca8f2fe79d9215526b", 0 ], [ "000000000000001bc0655da6f24c6952e811006897a0c6dd8b6bd94f178636c8", 0 ], [ "0000000000001e1d09b15393190cf686e25488db7fcbc2f1ebacc8165fe6e3a0", 0 ], [ "00000000000cc79ae066badb4157def4067057cefd705bf87f1d832845a7ab36", 0 ], [ "000000000014408398244b94b4eff6b54875802ede6df2d1d21915333a195719", 0 ], [ "0000000000114135a1bc757110c05162fa649b694db9569be117e34832c87257", 0 ], [ "00000000009b15fb2bcee1af904989ba0761e4cddc6b3ee214c0bb07dac6211f", 0 ], [ "000000000012be506dde2c54adf355bdb41a457b0abec436202a3be73f0b052c", 0 ], [ "00000000000963760ceb5fc65570650d494805e05c9d753f3ea6d44247ad3d08", 0 ], [ "00000000000bfec54977673f68b6fe5f088398e697d778fa7987f8bab6a70825", 0 ], [ "000000000000e7f428bb413c17032c0031af0d26133ba93f744a5a0c16cf7e1a", 0 ], [ "00000000000036bc80378323c6eaff8ab350b6d89955f602960cb7c93d2feb4c", 0 ], [ "00000000000f0d5edcaeba823db17f366be49a80d91d15b77747c2e017b8c20a", 0 ], [ "00000000001ff8fd57798082ab5a7452ada211e1c3be38745155505601498829", 0 ], [ "000000000020f960b535eac585e5810ad64f158c1142f0eecd925c8058172933", 0 ], [ "0000000000067bd89409368d221507a160e5c45972eeb01efe210054fe8e7d85", 0 ], [ "00000000003521f2d5ea3232d4835ca6c6bae083ba90458f67d4cd765ce93b09", 0 ], [ "000000000005ab3ff3a0c484eff7b571fb78ce27d93f77a480074232e5ce0c1d", 0 ], [ "00000000001048c9eca7cc1cbb86946c04498052071f7e7c775bba565ada337c", 0 ], [ "00000000000154caacde41be616f924d7d478812148242fba85605eefec9ac61", 0 ], [ "000000000000c34f75bd6f338c0206a31a8d5021cc2ded51e88a6ef4fe686d10", 0 ], [ "0000000000001e0581d86c49a6ca14ba88639ef908abb09210b57989e06b1a1f", 0 ], [ "0000000000d0e6dc0bf830b50bde3e400e16ec4f772f92a55390e62d4aa73af3", 0 ], [ "00000000069c2501a2f32cc69af72a602ff674438ae04dd05516f72a71b9ab26", 0 ], [ "0000000000c926b38954550c9b8d363ff058c2eb135eebdb3e640cfa67df803d", 0 ], [ "000000000011e9ad9c18e9e2095c3662af5be1e918dff653758583aa45dc8197", 0 ], [ "0000000000f311624ff4dcdf07400d0d2fec8b16b14c1c16babc377a2d85ad21", 0 ], [ "00000000002e455cabfdc2a8955e8ddfe717b12efe5b80937b0c0ad6ac977fc5", 0 ], [ "00000000000fed8889a22339b340f599ac7908e790bfc3cfca9b78078a52d228", 0 ], [ "0000000000012ca4492956b3f859b00e5db14b54d422cd95c68c7150743db365", 0 ], [ "0000000000004c58e8f7bac59eb4a036764a4d8e0da51c0290858ab14fb72481", 0 ], [ "0000000000002f60bc99563ff5b4b800c176fe8bde95e8f968fd6b53d74c9cef", 0 ], [ "0000000000000bffd10a3fb0b5b86d8b2561f39d07f8a4c41dfa08e3e49b7db5", 0 ], [ "00000000000006a296be9cd8fd4e3145c146863adbe08b71831abb8a869d032c", 0 ], [ "0000000000000c557f496e82891039ff22e277bd604be6e2e8b95e519bee91f9", 0 ], [ "0000000000399b30d2111c4bf3051c1f7f2f35bba7ff290d92393341ae47df55", 0 ], [ "000000001f88733439e4e8d3c474504aed62037faa16f3845b4c671f69732e26", 0 ], [ "0000000018aa2f93d2ab76a7e2f1bf5b565b4a1b0ececb6ee46490984f6c0d4b", 0 ], [ "0000000005e22674fcf65ce7be896a0557205ab26d1f76d73a717f5f14a6d6ad", 0 ], [ "0000000000223d866b324c097973210f8fc715c9535908359d61d8e1ab2f0100", 0 ], [ "00000000002b321fd6452ab43849bd7a781953ec4485554e0fdc579f2a52c90a", 0 ], [ "0000000000173132748c51b5754b0341232325bd118455bf3c8d25164d3eb92a", 0 ], [ "00000000000143158cdea5fbb9453bbe1a7a900e6feba1e2193e4f5c106d9fba", 0 ], [ "0000000000014677751456af5630025b3d9921a4eafb4d36a06498f0c6a84c56", 0 ], [ "000000000000243976cf2d30ecd3cb1fd0b805fba4da92d2758f78e1c6f8ae92", 0 ], [ "0000000000001323db1ab3f247bcb1e92592004b43e4bed0966ed09f675cf269", 0 ], [ "000000000000017a410c22c4b6caf710f5ccf005d644caf276ea8626a538798d", 0 ], [ "0000000000170b2b1374e3a0dfdce2fbc5e302e1e0e9fb419dc057c9959902d1", 0 ], [ "000000000015b4fad4d929630487680cda2d3aada138c58cc08241ef6dd4ab09", 0 ], [ "00000000000abebab869f1620843d413a3d9e06dc7d9f5201a414d547ace1f99", 0 ], [ "00000000000b0bdaf05c2fe8b12ebd2372f49d8eabcfbccdadd68b5e5b7c9565", 0 ], [ "00000000000ca1af42ee1be2c8895d94f39dab5fcdbe0b4b4065f4be534e7294", 0 ], [ "000000000069d0cc8c0452bf86cff87db05232f801a162acab2d080d6e4e9ea9", 0 ], [ "000000000019c7f7685f5bdc3afbb5e978cb3f4f70fea7b2b410139741303b53", 0 ], [ "00000000000d3874ce21db78f4d1883ad9ae8b26c1d7c13f3d723ff85629d595", 0 ], [ "0000000000033f87c25275ff72b58630d8da90221f2c84bcbd77c8e615709f8b", 0 ], [ "000000000000dc72adaaae6483eb6737de7d21b3a24b2426330e80b078ceaed1", 0 ], [ "00000000000002fb1337228db02ac464565271f22f045c1b6ee5e449f057a829", 0 ], [ "00000000000001902376ff640d3088899af0819dbd15f602156a13ac2fc8e94e", 0 ], [ "000000000000007ee49761a1c8284a3b8acefa39e37e455be4773d648e2db794", 0 ], [ "00000000000005b4d495a77f57018dbc72bf47993d494349329a3c653f04ab93", 0 ], [ "000000000000009dcb3ae6d68828e2f5ccfd58780abb260354e74484106f81ce", 0 ], [ "00000000a3ceb118021fb42d39be52db951c6f852bb9a241046e972706f7329a", 0 ], [ "00000000574e8e1c27fa54c77b4e7cd1b79de070f0d3ad5b383206ab9777d983", 0 ], [ "0000000039d562f640c1743421d53e7e04c3e8ba222c339fff6f3d25b1d4a7fe", 0 ], [ "000000000001cb1559d55c697871e18d5c26800f77fb11587241bfbec3b15e26", 0 ], [ "000000000006e01a93090319756c7ca826ef655feb0cc2ef9abcc59d67de5e5b", 0 ], [ "000000000000a81aaf5a4c013032638a077af6aad8bc449d74daef8ad3a74419", 0 ], [ "00000000000087d0574963c1582f2161298e2de5e48f74566291ef9afc2be24a", 0 ], [ "0000000000033251e71c347cd663945fb68efe82a8c6666c0b41e93f1c46658d", 0 ], [ "000000000000f592857e6f0e4711b5b93fdf95f2b21a5963bde15be750a07908", 0 ], [ "0000000000004353c8426e18b942a5012934ddac8322b86d6ab98ed7c0ee86ed", 0 ], [ "00000000004f027845b699f42e7d0d30c530e99524c5f97186ce6a250a5fac42", 0 ], [ "000000002fc6407edc060df90785082834867331e6746a43ed34a26fbdc5df64", 0 ], [ "0000000000048733007c91ea3665bd4e1653b10799e3f43abee0fe830ffbb3ad", 0 ], [ "0000000000025a9b1c5afceba0c78c4b0320797acdc1ad50b4e040f148fbff7f", 0 ], [ "00000000007ca6d026d27387edc1c5570de41c61bacbcb1dad2c0f300b49e637", 0 ], [ "00000000000258f683a77ad509da82a4fab24188fdb4b4690e212c50794a9abb", 0 ], [ "0000000000015111bce7b6ac13c930484e14e31e13e43355cb4d63c8f1782440", 0 ], [ "000000000001ca074fdecac7749d95f28f10c83a7e13787fd865bfbe505382bc", 0 ], [ "0000000000001c11a6505dd44ab405fdc07ddfc015f3c1166a5d9352ab58b52c", 0 ], [ "0000000000000c83f7f8e1cab4efa08d6c68c4555fb6ab542e01b87edd8f56ac", 0 ], [ "00000000000009561d0ceba15388573d2a994aff24512ec3ed7d7881aa0997dd", 0 ], [ "00000000007dc7cfbbb94db1fbc076a70a1252fd595686b4d75b2ea77ed6ee9e", 0 ], [ "00000000000251feb68a8c90852f73aeb29ebda191038737b7edd37c9475f4ac", 0 ], [ "0000000000013f9a97045ea9047654e514951288911b2c3986787c27bab49106", 0 ], [ "0000000006e8c37735c61f22bec69f4cb7eba03172349e7012b7704652f3e83a", 0 ], [ "0000000001f341add5657043d8e50e53ba079fe24966a2668f904be5579c84b9", 0 ], [ "000000000029a6275cd477d77939424bd183c2f1308a9912f45aa7cc9ed13b56", 0 ], [ "00000000000a0336239e5e1faedf5bd2eedf38c9a5ba34a832356aea70aeb102", 0 ], [ "000000000003c1a2b25093a64eb624055f6a3a26e18b8e7ea2d9382ec7a3609a", 0 ], [ "000000000001bd89bf7e8740ce22adfa6e8793bd1716a647e558ed1742ee8329", 0 ], [ "0000000000001320421f1bb2c94000e11a621f581fc277c0e2911c3b89f680bd", 0 ], [ "000000000054ce90a949f5ae2d43c4ace599668c6ccbc50620f6d5705922ea7c", 0 ], [ "00000000200d16fea4857e6b73169cc593421a57971acdbcaf87a31d7d8d72c8", 0 ], [ "0000000000e75602181c88f713b91c49de291ed878be305d25b75c0ec5fbe942", 0 ], [ "000000000081f8169c3c3665f20351dc0fe499612ae232ec0b55858a8e5dc6e9", 0 ], [ "0000000000d7ad232e7593fb435d125343b8113bbdb3705ab58ac0e18c26cc79", 0 ], [ "0000000000076df615d887e33193ca2dc0f2fc0e70744512c95da6242e9b1a81", 0 ], [ "0000000000084a62093d1929843e74456686429b698a7ea9b1901c1565779f58", 0 ], [ "00000000000251d1da01e9de9fcaf3ca3a64bff78a5faf51a8e697dfab6b5e4b", 0 ], [ "000000000000609a8798996b1f1fe0b66060a628eadc380d0d369a2318c2d0ec", 0 ], [ "00000000000014770aeab044a022e86d888a6ede75b6474022c71aead3a1db74", 0 ], [ "00000000000004101d04ebc90ade5d4b911aa13c038ecf25e9887d877203ddb8", 0 ], [ "000000007c700410b61eb7ff1aaccbfc3a79e4e4484ad7a2b0eda4d91dc4b613", 0 ], [ "00000000055ff438a031413ee042fd3c0a2b69be98690542806ff123b7988024", 0 ], [ "000000002eca5f9f2c3b656d2550662fdee4c95da133eade51a5cae653bc69fe", 0 ], [ "000000000c679b76ccf0c5b943095fdee8fa466311edbea2c4a05f9430ffef3f", 0 ], [ "00000000007c6f494e32d5d9de58fa008a770fdc0a7b4a141be5b7c2de3ab970", 0 ], [ "0000000000d5dcd5a26c8ad29c1293e70401e2f90d8288469df3816b8cc6d4aa", 0 ], [ "00000000000d754d94f36cacbfb620710672afb1558499cabe17ca62c54a7d3a", 0 ], [ "000000000004096bb78fba714b130f7f1f929e2803c75a7a85619f7a2b86567f", 0 ], [ "0000000000020e686c38d44c35896df35f9f1b7723a82a826a5e2393c25ef68c", 0 ], [ "000000000000504f9af6885c0cb6484109ea205a956c8efae9557a1f5b9233da", 0 ], [ "0000000000000e8746e52e4320ec17e66434a3936a3825f7046fe874e92275fb", 0 ], [ "0000000000000f48d818a9a026270c9f733f629959bea25192596d59874b1ce2", 0 ], [ "00000000eaa9214cb05b241828a1cfb0c4209fb7ea64429815d61f7c1d98939e", 0 ], [ "000000001f7f915a6002cce4edd5cba392307f3a199a520ee8937327a9135162", 0 ], [ "0000000009674ee0c606d687bdcddf8e023462927e2902b3381bc4bb862a7397", 0 ], [ "0000000001f3f3528c083a4b11eb2f04d8bbeca92b57f05d8282909bde78bc77", 0 ], [ "000000000131917ac459aefb91774dbb42caeca497afc0cfd1766e0338cc7f88", 0 ], [ "000000000027634444081e1289354cb50034a506bb306a2ac1d8280683771c5c", 0 ], [ "000000000017a852acff78fbee573329d45bb8b121e9f6fc1e4f687bb3778ada", 0 ], [ "000000000006789e1a00eca982fb2827f680b254c4a0ecb005af4464f3585a02", 0 ], [ "0000000000015d2e9f54b1e9419d6b32ce68ae626cdd7f2a1954f22ca39ae0fa", 0 ], [ "0000000000002f7893bc169165ed9fefb434b6201103f23cc84a747a68ff8797", 0 ], [ "00000000000008471ccf356a18dd48aa12506ef0b6162cb8f98a8d8bb0465902", 0 ], [ "0000000000000596f00b9db53c4111bcde16f3781471c5307af1a996e34ec20a", 0 ], [ "000000000000007b5d2406f64f5f5833c063a6906552e815e603140c00bca951", 0 ], [ "0000000093ca5d935740a1b25f10ce092fd777c2bb521f3156619389ae78931e", 0 ], [ "00000000292f3a48559527341f72400a0f8a783aebcaae5bfa0e390dfaa5286b", 0 ], [ "000000001e852ed7ddf0108d1fce0f4f686f43c8c1b85bcb12c43e564dc7630e", 0 ], [ "000000000c4bea8fb1e7f3a1f3e6c6b3f71388c0ec7eef3de381853767e89f87", 0 ], [ "00000000029ef31a21711b55c4300efa38ace0b706091e373f48285286f2c578", 0 ], [ "0000000000979060786bb008f193d3917e28667bb1b28329f3adadc172e4cce7", 0 ], [ "000000000019030ceb98013b1627517b45b04ee055ef445813bbebaa25fa1ed3", 0 ], [ "00000000000adf202247bb794fc9a3c82cd8767143f1e6ed5f60940ee18b09a8", 0 ], [ "000000000000b19061e2481d8be6183b3d881b0d58601072d2a32729435f6af3", 0 ], [ "0000000000007a6d34f59b29e8d4da53e51e3414acd18527466d064945fe19fc", 0 ], [ "0000000000002e66ca213a2c3e9eb5fa62de29feb83880a0bd29f90fca8ad199", 0 ], [ "0000000000000b4ca10aa100728d0928f37db5296303db1b74ffe29e4a17b6cd", 0 ], [ "0000000000000143309f6b19567955743775f61f8dc6932c0b46cf5fb11c6c72", 0 ], [ "00000000000000b04d5409b3ac60cc18c0b9a3d58b303594635a8f75a9d2abd5", 0 ], [ "000000000000040a2699f62a552703a278608248c2ce823f4cd8845376e9a371", 0 ], [ "00000000000005cfcb850db7e83d4963994f958bae9b1de1483f5aeb3d449925", 0 ], [ "00000000000190f80220e70c1481153671a7c90fd856988c183ab0e3d9313df8", 0 ], [ "000000009374563a06178641d06776f66554c2a094b5319f0801fe35cef72ccf", 0 ], [ "00000000003e4e6e5e8e4a89e7de50eed104d4a49d2992ff101b6740beec7cb5", 0 ], [ "0000000000618cd377d14aaa441cbdb92527894f98da316eca81664f8ab5488d", 0 ], [ "00000000000d977ab2897885fee712f58612fce8c10ffbe9400326fe3429b77b", 0 ], [ "00000000000c3575b487dd0f938c5bc744fa65ca4ca3a9c981b8bda903ec110b", 0 ], [ "0000000000247ac689595ed8d62678bfe53e5af13c0f5455e558f5e6bb375c16", 0 ], [ "0000000000093d175376aa621176511f335a48f824b66d998e8082f85134a48b", 0 ], [ "000000000000c0c0448fe922f2c737946297d35f2c25ad7cc223e11bbe58e1f8", 0 ], [ "00000000000016abe4e7c10ddb658bb089b2ef3b1de3f3329097cf679eedf2b5", 0 ], [ "000000000000242757cea5b68c52b83dd8c2eb9257492074fc69dfa30bd4cbf4", 0 ], [ "00000000000006813f3dd7726a509fbe3101835db155dfd35d44aeae6aedb316", 0 ], [ "000000000000053cc4f39cff1c8cee1aff7e289a85dee84164d2d981afc8f17a", 0 ], [ "00000000000000789724805cf1d37ef689acf52c47a460507f540d5e5ca79bfa", 0 ], [ "00000000000003d71618bb8952887f65540270a5e54d6246b9419e08831b5e4e", 0 ], [ "0000000000000251a513a33eadfad67c015f6e3b291dfd0ae1cc4bb3a43006dc", 0 ], [ "00000000968009e3f8d6e6071e7def68298307717a9af6c2d44986deaae297d5", 0 ], [ "0000000062bcacb734df83bbfa3e1b9a8dfa570ecffb6c29eaaf8e9498cccd30", 0 ], [ "000000001d4618c0931bd3c25ee592c35341f30ff3b549a671f637b3c26ef414", 0 ], [ "000000000418b329df96a004f1b652ad06a7ca295f9f2e711c412d00493f5a86", 0 ], [ "000000000302bfb88e9027237d023c4b969e106c9a7a23a84103776de7880836", 0 ], [ "000000000069b9f7d9134fd93c8b7e3af8b26bbcbb5553af02fb6ed644d7fca5", 0 ], [ "00000000000411ec444240ee91e2777ad18b80dee854e3e838e32209e84774fa", 0 ], [ "0000000000007c73f322eba4dee5463305227c7e1a8139f1b7b296444f265052", 0 ], [ "00000000000129adf0f9c0242aedbb9d87935d67ee4ddea758c00344d4b6a29e", 0 ], [ "000000000000343594e671158b6e1b4b6499f6ad66e2aeabf1f6d295d3dba850", 0 ], [ "000000000000320f0d5c22ba22b588b97a0e02273034bcd53669b1c8c4eeda1b", 0 ], [ "0000000000001e8cdb2d98471a5c60bdbddbe644b9ad08e17a97b3a7dce1e332", 0 ], [ "0000000000000026c9994ccdd027e86f51a2e36812f754bd855a7f9b1ca56511", 0 ], [ "00000000000002746a820a2c08b35b8d0493c4b5d468fcc971b9c88003e84849", 0 ], [ "000000000002949f844e92645df73ce9c093e5aac0d962a0fa13eb076eec835c", 0 ], [ "00000000000156fbda67468ae2863993b98a41227c420246e4bc4e68c84df0e8", 0 ], [ "000000000003b43c6c807122c8dd10e2a0cffbf72946f41c97c1ab82d416f74d", 0 ], [ "000000000004e0635c2438b1b649007e5d424b3de846299a8db53049ebf4da0c", 0 ], [ "00000000000258e4b79e3cca2ab7d12b35ba77fc491572f2e794f0a10f5236d9", 0 ], [ "0000000000f5816875d9fece105e499b0467b8fb23ea973c48d828a235acdebd", 0 ], [ "000000000001353bbaec810af7a4c74b4964ae072361c0889ed6d59cf16db6fd", 0 ], [ "00000000000b354d8c389473670ca6bed7e3dffa069f270d35ec9dad810af141", 0 ], [ "000000000002fa1f39e7cd8730fa08085ba2b532146ad1ef3b400a13e835ca36", 0 ], [ "000000000000d2c7943eee59652a9783bff27e474a92ec206c5c6e3cdd58d0d7", 0 ], [ "00000000000036034181b4d9a84a97490b49fbee4262b9cfb25a7bfc9c0eec9f", 0 ], [ "00000000000007deb59381cce692f152fc902732d96a7e7d463bc83915b37c0a", 0 ], [ "00000000ea7d32833462c0f72ade0cae4766e6065caa4e510331929c56d16632", 0 ], [ "000000000068fce0ddd370d4c8f9129a7bc7843e75fc57666202d3b90239e269", 0 ], [ "0000000026b4a2212c9c9493f8bd9d5331cab6d8eda8ee017410e58a783ca069", 0 ], [ "0000000009535ea2dc7e83c31cd17f1db1bb78b0a678fc0610844273de143bf5", 0 ], [ "00000000008607cbd5baca91d5b8b82ee965aace335744a3e21578af22bee8ba", 0 ], [ "000000000030dcedae0f5e98c4e176f9569ce76c4d4135bb028fc3144ef381d9", 0 ], [ "0000000000297c3f0e3fa85731222ba934a955bf513247a72a33c74c498cadbe", 0 ], [ "0000000000020a0d4a1e8120cbdb486e758b58919c9df12e0edc8ca1f2795e94", 0 ], [ "000000000000078773afc9023182bfb6534a60158672e6bc6e8aa5052854da80", 0 ], [ "00000000000102ecdd67800807d9e137357805b9bbf8a439ed86bde5b19fbeb7", 0 ], [ "0000000000005c3d2e3c7ee737c67ab465533acb233e0df902c1525fc11c3a55", 0 ], [ "0000000000001a77771650cdbbceff87caa4461391ba6a4ddc9815b5b0ab47b0", 0 ], [ "000000000000071ec390bbd28fa2a84e52ab5b32901d0723d22646b04ae01dc3", 0 ], [ "00000000000005c3ec3194f710c6f26ee736d59cc935ddfa574440f39846433a", 0 ], [ "00000000000001cc3df6924591939269d61ead563b9eb68402a2ca01d7ff99e2", 0 ], [ "000000008c778b3554ceaf3a13a856acbfe46b5750fb86fd92ba30651c2852f4", 0 ], [ "00000000107ca31f75f8ea76073dda3c33330b2706c1ec20c3ec240e853b65c5", 0 ], [ "0000000006ba99b08e7f2869ce113e2ad7464891de7b4cfa96f330d706a2da46", 0 ], [ "000000000f31036bd51b2818f6dfb90ada9be5019abf55fb15694b181e269865", 0 ], [ "00000000004fcc101bc47eb7a379b9f608d5c00ac04d2d0ea165ae2937070796", 0 ], [ "000000000044d5ca3eda838edef0df7e69e1934047f8482822ce58ff7a18466d", 0 ], [ "000000000029bdfb157be6d400c4dd3370d98afdd8cd3db6f1ada8c19bbf4650", 0 ], [ "000000000005e9699ad8035caa4f73af781ac2040c87b8aa77459b3607209aa8", 0 ], [ "000000000001c0ba033f7d85beeaa167c9bde0e192240653a7ff6d9b81679842", 0 ], [ "0000000000000e0176111f29e800b49c7b8c7226dbbf4df715f1a4f06bcaaa49", 0 ], [ "00000000ac3bb2cf42192e9053f5384355228a2b3d70b4ece4d45773a5d5ddd2", 0 ], [ "000000000f29f7b60842b1044b2db7998e9bcbd92f8ec6fe8d159c6d582f1f1a", 0 ], [ "00000000352f86bc5f9760961a25de009940508bb2cd0b37f378fbc87dc97eef", 0 ], [ "000000000e9b3086008679ed57f59857f64c3954368ba1088117dbf88d5839cd", 0 ], [ "000000000015324bd8fed0e61b62bd1d6c663b862cb98ea03c494a92e4a8d0af", 0 ], [ "000000000020475a181b7a084b341860a72fc0c1fdfcc13a85adeb0471444b0f", 0 ], [ "0000000000031905c508a975707b74f24e733880382775ee0e6250666473e1d8", 0 ], [ "000000000000ca38b15d2ea33a6eef505a9c661540a18882f79ba9a3c575a9bd", 0 ], [ "000000000002739979a7a89fa279303b6606885e750b19e91ed637d7f222b392", 0 ], [ "00000000000091e935fc266facc2c92759d5468a39aee5be6b76b519a9bc7567", 0 ], [ "00000000000006e339938254208203b67c3c400f703fc29535fc646699e36e58", 0 ], [ "00000000000008f6f1d1150d77f93a7f1baa24b65ceb471b1825b2e92ca6997c", 0 ], [ "000000000000004894e1edcc5421dbcec77d47c5c50bf27b2cff3f1c242c9eb3", 0 ], [ "000000000000054e97fb5e1a8bd7900f7c329385895761aaa40d11b3c75b0c8e", 0 ], [ "0000000000000600f4bcc5a89527eede43d1d3342dc12eee1371ab534b0102dc", 0 ], [ "00000000d1ad5c3ef8c3bb4610b34c264e4ca1ea51c4c8bac18b215e7dc96948", 0 ], [ "0000000062f6a07ae11f9724b8ba9dc2b7348ffd02b59edd3cd2bf387fab9723", 0 ], [ "000000000014e4c97c9b09ff20203213f3336b0927fd19d214cef1f544756e39", 0 ], [ "0000000000d004681880e127aed3fa73255a2e75c2e5c8580cd555526614b294", 0 ], [ "000000000008093189bba28d40662d6964afc1c0fc9b5c1681bbe32e8bee6c0b", 0 ], [ "00000000002df10cf8165b2204ef4db6721c8c2119d60463b040fbc81c266bbf", 0 ], [ "00000000000c28c789e7cd9800b98c1dd32e2dda54048116ff47ed856a14acfb", 0 ], [ "000000000003e8e7755d9b8299b28c71d9f0e18909f25bc9f3eeec3464ece1dd", 0 ], [ "0000000000004b95a0103abe2cb97806caca76f6922d9c5df003cf4a467df822", 0 ], [ "0000000000005f12d2ab72bfa715860444c281265ef77e09dc2d041ce89506c0", 0 ], [ "00000000000016eeedb3f367daaee93334188db877fb01cd0282b990f60812b3", 0 ], [ "00000000000001daf3bd8306b6f6899af8aa656d87ac2aa37d493fdcb0cb3000", 0 ], [ "0000000000000390b86892ad0bed9b520783056961cad7362ace8049aa00471c", 0 ], [ "00000000000002105d01b4de7d3e3ada9c757a239151d50b5dd193e3951a23cc", 0 ], [ "00000000000002362fa802df308201a4b1fff2fd8a91892915a46f5d54098ff4", 0 ], [ "00000000000004fb8aa6c6aecb64b9d8d7e691a6cd56fad69fc5278b9e8d98cb", 0 ], [ "00000000000000ce3bd9752b2508ddae1ee71332e905163a3c0d7e10b8c472f7", 0 ], [ "00000000000002d0d8520982f15a45d4a405334c61886b6d13d95843386af647", 0 ], [ "00000000cafd25502ad67d5d409edfc98f5bbd3173e86e085c69658d58da5f70", 0 ], [ "00000000b01e0675317a29a07731ea092fa029016a40ed8bb4fc17cde50eda05", 0 ], [ "000000002676805396ed2883ccc8ad401aa0a974627559fbae2416ba5c54999c", 0 ], [ "0000000000030ab759158f3d425824228dc5c91f32db91d404bee29ee3a41878", 0 ], [ "00000000000da1c8040ec08e7490fb201ca1fb3571f29c0efd3351ae197d3017", 0 ], [ "000000000004e3cba890c16ffc7d1c019d4ab88afa39315164e1b08b8e6a9330", 0 ], [ "00000000000bdcfb630b43977be44529e54daa02d199014a0967deac669bd060", 0 ], [ "000000000007254038f9c621d6df0d9fbd90b5697e4170cd6090daaf579f3790", 0 ], [ "000000000002263e27ea1cec943632bf469a28b067f0bfde3b9a6b48540981b4", 0 ], [ "000000000000f194a8d17e683d17f222d23a9032f034d4dc4497263fd785dfa0", 0 ], [ "00000000000036e359b7b07044e3cd5b132a3c72501a0f3f9ccde167f5316bba", 0 ], [ "0000000000000b10e98a90e0fd1ffbf7d5fc5a76e8e6e960c6fb158711af6f48", 0 ], [ "0000000000000104e1e4303b8dae78389bb4e6c38f3eb3fe42aec6464bd5c397", 0 ], [ "00000000000000bde368a635921f5ad25aeb4b784651de24d624cf20c27691c7", 0 ], [ "0000000081a626a33cff134e7e56dc0f0a67b1735c96256774885d5d095807c0", 0 ], [ "0000000055d357cdf39130eb767f416101e79025515906bea528f43cb6446920", 0 ], [ "0000000012558b30f9c1a156fd80b02451e8dfcc7fe0350fb4adeeb84951a0a6", 0 ], [ "000000000001a4868924fc7cca0334ffc4dd49c07fb841c1da059a7c219bdf95", 0 ], [ "00000000000010086bd2bba88c71b08cfc7e24183d610a2803e6d382049d52c0", 0 ], [ "0000000000018c83992fe05d820b097228de93787e3f59e65cb89ad4c385e364", 0 ], [ "00000000000023ab80324770ff4c6802d09e5e1e7de78d2a8e64783904d47f19", 0 ], [ "000000000000287fa294ea557835d8c98bfe94c4d8b18d5b10f1b62d68957113", 0 ], [ "000000000001d842f5a0dff13820ba1e151fd8c886e28e648a0be41f3a3f1cb3", 0 ], [ "000000000000906854973b2ec51409f0b78b25b074eef3f0dbb31e1060c07c3d", 0 ], [ "00000000000009e694e22b97a4757bffef74f0ccd832398b3e815171636e3a85", 0 ], [ "0000000000000594b95678610bd47671b1142eb575d1c1d4a0073f69a71a3c65", 0 ], [ "00000000000002ac6d5c058c9932f350aeef84f6e334f4e01b40be4db537f8c2", 0 ], [ "00000000000000c9a91d8277c58eab3bfda59d3068142dd54216129e5597ccbd", 0 ], [ "0000000000000051bff2f64c9078fb346d6a2a209ba5c3ffa0048c6b7027e47f", 0 ], [ "000000000000df3c366a105ce9ed82a4917c9e19f0736493894feaba2542c7cd", 0 ], [ "0000000000007c8006959f91675b2dbf6264a1172279c826ae7f561b70e88b12", 0 ], [ "0000000000015ab3720de7669e8731c84c392aae3509d937b8d883c304e0ca86", 0 ], [ "0000000000016d7156ee43da389020fb5d30f05e11498c54f7e324561d6a6039", 0 ], [ "0000000000009c9592f83d63fe39839080ced253e1d71c52bce576f823b7722a", 0 ], [ "00000000003dee6b438ddf51b831fbedb9d2ee91644aaf5866e3a85c740b3a99", 0 ], [ "00000000000155f5594d8a3ade605d1504ee9a6f6389f1c4516e974698ebb9e4", 0 ], [ "000000000001e21adfc306bf4aa2ad90e3c2aa4a43263d1bbdc70bf9f1593416", 0 ], [ "0000000000008218e84ba7d9850a5c12b77ec5d1348e7cbdfdcb86f8fe929682", 0 ], [ "00000000000054fb41b42b30fff1738104c3edca6dab47c75e4d3565bc4b9e34", 0 ], [ "0000000000002763b825c315ba35959dcc1bd8114627949ede769ac2eece8248", 0 ], [ "00000000000007437044da0baed38a28e2991c6a527f495e91739a8d9c35acbb", 0 ], [ "000000000000032d74ad8eb0a0be6b39b8e095bd9ca8537da93aae15087aafaf", 0 ], [ "000000000000006d4025181f5b54cca6d730cc26313817c6529ba9ed62cc83b3", 0 ], [ "000000001c3ad81ffea0b74d356b6886fd3381506b7c568f96c88a78815ede09", 0 ], [ "000000000140739d224af1254712d8c4e9fb9082b381baf22c628e459157ce49", 0 ], [ "000000000306491c835f1a03c8d1e17645435296d3593dacba8ab1a7d9341d38", 0 ], [ "000000000002b383618b228eb8e4cfcf269ba647b91ac6d60ddd070295709ad1", 0 ], [ "000000000000c90fc724a76407b4405032474fc8d1649817f7ad238b96856c6a", 0 ], [ "0000000000002d5a62b323a5f213152dd84e2f415a3c6c28043c0ccaaddb3229", 0 ], [ "0000000000008c086a21457ba523b682356c760538000a480650cd667a29647a", 0 ], [ "00000000000007c586d36266aa83d8cc702aa29f31e3cc01c6eeac5a0f5f9887", 0 ], [ "0000000000013bf175e35603f24758bf8d40b1f5c266e707e3ba4de6fae43a7f", 0 ], [ "00000000000096841c486983a4333afb2525549abe57e7263723b9782e9cfef1", 0 ], [ "00000000000012dfd7c4e1f40a1dd4833da2d010a33fc65c053871884146c941", 0 ], [ "0000000000000b47eb6bc8c6562b5a30cefcf81623a37f6f61cc7497a530eb33", 0 ], [ "0000000000000021ca4558aeb796f900e581c029d751f89e1a69ae9ba9f6ebb3", 0 ], [ "00000000000000a5bf9029aebb1956200304ffee31bc09f1323ae412d81fa2b2", 0 ], [ "0000000000000046f38ada53de3346d8191f69c8f3c0ba9e1950f5bf291989c4", 0 ], [ "00000000658b5a572ea407ac49a1dccf85d67d0adfc5f613b17fa3fff1d99d51", 0 ], [ "000000005d6be9ae758c520b0061feee99cd0a231f982cc074e4d0ced1f96952", 0 ], [ "0000000001aa4671747707d329a94c398c04aaf2268e551ac5d6a7f29ffd4acd", 0 ], [ "0000000004b441b97963463faca7a933469fabfa3e7b243621159e445e5c192a", 0 ], [ "0000000002ce8842113bc875330fa77f3b984a90806a5ec0bb73321fef3c76c6", 0 ], [ "0000000000019761bf9a1c6f679b880e9fb45b3f6dc1accdbdcfce01368c9377", 0 ], [ "0000000000008a069efd1a7923557be3d9584d307b2555dc0a56d66e74e083e1", 0 ], [ "000000000001c14cec52030659ef7d45318ca574f1633ef69e9c8c9bd7e45289", 0 ], [ "0000000000009cfccb8a27f66f1d9ff40c9d47449f78d82fee2465daca582ab7", 0 ], [ "0000000000007f30cfae7fbb8ff965f70d500b98be202b1dd57ea418500c922d", 0 ], [ "0000000000002cbd2dbab4352fe4979e0d5afc47f21ef575ae0e3bb620a5478a", 0 ], [ "000000000000017a872a5c7a15b3cb6e1ecf9e009759848b85c19ca6e7bd16d2", 0 ], [ "00000000000001ade79216032b49854c966a1061fd3f8c6c56a0d38d0024629e", 0 ], [ "0000000000000090b8dfe4dde9f9f8d675642db97b3649bd147f60d1fc64cd76", 0 ], [ "0000000000000109ed5f0d6fc387ad1bc45db1e522f76adce131067fc64440ec", 0 ], [ "000000000000003105650f0b8e7b4cb466cd32ff5608f59906879aff5cad64a7", 0 ], [ "0000000000000113d4262419a8aa3a4fe928c0ea81893a2d2ffee5258b2085d8", 0 ], [ "00000000000000f15b8a196b1c3568d14b5a7856da2fef7a7f5548266582ff28", 0 ], [ "0000000000000034fb9e91c8b5f7147bd1a4f089d19a266d183df6f8497d1dff", 0 ], [ "000000000000005e51ad800c9e8ab11abb4b945f5ea86b120fa140c8af6301e0", 0 ], [ "00000000000000e903f2002fd08a732fd5380ea1f2dac26bb84d57e247af8ac2", 0 ], [ "000000000015115dac432884296259f508dae6b6f5f15cef17939840f5a295c3", 0 ], [ "000000000029913c80e5f49d413603d91f5fd67b76a7e187f76c077973be6f8a", 0 ], [ "00000000002e864e470ccec1fec0ca5f2053c9a9b8978a40f3482b4d30f683a9", 0 ], [ "00000000001ccf523df85df9abdb7c5bbad5c5fcbd12a4a8eb4700de7291f03b", 0 ], [ "00000000002aa81027df021e3ccde48dff6e7f01a4aba27727308f2ce17f2f1a", 0 ], [ "000000000015a577d71d65bde7e8f5359458336218dc024584f7510b38dc1259", 0 ], [ "00000000003aef1877bcc6817cac497aeb95af3336ba2908e8194f96a2c9fc29", 0 ], [ "00000000000ccd42d542ddca68300ec2a9db2564327108234641535fd51aa7f3", 0 ], [ "000000000000a2652b2e523866f3c4d5c07dc1c204d439b627f2ab2848bfa139", 0 ], [ "0000000000002c065179a394d8da754c2e2db5fed21def076c16c24a902b448d", 0 ], [ "000000000000175a878558186e53b559e494ce7e9f687bf0462d63169bfcce03", 0 ], [ "00000000000007524a71cc81cadbd1ddf9d38848fa8081ad2a72eade4b70d1c1", 0 ], [ "0000000000000159321405d24d99131df6bf69ffeca30c4a949926807c4175ad", 0 ], [ "000000000000016c271ae44c8dca3567b332ec178a243be2a7dfa7a0aef270c3", 0 ], [ "00000000000000a7d62de601cdf73e25c49c1c99717c94ffd574fc657fd42fa8", 0 ], [ "0000000000000052d492170de491c1355d640bae48f4d954009e963f6f9a18c3", 0 ], [ "000000006f5707f2f707b9ddcce2739723e911210b131da4ca1efdff581212ad", 0 ], [ "00000000021be68dc9c33db0c2222e97cd2c06fc43834e8f5292133c45c2abb4", 0 ], [ "00000000019ca3eaf7c39f70a7a1a736f74021abf885bebc5d91aa946496bac5", 0 ], [ "00000000006e4752fbe2627ebb2d0118f7437908a8219f973324727195335209", 0 ], [ "00000000038471612a0955307f367071888985707ec0e42c82f9145caed8fea1", 0 ], [ "000000000004604d2d7d921b21d86f2ade82ded3af33877ec59d47072023d763", 0 ], [ "000000000034a3e45665a8dcbb94e7a218375a5199b3f3ca2cc7b5fe151bb198", 0 ], [ "0000000000043fb2c2ff5db60c6d2d35a633746e8585e04a096a9b55a4787fe6", 0 ], [ "0000000000020d4d8735b66134c1fcdd1d3f3d135b9ff3f70968ef96c227fb75", 0 ], [ "0000000000004f3f4dc1fa11a6ad9bd320413b042eb599c4599a14d341f6825f", 0 ], [ "0000000000001e0a495d23acf46a44f8b569ada39ac70730da5e9109871b77e9", 0 ], [ "00000000000002257a08acca858f239fabb258a7cc1665fc464f6e18e9372d32", 0 ], [ "00000000000002845d416fbfa05a5d40ba5ba5418a64f06443042a53cf1fd608", 0 ], [ "00000000000000fee91a2ae8b8d1bb9a687c9b28b0185723c8ff6ffdac2e9ce4", 0 ], [ "00000000000001d6874b4d88e387098c0b7100ff674d99781fc7045a78216a15", 0 ], [ "00000000000144a03e701c199673d72fc63766bcf0cdaf565f4c941c7ef72971", 0 ], [ "000000009b6cc4d8aee22cca6880e4d7bb30bff2851034ad437d63d3a7278de7", 0 ], [ "0000000023e998d64618475e31b4aee9d83d2bc32cb6d062aa97c0b4651fed08", 0 ], [ "0000000000036f4bf6b42a7776a97872fa24362064c5bc4bc946acb70ab6fbf4", 0 ], [ "0000000001e2252455ffd0cf0b4109ace996a0d2a03999f5cc5c5e08fb6130ac", 0 ], [ "0000000000002713db42d53f0c2d86c904f4e0338652acc1cbda953c530a15bb", 0 ], [ "000000000001b075f9ccc604a50326732f5d42373c4a831978be0e2d830cac75", 0 ], [ "0000000000000bfa7d93c6b36298b933b1a652c95ee9f0de4151e007f3180391", 0 ], [ "000000000002c60a0af1cfeb9c26c60970b354897fd0a94c8e5c414d0767b06b", 0 ], [ "0000000000001f2d9462507a9408859fb0b5f97013d6b4577337b0382340c5aa", 0 ], [ "0000000000000b7428e0d3c6c7fd2df623a74125db4989b1c61c78eeed1bcde5", 0 ], [ "00000000000002e8b4f1fa041a37515c1b76d59994792f1c772c9a4993c194dc", 0 ], [ "0000000000094e70c0cf5185b480542a1faa8392a3f2f7f583d91e033856d7ce", 0 ], [ "000000005b036d8c18ed5d1219e4137bd71438c9b1ba7ff4d10a626e9a7bcc98", 0 ], [ "0000000008745d4a943e958f5cb5084646c0fe1cae57eeab666c3ad0d4ff1dec", 0 ], [ "00000000000f8c5b3455e540d074b5c71709e37f8950975953798d27bdc701fa", 0 ], [ "0000000000050885884f7ac233bb174cf7b33c037f81907f7766afe9d0ad9091", 0 ], [ "000000000002d7cd1043ccd0581a47d6fdf82a7cf1646b61495f917a48ebeb5c", 0 ], [ "000000000003a2b3e3d7ef47829db1672bfd79e49f32ef3a04ec7c4df355392b", 0 ], [ "0000000000032a6c7e5bc3878c1815bc6759594a4736638fdacaa5642be3e649", 0 ], [ "000000000001386a3904f0ba4f25dc7ace09b67a6fe8977e7aecc55813fa9ac5", 0 ], [ "0000000000003fe030a2231da87076679c1d38d323bf56b45ceb49a5128fb4b1", 0 ], [ "000000000000147cd3b6195c6a727cd4fe6b3a879d7934e52bf29020ed9c6fcc", 0 ], [ "00000000000003ed5a0a7176f3f1b3ed26510045af2860e5b6313b358774fbad", 0 ], [ "00000000000000c2952ac8a580895ac13799a9c29badb6599bc4a86c1fc83b6e", 0 ], [ "0000000000000056f49d6f7b8243eecf6597946158efe044b07fd091398e380d", 0 ], [ "000000000000006b039683c36b18ec712346521edce4dc5b81cdaf6475d89bd7", 0 ], [ "00000000000000525de83fba2439549ef0ed78d6d08516a0513abb972b0fca95", 0 ], [ "000000000000006c5403ae9c42acf37362885c75c1a71a6b7fe20f9cfc5304a7", 0 ], [ "000000000000006f881a62bc5ec9d4c4da83ddc6619a7eee82617e26e2c7ef3c", 0 ], [ "000000000000012941300197c5b6627a66f9cf48ae9c6791b36c63c0218a1be9", 0 ], [ "00000000000002cd7ec2e00992a4dc6c5e0a56cfbc19b5afa9730bd94f174b5b", 0 ], [ "000000000022e09ee2ee7b3fd223cb9ccfe11058cca5ad0c705fe5a0c26b28dc", 0 ], [ "0000000007d35ebaf81412d40d1224bdc5792bfbc70827c09f05dc5fb168e67f", 0 ], [ "00000000328e1b1aecf68947ad53fb11c58a383704ddbb8b29704669e22225bd", 0 ], [ "000000000003d3b3f171fd10fda1be9d4464b1438bb9443081c2c224a047cc4e", 0 ], [ "000000000001e3c5dcea0586d3c8f69c0f35658fae283d29f64df9b5301bc721", 0 ], [ "00000000000ce5f3757a0cab09a8cb131b3f2c63303375ad1c84fe423866d33f", 0 ], [ "00000000000ca01b96070fb643bcebbc862cff4da78dcd52de1418c940d4f466", 0 ], [ "0000000000006eb74e5036cf42888759c4ebf91a5eb128463e60ae9ab02876a3", 0 ], [ "000000000003aae0765dfee956b322477d786a2cde617ff073e0bc4eeaf7c252", 0 ], [ "00000000000033421d804b4bc0f7dc61715d2fc0cc2a98904ff5e1f9ef909010", 0 ], [ "0000000000002a24b916b5f03bd47250276ad32f08a1684334c7f181b0b7a055", 0 ], [ "00000000000002a7399ec806255c4ae63d7583001bbde70e2038e9b90fb824f4", 0 ], [ "00000000000000ec89aaa13c7222b3ec787a487cdc7a17c1ee87ce313e6ed4d3", 0 ], [ "00000000000001564cf9db3397bd0983a68f450d5b7e59824339fe1d46ba1c75", 0 ], [ "00000000000e932953388774b6b3492d8756f936d74fda1d33eace33538fb0bb", 0 ], [ "0000000084c2d56f703e72f6ad637105409552792ee482bbc14376cfb29c30d9", 0 ], [ "00000000392f30ba333fac2e4937e162105ba2b20fe953848b1a4c004f460223", 0 ], [ "00000000000842b42c56e4dc573efd9b6b6864dba81730c4f95b837d52078ad5", 0 ], [ "0000000003e4cca12f6109687fcccfc5c3827bf3bca2487096fec0293b4b351e", 0 ], [ "00000000007b7eece3ebbf77ed583a711c8427284ea9b556ec67efd14e7f5d90", 0 ], [ "000000000002c0e026657401be7998fce1618869ec073a49ac935a15d16c5741", 0 ], [ "00000000000cf19ef67151f6d06b426371dfa63d9d2bbd6024cca520cf4d96b4", 0 ], [ "0000000000019a6ef183423833a4347d77e8687b4fc83a85f4c98c579631acbe", 0 ], [ "000000000000a292b9ff43becd4770243d2750e2b3c4e81a6ed79b8abd2f5052", 0 ], [ "000000000000280db4a9a31097024bc81f0358ba624f1f8dd83a2362a156a817", 0 ], [ "00000000000009b17b295d898cda8899ce547183fd63fa901b9f502aed00c45d", 0 ], [ "0000000000000013f5c40f6b0e7e8fe854045135564a4df6ff4ca736861d7ea8", 0 ], [ "000000000000c39ffca7d1daad0d4f8af9ee108443bb1b4352cd740fd8297aef", 0 ], [ "000000000002f42ee90d7d459393eb90e2ea5a3ed292394ce1dc5f7a42d66ce0", 0 ], [ "0000000000010d6bd31805e0a9b8629192c0ad704641d2b08c28865052bbf469", 0 ], [ "0000000001015f5067612dc0d681d71b33d278c50ca88d7756322ab90f753290", 0 ], [ "000000000003dadd324301ee6157c29e7aa9f120edefaf05369d849510e6d60c", 0 ], [ "000000000000a62107ea11c5db9929d819181d8903624e9088b8700d1dc66ea7", 0 ], [ "00000000000022b91e1b652f626cd3a81bfb2ff70717ace53c488dd45c75fcbb", 0 ], [ "0000000000002845027a6a08c436c6e99aa8af0f7c744a722fd598ba0f66f4cb", 0 ], [ "000000000000ae5347baecbcb3cd01265f0e52c8819f830dcfc6dafa1ec4327a", 0 ], [ "0000000000008dd3169522647ae90ca0a3acc405f0e8c2b53dab013433708921", 0 ], [ "00000000000023abea5dd709951fb1fa5c34a75670ddc7eea46d2d23c6033669", 0 ], [ "00000000000006fe20edd4be3beabc4432fbe410ab53466660105ced53056190", 0 ], [ "000000000000003f6d6889d2917ba88f6e286c156028baebf05be409e1b97ef8", 0 ], [ "000000000000005d871f102aaa25e60855c96c1aa8404f004db1c8bbfab341e9", 0 ], [ "0000000000000197fac06dd6c7f80c838b6a21f1ce72f10aa6ba0aff40c3cb92", 0 ], [ "0000000000000289a999cf132efbee896d8c22e2f9d1036381b00d72c41660e3", 0 ], [ "00000000e9f6bd4700dea0c0841272461e4e9d125b8fe2c35a2ca39f77269321", 0 ], [ "00000000f91f03ac1d08214a3646c2bef1878961a8c40d867254d733fd9cb2a3", 0 ], [ "000000003d42ef351c6a1fb5e2d43d1a28ca095052be35ad9bb901b097c667c8", 0 ], [ "000000000014b426a9844698b6369c0e2befe4e369f1dd01c157dbdd472c9136", 0 ], [ "000000000016dfa525db05b9db92a080e0da65a4a0b15e538649eb4c0c670cf4", 0 ], [ "0000000000027a82eb5b1ab46a276a9aa19e3a1e52e2328c07a50db314664148", 0 ], [ "000000000026945c53ba1f9b0c34f9e502f3aa64c9979ce583b93daf347d2292", 0 ], [ "00000000000f64a42d38e16119aa724e6d859d8b7ed2964bd0929a226e57c838", 0 ], [ "0000000000011bee42dca16315be14fd0be451e4385c787a66c7dc6c0a498ce2", 0 ], [ "0000000000007fcace99545546c5ee4df862e21840543865ad0944ca7b82baf7", 0 ], [ "0000000000003b3a9be8e418e11db77aa16dbf9f04a9b43b34466e7b41520fa2", 0 ], [ "00000000000004ae741f8cd7f6f20231f8be6b89946e50339f0089a2e5c6d4d6", 0 ], [ "0000000000000379b21385de297e65a62e4d15ee27fbf1e3b4fa7a46b4a274ba", 0 ], [ "000000000001fd6b7db603c305be360c602800e5d9068bd65bae111b4561d5ab", 0 ], [ "000000003925c7eb3144eb77e7891a607152b662b161cd4a052e2a5689c4b694", 0 ], [ "000000000000a8476194924cd6612277821149e22f7326a054c09c7d55b8a9d5", 0 ], [ "0000000009ddc12332eb5903b89ddfd116bfd9b300c4d70821e749a302fa438b", 0 ], [ "0000000000028fe3bfc47a9ad8a71c90fa3edea0c1d04f823c5a9d8674b9d1c5", 0 ], [ "000000000000075849c07342e632fa3f2b4e137de35703e91c62cb568a8583ea", 0 ], [ "0000000000001100406d8447ce19989346956134e2dabb87f93ff1b32208dc21", 0 ], [ "0000000000006a8a2fd9d16a22f28523940811b3c4f179f888249b6f5f19c708", 0 ], [ "000000000001af7c8a48d294945d937c3f1ab297617bab1a0eb1d9a40e543139", 0 ], [ "00000000000040eafb8f54cb988a19d0370379be0b2917787e640720677ba6de", 0 ], [ "000000000000025f7bc6cb5759f267fd649620c69f6518213729bb6aeb4d98d3", 0 ], [ "0000000000000217a8588f1af88d2f73a96a658f0aea62de5c53b5b348346456", 0 ], [ "00000000000001b8aa8353bbafb6f47125f67a711c0a2a7a00bfebff5a8df093", 0 ], [ "000000004ca77c8921259d7da52f341526df3f34edb62e3e2888b7ce42b8c29f", 0 ], [ "000000005c8253a86af2492291e888d78d0a69a7a657a221e59b23eb6291fcff", 0 ], [ "0000000000fba14ebb3757a9348a05b07ec207b25aaffeac4118237e665fc566", 0 ], [ "0000000008f01a3c024cb6d1814e54659c72b17e34e2b60fd35af2184b6bd3ea", 0 ], [ "0000000003da1325f0d607889753f3a7214c3e559b9834c6f0e37bd52e14eaec", 0 ], [ "0000000000d303f0b50fc25ea141ad3c26d0dfe61fa4cfcc6875edbcef902163", 0 ], [ "00000000002131de3bcff721c93c169e34450054c18fc02cd5a8e08c7c3fd567", 0 ], [ "00000000000c69cdb751a4ef5f527ae244909ddfda10a4caed4d6f8dd44e51fe", 0 ], [ "0000000000024819bfbc99fd2032441181dcb2456ada1d047c4b6b7829be62a0", 0 ], [ "00000000000077021c5164bc1014b24abd321f160bb914a1257a86645f923385", 0 ], [ "00000000000038e149b42e964bdeb10f01fbbfd38ce57ec25eb3fdfb712cf9b0", 0 ], [ "000000000000047dd3d1ce9862add6979aa622a7cb2141b4c6ec569b172dd776", 0 ], [ "0000000090c401521295d1040e0f9b6cb65da914085bb9346e60477837dab234", 0 ], [ "00000000f36784781eaf4b0d3ef92525b6cf55e910c782bd4f355b71ee40dc36", 0 ], [ "000000001d3848f040d48696a9e258798bea34969e810ad01e8092183f201dfd", 0 ], [ "0000000007658642f1e8ac45feec2766358f425030b14ad824f3a6df30b9eb15", 0 ], [ "00000000028e5b819d9e197b1d3f1246a2a6990d8e2360371dbf258c2c5861fb", 0 ], [ "00000000002a8dbd19a807d955c7d01962fea32f5ae027345121176ac10c20f4", 0 ], [ "0000000000144908febd5cbacd1d9b828817f0350211be3248a1ec2d3ac3e251", 0 ], [ "00000000000a302f19d696c7be172c6ac92ec2adf956417bba482d3e5285e5d7", 0 ], [ "000000000000a289eb62cae8c41644d7c9de31148f711744aa5409164b90d6e3", 0 ], [ "000000000000036a6f6002c633b6be318745d2f2ff1520daa6a49db7649bca67", 0 ], [ "0000000000000293db488f4a3c7289489664e6e7e1ec917dc58c83ec828a4730", 0 ], [ "0000000000000e24d4ce3b9247d6316791438ab82ea755e788112bb9729730cf", 0 ], [ "00000000000003a18b92493908ebe4ccecf24bfeda95bf3b8a026e3c01af116a", 0 ], [ "0000000000000007a2b7ba9dd58c20651b477daf83df5a7ac24b856b22f1fb25", 0 ], [ "000000000000000ce321e0271dd532a6ce58737151baa84a77a585df614c2ab6", 0 ], [ "000000000000004ebec3379d6a8569295a2d0a0c0e0c815d2b01803315032185", 0 ], [ "000000000000001bb9ed28d9b0a70fee0b6d42f91f3db53f2086eef4daabce30", 0 ], [ "000000007c5711c573d147a6fae21faf529c039220c97dfe2ba96e732d88fa89", 0 ], [ "000000008e5a5e820d1a10dbeecf6f6df3bf7ab56e46eec275d8ca1a52e86b68", 0 ], [ "000000003fa06ace5db33de18cf03b0c56d4e62cdaf8ab533919953c22bffaf1", 0 ], [ "000000000000e6442b0c74fa811319edf2edd5f8d9b2e3ee831b4bdee644fbd0", 0 ], [ "00000000011d0c3f98e9c3db6b51468be632bdef0c47f5e45871b771e5b0bc57", 0 ], [ "000000000000e3c0978d872ed3b3a43f6f319995459105159b5f4e92143d40d2", 0 ], [ "000000000000cdf25c3e15601dcb798c6cf8d2dd89002a4e046b746be6b87fa0", 0 ], [ "000000000000521507052d13f4fac6c01c0099466720bea95c2e9349aef7fa5f", 0 ], [ "00000000000064823750f1a6b7cd1748dfcc73376086cfdba987d2a36fcddb71", 0 ], [ "0000000000000b4a41be0612f47a58efb899dc1cc0965c1c1fac89e1ea69f587", 0 ], [ "00000000000010aab857bf7d475d9a594dca8b1144597a9e69c70f20fdd20b4f", 0 ], [ "0000000000000c264f193e8d5099f2c20c08fdf9e5ca9006fb53778c0d8eb869", 0 ], [ "00000000000002adcce72a5cce517f1afc33c765927b77ccbce5cdc6f5f68e45", 0 ], [ "00000000b179a6096a58938311b3b8cc4479ccdf3909667a58598acc4ebd0192", 0 ], [ "000000004e86c06d23b8a4c20e6cb5a4c51cad24fca30e41695f8ad00852a88e", 0 ], [ "000000000bafa134d62d9df490ffdbc1f2b86b4373b86c079c5b730034aad214", 0 ], [ "00000000033e9b623ca1d89418114f63af55e042dafbfe97952e7a5fe7a3ebf4", 0 ], [ "000000000119025b6c9bbc3390708b1a77e85eda69fcb79666418ac2cb874a17", 0 ], [ "000000000000feafbf3a525a1dd7950fa53f7df1b0210e79337ce588d35a8b9a", 0 ], [ "0000000000007044088a1cc9ddc0c3779c0e156dee10fa15a760897ed4249f8f", 0 ], [ "000000000001a10e8b1ad577278f946252298b49b74ac9db70ea80c0a9c12db3", 0 ], [ "000000000001281354a7d86b3c750681283276c0bdde2b18c38d8354138ca4e1", 0 ], [ "0000000000000398b17fcd5d4d59ccb31d642f7b60c2a4d4d2aa7239ebc0efa9", 0 ], [ "00000000000021a571a2c475115fe723b593633efb85bf0ec0f7d67b780e70c3", 0 ], [ "00000000000002d1506c82becd7b480c85402d27f23a1248cfa128b7a8c009a6", 0 ], [ "00000000000001978f804f5cf8e4a0dc0c454fce0f0e2614510b8eae6e504b2e", 0 ], [ "00000000000001c4558889a43ac35208f502bccd9d38c741571723e9d79bcc26", 0 ], [ "000000000000005c782bbbc75358216e1ffc37973cd43a474b87dfbac4c61fab", 0 ], [ "0000000053bffe3e3db3672c5f050fa54239f93833ec5c38af92e83dec71a9fc", 0 ], [ "000000000001362fd5182f1cbfc1981937cd67ba54bc7b6d7f0a68f94e369f0a", 0 ], [ "000000000386ae84caa25e9dfa7816594b7c30a079e340bfcd951be2b5c092b2", 0 ], [ "0000000003cc09a351d647c0e12063d45b20e6f99c27c18ea62342b9d246581d", 0 ], [ "0000000002527c4756350bafee88786cd7ea27bc802f482c4e50cafc547ff9f7", 0 ], [ "00000000003d7288f44aa0b725af7816d2d333e118de12c390423d641139d5d5", 0 ], [ "000000000008c0a0fadcfbe27a880ce9c387425d3a2c6b06c1a599e4ce51ec92", 0 ], [ "00000000000158ab2486a8f1251c5c94502763ced9eb85847bb9d2eb476b515a", 0 ], [ "000000000000c817e5775378accf08412657e2557d2895df0fbb8475b5e190ba", 0 ], [ "00000000000078d59d08215b3aecdf0e0665d3a16ae1716e408df790a3566e72", 0 ], [ "0000000000002208404b39b95cc20845de19b47e05e8146146056d3d9bb382ae", 0 ], [ "0000000000000543e9315ca8b3b72bd3590f24535e4ddc6ccb1050b607777530", 0 ], [ "00000000000000abb8d3ffd3cc347cee5c092dde5355a7dc5d288036a28760fb", 0 ], [ "000000000000008bfbcce7d768df6f4610205dcb40173e8c4c417a2325487f34", 0 ], [ "00000000209e49391ad09577f87d1e0ffda27d2e749fd305c51692112627c99d", 0 ], [ "000000000005561eb4b2e0cb8107c81617284e7bcd7d390d16a3cd5925cf42a9", 0 ], [ "00000000006b24215c790a371bc18c53c83ff35e2c82d459bb6240cd9615dde5", 0 ], [ "0000000000af315d6fbde8488d68dbd055a56d79555ed32c3ad4d70286b4df2a", 0 ], [ "00000000019e49bc89fcabc4050521fb8835f926a62cc10b68e9618ffc117162", 0 ], [ "00000000009c0dcde4e694463245e8e5e45d2897e7fa67772ce0ef37094f3afd", 0 ], [ "000000000005efbda8c010f29a5b81606d186459047ce4b7eacde8d9659dce97", 0 ], [ "0000000000051c1655579a441a7f4d543c323d482405cf1d1250c3ccb665d426", 0 ], [ "0000000000007f13adadd1fc6462fbc5231425b81826af4e5f0cbb0de54a5b3a", 0 ], [ "00000000000011e00df09353fcb53766447279b96228da0525d769f33026bebb", 0 ], [ "0000000000002b91e6bb56015e0e60dc650a63666aa3943058e9641d4d679fa3", 0 ], [ "00000000000008e4d5fbcf207583267efff33e6c8d0a5fbdaa5704aeb674fe29", 0 ], [ "000000000000018aeeabcb422b5b0a46cf3a5f2458125c043c5781ffafeffbf9", 0 ], [ "000000000000004ca501cc9138ef5fef4b7b235682b81ab9719b3cf215e94f73", 0 ], [ "000000000000002b5bb1c4c43059575556a0ed10099ce5095f805d3d9ae10cab", 0 ], [ "00000000000000018523a377832f154b2b65142098bd18dc175273c92ec938c8", 0 ], [ "000000000000001f58a18e73b959b3fba53f697f78aedeb431d4b5df42cc2eb9", 0 ], [ "000000000000002df432cf9306f7eae841b8e5b7c137c5763fd4bfb46f8a309c", 0 ], [ "0000000000000004941ebdbe86526e806cd13ed226daafd0dce886bfa23d2352", 0 ], [ "000000000000000cef306e6a9eea2d7c83d051eab259bc3f6e985de5f4ac3d6a", 0 ], [ "00000000816ec1fe265e8abbf9f1de03498abf8cb6cda9d29a7ec6c8518524a8", 0 ], [ "00000000000011d74604be4f183ed34f00c15d7218834802163c2728d0338535", 0 ], [ "000000000762bc47cfbc6be9269fdd65dde20d2c88719ffd90a6f2945f7c6fe9", 0 ], [ "0000000000002db5f8794e7dae8b50458ecd05742d4d371123252e7472573619", 0 ], [ "000000000002b4f1a7ae7549fa44b1b320421aa1b59e1c5ca19b086873109677", 0 ], [ "00000000000047b8f650640a6c7ade46b2116b25e9e31138272ed319ea5b2844", 0 ], [ "0000000000001256a95ea8f9b361e7eabc372d62653e9eaa0dd7fabccc61af5e", 0 ], [ "00000000000035948053b0e71b73d618b490cf735780b470d55d96be66abd773", 0 ], [ "0000000000002e1c730a9822a12a74ad4891d1083ae398430520d835487c3dd8", 0 ], [ "00000000000014437f28476cbe0cb6637ae1615a20e661daa90bbfc291f00660", 0 ], [ "0000000000002f1c8dd72d46575a0c4c98a1197313ad1450c21b190086d40a89", 0 ], [ "00000000000008260b87de90b439d1e8e854eafa3c271bb9218994e9d903a779", 0 ], [ "00000000000003476bac3afdde7dbf55d6a974817a87ce6cf4b20564916bb48b", 0 ], [ "000000000000006b4e74497ddcfac98e9965bfc81a5087f5f091de0d9f5118f6", 0 ], [ "0000000000000033ded358c7074b4e19f90fe1db1f258cf6ed9fd0227923d09e", 0 ], [ "00000000000000048fc3781990e18e064e3d5f3d73bab1a199d0a6519f4eda1b", 0 ], [ "000000000000000e1871dc667a7e6596c15a320c1f2a9a81a784aa14c62f15f3", 0 ], [ "0000000043dcbf92d928170baccef4bafe45f5009ad6e8c7a4fbc924bf1e659b", 0 ], [ "00000000a5409910e5907b6b1db12c4bd8a8063e15f39e749488fa9c035de6e9", 0 ], [ "0000000000008c9975f8e4192fae850a9247e14e38638ec745dd42c230137728", 0 ], [ "0000000000003f0df4894e1939e7ab333536e1b71a06b676419e699210a9780a", 0 ], [ "0000000000003771556faa6de8495f58a1b1eee15abdc71f3fee10e03e72756e", 0 ], [ "0000000000000f9c4ee2d147531d9381bd7fb8140d4d7be0f8f058b4017133ab", 0 ], [ "0000000000001833e3116f1c5f9a1c5be36918c19d6f0850842f2b54b6e674d8", 0 ], [ "0000000000005c08d8ac7add24fb3a01857e68b0a806951986d0f412a9ada58a", 0 ], [ "0000000000001f70115a7353a76b574f844b9ca9d551d346c741c005cfe2a06c", 0 ], [ "00000000000028fb7a7dfb30d02e93afa2cd462c8b4a12b022036714c9e6d2f1", 0 ], [ "000000000000386fa93b299ece82b0faab9e169c3167c671517090a2aafb3825", 0 ], [ "00000000000002a47ca39571cb0a79edbcdbfac91f9297776c3c2a9d8deec299", 0 ], [ "00000000000001cc4e0aee1cc285d411eb1cb56ac4c8fe2978e63ade53607002", 0 ], [ "0000000000000016bb8db548039b254578f550bb702c66eed1c441ed7fbec8d3", 0 ], [ "000000000000000f0cecd63a2292a5cd53a542fba78ed0d6fd3d93e9f963bca2", 0 ], [ "000000000000001781f58897dbcf54dc50fc2c4e5c949090a79aecd98723608a", 0 ], [ "0000000000000011ec668d99fd0aacd40bfc8ccf7b364d4879248e8e628bc5b5", 0 ], [ "000000000000001a381d52991c224ed1c6d7c4c2ee763098e022d0c04eb78381", 0 ], [ "000000005b04a3f6f1e7f273875826c8538d9bcee2ce58a98e61ced5bc1fb902", 0 ], [ "0000000049e220ce8e607b6cd231aa1f6ba7758521195d8c60afb920900ed146", 0 ], [ "000000002467012239401cdfe357bf8d49f4bc74be65a3145925a230dfb360f1", 0 ], [ "000000000bb9e52739adc38fb6924c68ed1f1962a45e75aaa18066bb7700cfa6", 0 ], [ "000000000001e91216fc79f80d58182417dee38dc449e592328991a344079a0d", 0 ], [ "000000000000ae1204d3836b126e30685d7391787820e6f8481afa7f4891d88b", 0 ], [ "000000000005fb7034adda5521e1deacec2a95f6ce7f65df5123742f4350a633", 0 ], [ "00000000000006a1154387c23fd70c2cea8036dab861d51fdf687639b2881ef8", 0 ], [ "000000000000fde46c03685cff7a1eff85bcb8e1604577e0bca9e3dc1cf3690d", 0 ], [ "000000000000d85499ad0085b4c7b9960d28e542c1ff7d8422ccf2a94fbc33f5", 0 ], [ "00000000000013b7bc74a6565c5c0a7d32d21567623ae8e2d18b43d5cb3c9040", 0 ], [ "0000000000000f26070144a87fe5ebe3676f0e6a2a2eefbf6556401293baca89", 0 ], [ "0000000000000302522fb697dbe69844a8cdd7696faf16a5c8a43842c2a3bdce", 0 ], [ "00000000000000b34c0ff70cd3b532a9cb1633896d2e683aa53827c6e0f1a25b", 0 ], [ "0000000000000018e983ee8b65a40d3587cab91a4fa8d29b68353778a6b7f862", 0 ], [ "00000000982be2494520c626711cf47b5bfff996c7f74189f9a9898a96057b11", 0 ], [ "00000000c1379d510ab27bb0a267a32ccf5af9e698fde308635634515496b25b", 0 ], [ "00000000000183573830f2976678ca06a90570c40090b7cdb52b3d3940eabffe", 0 ], [ "0000000004799482beb4d1622b71685fd616c923ec99a91f2b6309195814194e", 0 ], [ "000000000009dddcccfbcc285ce93d763201404707b6ff30740bd8e508a411a9", 0 ], [ "0000000000bfc6f8a9569e69ec3b57a385d81e920a3ec84d0e97a070f27107af", 0 ], [ "00000000000022d8d77623449f408a6dec9e7fa847c08c8c246b049c15f9d054", 0 ], [ "000000000006daa9307e62107c6984c2b90dae469f1fd1bb156dd7681de0eade", 0 ], [ "000000000002cffd12d7d9867a7837ae6b45b383c74f0563ac5709e4eb28cbf5", 0 ], [ "0000000000008653ff3c6a0a517ba04729eb63a0ae60c7baa975a407fd561bbf", 0 ], [ "00000000000005e761b3a0236e409d3ac72dc993e9cc6835f3504e62b3786d5e", 0 ], [ "0000000000000bb93ca70e18034211414c6769524031248b7345401ff7dcdc6a", 0 ], [ "000000000000004f9e54d85a4de7e13a6e64b8145713d4402927196d6c788c01", 0 ], [ "00000000000000d19157da447106f9827f098b01f43810b3fd97eb13488599c9", 0 ], [ "0000000000004db0a4da49aef66b6b47c3d2fe28152c4eed5327f36a2e1049fc", 0 ], [ "0000000000657115d4dbbab98df8f80baa50f6906250c62d9d634a45f2512b9f", 0 ], [ "00000000000dca4d6732b889fb9b824a7b80a1f509f81c52a02805ad4b008184", 0 ], [ "0000000000cbbcd73925dafd962774ac5457ba8ed5b60966f19915e2a92d985a", 0 ], [ "00000000006b6eba960b8ff4e8a84b343167ae795a05d508b36261b36a0183fd", 0 ], [ "00000000007a284f086b9118b29d4f1bb80b36097f1676ed923caa96f86259f8", 0 ], [ "000000000002065b5c30c9149183ff50f298ae4afc718002cbbd0963b07b5747", 0 ], [ "000000000000e2b2be6cc8930626a1cca225656f4dfd4323bc114bff9381de4d", 0 ], [ "0000000000014bdabb06ea628423a845a974dc4f78b841cbfd68fac5b5ae4bff", 0 ], [ "000000000000075561bf4f468313206932c444c56f8a0bdfe7d786674f39195a", 0 ], [ "00000000000029bc154a17276deb51e57fa4c67be260879e9e90c4a99e484812", 0 ], [ "00000000000001df7d47715a9ee239c41ea4f6927fc10513fa8e196d030a7c48", 0 ], [ "0000000000000190df480d22883145a8137ca5364e6370181e481f363b2dd942", 0 ], [ "000000000000009f95d274cfccced7b153b6bdc7a1f479086aba1af3ee51e2d0", 0 ], [ "000000000000000c555ee275a7c75daeea5fc8c9cc3589ce8ffc485b0e2f9c84", 0 ], [ "000000000000156317725ec06f7396591ade0dff87ba7e5592ae7ca8922397c9", 0 ], [ "00000000004bd131ef3e9b59e54b2713dc907f11ef9bcc6bf3b0d7ad791500f7", 0 ], [ "0000000000c373a55ff87189465e900beb21621c91cfd99f6c303a78206c17ae", 0 ], [ "0000000000380431e66d9fd20f35080dd6078d0506716b8a5d7e39613a98403d", 0 ], [ "000000000025fc2f72b36008becacae888c81811238a88086b45b28c5c394067", 0 ], [ "000000000033b750e7cb47cdfe186e26d38c5c461e0d8395e489c0614056041d", 0 ], [ "00000000001331a7e5cdf0b13e176b4f9bdabe9e5b2db5356d59cb0dfe1f0e46", 0 ], [ "000000000006eebd9a33e800fe588dcd34bd363357b593f6808e397893c6028f", 0 ], [ "0000000000028315ddfff7fb95659327af8eb64092b7d130fbcbcaa784a3d4ec", 0 ], [ "00000000000029d270b7cabee53577c273d9c01c87c140e5343757d3953d4b63", 0 ], [ "0000000000000fef07c22f210c2b7ad92b06570cdad0c82e195835e0d0378aa4", 0 ], [ "00000000000002b56610f192a962de55e7060686e269e3cbea07c55eaf0ce120", 0 ], [ "00000000000003c962e2f34b9e525dec519a6393abe3f17db6af189455cd7baa", 0 ], [ "0000000000000057e6976dff338a43dfa1925a5a43e97de23f01b96eb0b839af", 0 ], [ "000000000000002db1fe2ead78cdd2d14392b5deea255c363b5e734adc9b467b", 0 ], [ "0000000000000019b25e3df2e045927729efd35896654add081f6aaeebd71f47", 0 ], [ "00000000000000063c34230877c91acd3017621542e7ba4d9b7ef64d0b5cbe93", 0 ], [ "0000000000000028e023aee554cec2607f92c29e99611eb14736e2347e7bf42c", 0 ], [ "00000000000000369d46aac842d5505dba93ade92d5d3be1157e00ae5c047cb5", 0 ], [ "000000000000002077102f09a5563275c1efdbea9f6395f5146f1d6037970d7b", 0 ], [ "00000000fe37a092e270e704da91edd5d7c4234766916e878c8b0a02ef64870b", 0 ], [ "000000000039588e49d18e9ef322feab3d3cb6d14893262060890a9331b32f73", 0 ], [ "00000000006613c34279b3c58d8d54667ae590630aef080faa50f07ccedd3bfe", 0 ], [ "0000000000869e7bc5a3b34ebc2729be68c77c712317e79af584564555957b8a", 0 ], [ "000000000009637269a88dd5ac43497b8f27ae313211a3061470e8137d555e59", 0 ], [ "0000000000004a8598c4be94b043e7fbc600226fcd7547b83b64020d09a61fc4", 0 ], [ "000000000000100695b5f09c3a1e8c29fbe67df508481578d2948258a04d0fdd", 0 ], [ "000000000001bd1d57b3da64d209230bd6e314724b25a9b850e6181f49a6cd03", 0 ], [ "000000000000d00e36399e3a40021ac0a6adfe27973edff1ff66d6420df5a98a", 0 ], [ "000000000000a63aa2dcd75bf8fbccd35e3f00b684df023adbe2785eb4d8de82", 0 ], [ "000000000000308579c854b0c09965e609ba1c63f109f98e7cab1be5cbf364b3", 0 ], [ "00000000000000dba13892fba29915fb44c60e1a179d0022ca33e94c63a158bb", 0 ], [ "0000000000000384bfe36508812b0068cdf46b0cf1942db32cdc17a7f5dba83f", 0 ], [ "00000000000000f2916cb59cb479f2edcf2c2fcdee3547e772b904db17fc6d93", 0 ], [ "00000000000000313b0f9b9b90f1dc0194a5d54abe389817e195501274970183", 0 ], [ "0000000000000028d70cea4d1f3e5f3601b8c72c7e844f434972b2dcc343d3b2", 0 ], [ "000000000000007514deae00c442bc8c8121f2a943083c45a223ebdef390bc39", 0 ], [ "0000000096309ddb64473b6a32ef5814b5b197afd1de684c25b9978abb17d5bd", 0 ], [ "000000002201773b4ba28e91606a3925ce89fd0558a4d016565b6468d4966184", 0 ], [ "0000000000001cd5ff5078c2311f2f9ee10089d2630029c07febd67016245583", 0 ], [ "00000000001b93e7b197099b50a98462f56421e91533bbb5a5e6316d7748d271", 0 ], [ "00000000000001d5b12737375e9a24d85e35425534b80eb33f1f6067a3c9ee9e", 0 ], [ "000000000000a528259f38acd4ff2e48aa80c51e8d32d92ef22ac0319b7a4123", 0 ], [ "0000000000001d7a4a2d35e55b00c097a4e3cfc78b27acec46fc35e14ec3e71d", 0 ], [ "0000000000017069a0165b85c9a568d821ce1a23aa87fc32730ff2054d4e147c", 0 ], [ "00000000000163737ba8bd06de10bfd9610f897832edd1b9c00986b8db41e294", 0 ], [ "0000000000000f2a5f2ec3414e50228ea1a647b12b9af39005299c318adfb469", 0 ], [ "0000000000002ee72d7899719ed3a4f101a4bacb5a341e5de28354b668e5f534", 0 ], [ "00000000000000855dac9ef2f084ae3ba0609977b2d47ca7174ecc1219866c20", 0 ], [ "0000000000002037546232e2809ff4f0ea5416827c134afe7107d74a4939fc68", 0 ], [ "000000007b4f678d7e1d8b9653e1761e05d464eec7da2e65d5d3f44db1853c55", 0 ], [ "000000000000635cc05816a3b7d24b63cabcbfc4debbfa5410df25cf5c618351", 0 ], [ "00000000000064fa9ff8ad2346da5d75e029ee1d2c3d9499b2656c6a6d562c91", 0 ], [ "000000000000a1b3bcce8190963b2d801579b0f1fed6129ad4e21a223f52357f", 0 ], [ "0000000000005a78a3d8d9f33b6ece86970c75fb3b608125078d61c77bbc0788", 0 ], [ "000000000000d48d0c9e7933b8749939110764bc1a827f0242c43233af5512db", 0 ], [ "0000000000001725a132f59b477a352d478e0f6049f295fc609eed2f5e4ca3f1", 0 ], [ "000000000000e6308f8f026cb8b44bb0d4b15d2710446c7f3719662fe7aa5138", 0 ], [ "000000000000edd1062f75807cc0de78a3e19fa07763e23f744d544b1d5cce0f", 0 ], [ "000000000000217223dd962210aaf8ce96de32318adf53387ebf53f3b2b30539", 0 ], [ "000000000000021e3df79714b32722b720f916a9fbbf9102e7d753a7f242d8b1", 0 ], [ "00000000000002c7dc3f4aca17976008f1a3f2547bcb15b3a138af988f3e4f2d", 0 ], [ "00000000000000398aaeea76bd1c64e573b103ed06c11bc954e74c4c0b437add", 0 ], [ "000000000000001c1bf7b854488eee158545d3216e54ef714b83471d3a73c48e", 0 ], [ "000000000000002784b2be9801c004cefaf2b95f7626b19bec5aac1dd514fdf6", 0 ], [ "000000008e2cab9ea92cb5ef0416b8cbf824b37596d3a5b60406f1b3028f7faa", 0 ], [ "00000000104964a57ca5cbaa3008047f78705c4113033c750409a4b259936ba3", 0 ], [ "000000002f524bc577bcce253fde6910eba3ef06d15dd27232131b5e4345fe47", 0 ], [ "0000000000052c7116b0b643643af581ddca810363916a440bda2ace38add369", 0 ], [ "000000000000f00137a5682f26cd18abf5c6f3ef61f910c59a59d798477cb152", 0 ], [ "000000000002a0894c9aec54bfdcdb4a064e4507a6de62fa990177980ae17fde", 0 ], [ "000000000000be2b65c45a2b5d56f1be99f2975b1a9a52d27ace45f5928c7020", 0 ], [ "00000000000057d1aa7d139b6f28389f9f00682367e11cd6e179db6d78c21c5a", 0 ], [ "0000000000006e312b5a03b58c2ce0ea1f4e575290499e3e8628e55f57819810", 0 ], [ "0000000000009679b04fec87a5ef0cf0ceb8d799a6d01d5c87c2fd24c29468cd", 0 ], [ "00000000000026a1b24da68760401f5ae058d964a1830f8823c7958d6f0cb115", 0 ], [ "0000000000000ac34da5d627b47fc7f7b25599081a8b187ae52d70f05db9bcb6", 0 ], [ "000000000000025312bb3c989c7bff8448905d7e591c6d3cb16d3a560e632157", 0 ], [ "000000000000005e28077829e782ff8df6e9b907bb9df6493e4462d8285ea9e9", 0 ], [ "00000000000000498293c5ba0eea98e1bc9bc21178cb13033a11f8b49f5e774f", 0 ], [ "000000000000387bb87b8e7873d04bf3c2f70af050336a8536ad3f7735119f58", 0 ], [ "0000000041190a125657d94ea0ea26b2238b7a68f5eae7af62eaf902ac585923", 0 ], [ "000000000045beda231f53f748d8d0a0adcb0090603945f5664273e5f28e4c6e", 0 ], [ "00000000000c12330af00f371874b47f1f29a7d9bbb89f5d0a1e3a2dd53eaeb2", 0 ], [ "0000000001e172477961534ad79703de1bef9d7b11b27417c19dc4a8a1f3acf6", 0 ], [ "000000000022c7e8bfb93d5a5289cddd8d3083699997533f9f74cfe634f71f71", 0 ], [ "000000000024222a6945a270088ba37d36c48661d153c85676f87a879bdfe080", 0 ], [ "0000000000020caf59c7f3798fb533287568e2c5924b2cc5dfcb1da89786879d", 0 ], [ "000000000001a5f0697c842dd77697934e30bd8798c4a4aff1b7441dc6f23dca", 0 ], [ "0000000000000ba524c5e6b6bf1262f8ab7d9f74879bb41df26f1f5e5d13b4de", 0 ], [ "00000000000005f7c7e2f10a0f7d9d371a350ee6e6fb567b5ba6e7a43a9a2bf3", 0 ], [ "000000000000060cec749aeb779875b1be98f34ddcf541454cf4f1aff8e5d998", 0 ], [ "00000000000001f8ba23177cda6b436e7a2301e211fa35198854077254abeed9", 0 ], [ "0000000000000067d5b94b837fd148ab7eed397d1c0acd4599d7a8880c60e83c", 0 ], [ "000000001620cde7948f7134c978193490736b7ed19effdceeb142ac5c60c4f7", 0 ], [ "0000000000010b0daf2238423537f93efeabece0cba64ad3b5f8ac73c7382a99", 0 ], [ "000000000005b0676f677edb2b6710516e06bba963c09dafbfc2aac314126423", 0 ], [ "000000000007ccc73eca4f241bc1ff75302682c9fdb0939b6f706b9da00a52a9", 0 ], [ "00000000000a2a1678999a3493f1d603901e35518d90b3e739ce6daf127ebe26", 0 ], [ "000000000003b394dc98407ee9fd88e4f4e22507a788f915dcf72cc34c87cadb", 0 ], [ "00000000000351a1e8062b8fe34889239ba38ca52b3f7e44c7122fc1f0fe6e6e", 0 ], [ "000000000000df60569c57f26ca8796e636eed1357c8f0369d0821c0919ca7bc", 0 ], [ "00000000000087b5af3cfb531ded71bda8932cc80a453f22ead0bf93f50b08e0", 0 ], [ "00000000000057424fa933cffa9bd1116892a76ac912f2c8ee7313b2bdee3351", 0 ], [ "00000000000015c3e9e26653d1ef02aaa6d024489f90e83588f5f9ebf28d7b63", 0 ], [ "00000000000009016aa385782a3ec788fb22b41412855c89aeb73147743a6f19", 0 ], [ "00000000000001317fe059d7442ae8afc5aebccaf4d37ed31813a5caa3638c87", 0 ], [ "000000000000001d8f256752a735a1de33b92a928052f71b738d19ff366db867", 0 ], [ "000000000000001e9cf12031d4ab136ffd2fbc280607c4f996685d4aef460a11", 0 ], [ "00000000000000211e7a6e837358cae29446cb9c6dd5aa15c2284b5314f3bf46", 0 ], [ "0000000067af625d738843195c7dbf37d509f859a1875ae674e9a1dc8ad89e0a", 0 ], [ "00000000bba6b47cc6dcf36f0a6e33c7c60e9bb23ee1f72f62cb4e90d7f04332", 0 ], [ "0000000000041dde2cdeb55e44cc00c8d4c1f448c9220ed2c0153de7a2d55779", 0 ], [ "000000000000288c3e28aed36614437d861224fe8ccaf182e727b3e1fe1d633d", 0 ], [ "0000000000047b66b2cf5b617d9e7ece2ea2be7f886eca7e7f8f831a4c71a8e4", 0 ], [ "0000000000007a9fa498e65a666bd261e9a5ed00e6c3026e7916554841bea631", 0 ], [ "000000000006b245a6d974176dde00529110ea44bcbe8f78c567fea3e47a3d78", 0 ], [ "000000000002e1faeafaceacace03930ea6ea7d8d42ea4cbf141c2b578f3128b", 0 ], [ "00000000000167a7dbf143b59eaf118c2eeb5556061758b56981a4cbc6c7a08e", 0 ], [ "000000000000a352cbb22a1652956aa5d66610b696a19118b933048538192d2b", 0 ], [ "00000000000002c418d64c823e2ec8c77220717c0c7fe4e34b25afc4c0bad5e3", 0 ], [ "0000000000000d7373e7a596fa105a5d1b7dfddfbc274d45c864c724f0b9a2fc", 0 ], [ "00000000000002c3c89de3acbb14f5e38a6f5c3aed34d4c6c45b2222fc0fe3ab", 0 ], [ "000000000000001178732537c78ae21bbe8f9fed898f3b2c63692b9c93aba4e9", 0 ], [ "00000000dd9c07faaa65b8ce71e266699422567278b94487e9ebe4227d1ef2d9", 0 ], [ "00000000d226cc764f56ca5ec5a62562cdfc1bf3a4435350f0a27afdc5f94a79", 0 ], [ "0000000000009baebc276aeb84b5ccf3fd7aa95efc67c0f982bdfb084e40e9df", 0 ], [ "0000000000005d841db70c88b40085e6003fa220b0c91a810e04fb010e7f84e1", 0 ], [ "0000000000004a5013716cac6fe9e3d3c91b5688463feca69fc90045f00e2a17", 0 ], [ "000000000000cedd7a99d53af44ec7144316eec8aa0b04164f2618d090fa32fa", 0 ], [ "000000000000e311840eae32a946c81d8ed9a7fd5d1788444b5376a56def23df", 0 ], [ "000000000000c3babf9150364160f0bc91c0cafbf63849cf70a29574ed12751f", 0 ], [ "000000000000521dd5efaa7a298cecdd047c6ed85f981ff9185763beb9519c49", 0 ], [ "00000000000092695a2324a45f524b51ab700b458e5995790522f9d4002e374b", 0 ], [ "0000000000000925141aa9c81dd875e55a986bcec38a9ad9e627c81ee6437a88", 0 ], [ "0000000000000ae07f7535851cb685259a447d1ad5d3206fc4ee3693bb7421a3", 0 ], [ "00000000000000d41e23f89aad486957071e016d836382605770b65d7539d161", 0 ], [ "0000000000000016b6843174d892b13a0fa39cf807879b56b723075de7492118", 0 ], [ "000000000000001a7c215c98d09179f0558b18f6987150cfcf5e57afca65b98a", 0 ], [ "0000000000000027150d0a4ebf9001e210ebed81ab239535aca8fb5a489a1ead", 0 ], [ "00000000000000312d9a4fbd4bbde5e3f2266047e65f9a5e84474d62afea0514", 0 ], [ "00000000f82b6cc148557cb060b8cc3d697e38250630bc2e7188ad4500291b5e", 0 ], [ "0000000083ccf3997bad3cb32d0e46ff7875a0f454a3c48c2ff910d010801ad2", 0 ], [ "000000000000f40bd4d7c3d374a984d0c8a744c3816b713e8f43b9dcf75f7848", 0 ], [ "0000000000002c0374e865c02fed16da853c3086a28b0c212591469c95a71205", 0 ], [ "0000000000008301f16f29f38442d1b1f521650eba2382ea1e0055a291ca6422", 0 ], [ "000000000000a954b023180407c904341edba6375a982627b0724afc56f16505", 0 ], [ "000000000000c59c851bff2090533bc3009ae76cb1ee89e247dfaf11a5c77e7f", 0 ], [ "000000000000d779b30aab849a7f8a3af7f283bb95579d2d05714b6c3aeed955", 0 ], [ "00000000000016bca79f9de99fd3d0399f812f0fd5be4f84bd7ee442b846498f", 0 ], [ "000000000000dfdeb639143c64a17b99b2025a7d9bbb53e993ceb7c1656ceac1", 0 ], [ "00000000000023e3d31bc565be6041cad487f77bb23109aeaa804d72bb22d6df", 0 ], [ "00000000000007ebb67ae92e144382f52aebbc63a4604c8a07bfebfcf8a19546", 0 ], [ "0000000000000136c4a1582c01a5824f4fde4ddf91d653899b53994c4da9a3e1", 0 ], [ "000000000000001c197b662a51a6b9d5a4eb9521bc52c82f06aee07e8b58f47a", 0 ], [ "00000000ff1ae8e1ad7dc6a82747e125d99e099969d5fff2f193246529b225c9", 0 ], [ "0000000048af658629f65a2ef6052fc8cca3234f33d3fc329a0a2b4a73fadefc", 0 ], [ "0000000000008cef9b1eb41f402fa0f1f6a1ff6641ef3484d7decd9ceb7f1efe", 0 ], [ "0000000000003ff9199a773f976eda5420300f1f42f213d9d793ca002c17a5fe", 0 ], [ "0000000000001c6f16503cf6ce37c09f31eceac19e9a46eda67061bec5c6abac", 0 ], [ "0000000000002ee7040adca7f697117c59b8df1dc65519e3145d720b01add98e", 0 ], [ "0000000000009718224588a74633c646a7539d05ed503064e38f7734b146be9e", 0 ], [ "00000000000046fd759769b3296aa5636c3d113a309d633743e092b463072842", 0 ], [ "0000000000003594adc1bf018bbb36c059ac293164d83357817eb9b7b3ea320a", 0 ], [ "00000000000069f72a110745c74ead6d9f488906ee79cd2e6dfaa77f9371c300", 0 ], [ "0000000000001763cf5fffd8e1b0122a7d4bb0e1c2cb17bdf22fa25b70fa6e49", 0 ], [ "0000000000000c91abea6900d1ee2c168bc34abef1260776a164caecaaf283db", 0 ], [ "000000000000034624e683ff5df51b1a78fe027c67633c77258d3b1fca48a124", 0 ], [ "00000000000000b8afa5359b5cc460a77b047cbf8b1aaf640b8779c036c1cc77", 0 ], [ "000000000000001733362d084551627b7a71cf84b9365d4a8b1131d8e1f0fae9", 0 ], [ "000000000000001c3cd1aef22fb58f8917110976d342a0573aed0a466702adca", 0 ], [ "0000000000000015a4454fac29770dce9fc9786152a68807322ef00d74da0640", 0 ], [ "0000000000000005628d26b0af6507d1218a6665e8e6867ab37b84a69ba15cd8", 0 ], [ "000000000000001551c40d57827ecb548a09f2512ab66a3d3dd86f00f8083ae1", 0 ], [ "0000000000000014b20ef449f75f3c015bcef0e19d302e440f9ecb4c183300dc", 0 ], [ "000000000000000e814b868e01fe7a4a78d966e4ef73f5293c633005ef5718dc", 0 ], [ "000000000000001e4d4ba22dad9356f9753b1065765799c080807a075964f8a8", 0 ], [ "000000000000000c2726580d5aaf194818abcb0dc9275266fa6604b792dcc41c", 0 ], [ "000000000000002660dbfdb21d80939f5341395eacbd2edd67a61abd58044345", 0 ], [ "0000000000000027fb8a498f621a696f6ff1d9b45839940a0a45700f2e211bc5", 0 ], [ "000000000000001d3fc884a35029348110a0af44cde5f299c89a89b2f5e3c1e3", 0 ], [ "000000000000000edcb8421d37f2d46963dcd2ab31a0359574b49918627c4772", 0 ], [ "00000000000000148683ea86525485d22e85bda982e8682f5010865ca3ff3da2", 0 ], [ "000000000000000329cf100ca7c05279430275cedc3f4573dce6ac1d418b7734", 0 ], [ "00000000000000054a4bd09b4cf258f0bdd86feb97fdc38d66753f3e04a70524", 0 ], [ "000000000000000faf34df569486fe78f0b17236e2e855a507e5d36759b95751", 0 ], [ "0000000000000008c72a42e89efffa7da953b94c2217f50328d23a098933d6cc", 0 ], [ "0000000000000db7c34e4a5e1e2445b7f4e07231e5736251103a1d1dc5b5943b", 0 ], [ "000000007795c87d221390511e079257789f1563bcc772571e4481ca3b448832", 0 ], [ "0000000004e5b3b03b0223706a7ce40d576d64026784c2ff8614e6562f4d018c", 0 ], [ "00000000004a0ea723460162104583b750bea77bac8c967d801c11f5058ccc04", 0 ], [ "0000000000000ce34ac861ba5d727e47e9e19cf0db6df7926ad1871e9b94066e", 0 ], [ "0000000000001ac9fe6e55ac14cb60f3bc8e2bbd3e35b86a39a64aaa8b71ca54", 0 ], [ "0000000000001673268e6db4c8f163bfe25914d4bd50673da5715d8313b7d191", 0 ], [ "000000000000068296732b209e33279ba86496a74bff278c6b66e8004c8d29c5", 0 ], [ "00000000000091c08da0ddc2340050cac7b30bc5c1c83dd3e7dedf5b08dc3078", 0 ], [ "000000000000770a2e7934ceffff83c29b049fa202759b4faf4598fc0fa67ea3", 0 ], [ "0000000000001b5d77a10cd4842db233e04c268e57e3b2669aea7701da12adf1", 0 ], [ "0000000000000182a40214f1c538f5a44b696d630664456aeaab29264a6f184b", 0 ], [ "00000000000001245d616ac4b49515cfed74ba0ff4b7e8934bf18848075937b9", 0 ], [ "00000000000000321a47c18d57d5d8d058bfecb43cc19af593359a66851fc605", 0 ], [ "00000000000000231e08d8080db64b437e37f17295c82b8561e528829970e9b2", 0 ], [ "000000000000000fb903e2942029e3a65aa8d26efb48aa8537aafb3314ff6d60", 0 ], [ "000000000000000109729ddaea18bfb7a1f3dda86e11332946cc34d27a988420", 0 ], [ "00000000000019477d9b273423ac45be472ac63d88616e5169efe9e3bdb03fb9", 0 ], [ "00000000ea0c1249ac03ea5667ca8ff4f327468d873b6a9bb78206e9c3b8cd63", 0 ], [ "000000000006321457285a8cca551c77825721f502b83ca06972d697c9e5ce1f", 0 ], [ "00000000000214094cbb0ffedab25fb82fcb3db22ee6031e6b952b73fbacfedb", 0 ], [ "00000000000541d2b25e067f1707b06d91f19351b84d41aae164ad41facec281", 0 ], [ "00000000000164bec334a2c1f79e4af9dce78d838d573b90fc5acd16f544b3da", 0 ], [ "0000000000030c337a7f2bde97cbf6a7c71a0b4d24e1e4600e46eac009069221", 0 ], [ "00000000000555a5e32651e24a8b83f4aadfd689ba44d49e63329bd2ed484078", 0 ], [ "00000000000207e99a06cfc158f5d7d8cb27234cab15ab00fec24fd8b8956aa4", 0 ], [ "000000000000c819f87342c54f0d4970443264a296a88bf38848ef1bcefa05af", 0 ], [ "0000000000001838f689e4d297db56c3d50753378cb2458dbb5f4392ea90c585", 0 ], [ "0000000000000996e7334726403dcb4d2818096f645f6782222e0c9fa8ab366d", 0 ], [ "00000000000002502c65e6a2ea56681d90d9cc6e774929a6ac17c1adcb0c6aeb", 0 ], [ "00000000000000f2b4ec952983f6e68cb3a46d34738c38a958de3ced1da54e42", 0 ], [ "00000000ab4d311932bee7b754ad97e081cc24b0225d3089dae7440fc084c623", 0 ], [ "000000001104264440198251f8c9046dca4a3ffbacbc48be03baf996c3a48094", 0 ], [ "0000000000023cb28ed8ef3637a80e73ec92ea7210088053002be02818fb9f98", 0 ], [ "0000000000002bb738283a319f1f1b8bbe365b7f6f295982c29836d193808f6f", 0 ], [ "0000000000001a15e8f691a8bc14f3d694abef473c5818017ed391615b45fa39", 0 ], [ "0000000000733fe4dca7d1dae0a748bbd9b3b99e687b97281b806a199e35ca54", 0 ], [ "00000000002636ed79a1d367fefb464aa532b26dbbe8687ffa5d5f26eeee06dd", 0 ], [ "000000000006f45c16402f05d9075db49d3571cf5273cf4cbeaa2aa295f7c833", 0 ], [ "000000000000dcaf9949232b5cf92cc2976ee59521f93ec2197a9c762c6a3c54", 0 ], [ "0000000000006c7706b78284427681abcb62c432adc364975590f3b33d94b773", 0 ], [ "0000000000000efdfdbe08218cf961d25cc1956d464a9f067f25545301b79222", 0 ], [ "00000000000009ca0d54316bdadcbaa48c53fab88b9fb0556472ed9e91751602", 0 ], [ "00000000000002395eb7f05c543dbbc241cd4b5d64b3c948f64d8ac2083b197c", 0 ], [ "00000000000000e9883c7e7284799cdb9ff4e4208a313050a182172c950f7e73", 0 ], [ "0000000000000010b95f7caba61a90def8a4b527ce0574092718f478af780c10", 0 ], [ "000000000000001fa1eeb7f86389fada9fa3e05a8e497c23a08ed6a1dda63a8b", 0 ], [ "000000000000001086a413f0cebe3ab3aad747f6b34a5038856592675f4efef9", 0 ], [ "0000000000000028a8f135e691760cbe5c4b7a0d88a4318b8fb353aca220025e", 0 ], [ "000000000000002460d6c79c6e2cde645eec7b64d4fd48a1c71d756cc9c3dcb1", 0 ], [ "00000000000cae97fda2191050892ea192e7c15881773b17e3bc1331822fe4bc", 0 ], [ "00000000002d5f11ec72e37b756a94058c299fc647d1603efd6067e20c24d306", 0 ], [ "0000000000035d32b34aa575bbf8420ca6caa6646840539c47d842c8b6700771", 0 ], [ "00000000000038ea640ea920adb8ac40cccdc56ada27cb52e3ae06ba0580572d", 0 ], [ "000000000063dfc0373ccbd4d400980fe603c0af468d21d3ff8eb568b480ea65", 0 ], [ "000000000000a46891a576d73cad07d20f3ad9308657bc676b12a4f066915ce2", 0 ], [ "00000000000d58b8cf55bd312e564ec6d2959162b546dba8849b0e4e0dae37a2", 0 ], [ "000000000001e72b87fa955829ec8dc21878f11db8181c7475e2d03c79bd0e13", 0 ], [ "00000000000032848796aec72da3f9dc0ed83ccb99023e9afafd7f3d9bdf7103", 0 ], [ "0000000000002e93d090b9aa138cd12a5362e0cd4232a3b96561fa7bf280a103", 0 ], [ "0000000000000dbc7b9754521c68f2553265437d589fb6b2615dfe4d960ad690", 0 ], [ "0000000000000fe4edb18d30fbc59db0c34a39b33c878108825a7d8df4d99b2f", 0 ], [ "000000000000003fe4956957ad4d5c19c79613f9840ab51bb841da535b449861", 0 ], [ "00000000000000fd3eef340a6953ffc756eab83dbe091bda721387f7249835c6", 0 ], [ "0000000000000062e7ccfc5414355a0a1b5151b496d1b77abe7606920c0f5251", 0 ], [ "000000000000001f6d8dc4976552a596eff2eb0df15b0d9ee61a55091a2050c2", 0 ], [ "0000000000000018a7ce07d0ac46c3eebd72bd2db0db627675e146fcf9278e4e", 0 ], [ "000000000000000be86cb1664031f8666023b52d247063327613b00619f66514", 0 ], [ "000000000000000c1589a255f9fe686ee448e8bf60424529c1f842b71bb317c6", 0 ], [ "000000000000000a2d14a86314974abad5934ed38f63276fec039d636f33d652", 0 ], [ "000000000000001737d639951d593f9d269bafda7d5fe5a667d41f8d8d2c9cfe", 0 ], [ "00000000000000033f81ecaba0452707d61b7e76d1b18809b20db18de30f3b00", 0 ], [ "000000007f31278326e2cf458be3fbc904d4f98f3b348c8f2f3042d590fe2ddd", 0 ], [ "0000000000018ca40c2e36e0d484b57d41714c2bf5bf69ab06eb214c252d63f3", 0 ], [ "0000000000026de8f53cc5ecf634c484105891ef12e8abfc3e83d750c07d89b5", 0 ], [ "00000000000d340049196286b501419b72c66bc6a45ad177690e0ff641c6418b", 0 ], [ "00000000000e70c72347a1043a7e1fd35483242689618a5576fdac0845dc96a0", 0 ], [ "000000000005aba9cf40fb0f07b3226bddeab8b997de4e827ff9d9f7198f7ffd", 0 ], [ "00000000000c1c2b6e4b064438ecedc3028edb111fa571062d62f84912f407c0", 0 ], [ "0000000000016e1ed29108b0224b172a3247f61b7589526566cc62ac69ce9b5e", 0 ], [ "000000000000f0e2e3a76f57f843ef8823216e1789c9cfb97f17f87b719a22fd", 0 ], [ "00000000000022998f49c0c1a4519125272270b9d59857f0d302c76017171c84", 0 ], [ "00000000000008588b178655052953dc2eccf8c0a0648f15c1d5ceeedda91372", 0 ], [ "000000002cc217b3217e3016d6e6eba2584619caacfe944342ae905fe24c87ef", 0 ], [ "00000000000030120f45e78e7852729d6925cdb3c5dbc87eb8f167674e722bf3", 0 ], [ "000000000000355aff4c8c416f3ec39b77071a6614cf604c213f3e52a405a221", 0 ], [ "000000001d5087abd326f27fcff310c58999d8881e5bf0cf30c826177680508e", 0 ], [ "0000000000002393b75ee9ce1a0433cbefb3fad205406fab65bbc0a61a757149", 0 ], [ "000000000000c57b6c1be1998660d54eb13f10ba3371cb735859dbb7c4396799", 0 ], [ "000000000000bbe163a8a5f68e9ca2c99bf9af0a9fbecad4170384a0bf907b26", 0 ], [ "0000000000005d2078380dd0389664a0de79dd7c6a8c1b94e0522b0bec15f74d", 0 ], [ "00000000000029b25af79b8d27068c32865d2a41817fd1a34586ee280c7455ad", 0 ], [ "0000000000003af3be1fa30b8c225a0b9061aa07e3389cb44161d11b15ee59b5", 0 ], [ "00000000000006450af5e500b7a650da7a2043c7f3936e3aa93cd08c22457341", 0 ], [ "000000000000a012ef365be0880e3d7ba88a9a8378429733d608c6cb6d7fe59d", 0 ], [ "00000000b3a9e0680e5ecae9a639b6ccb19456c59eb66ea1b53495f8cb579874", 0 ], [ "00000000015eb6c7abe02adb992d359926f511d9830c6a18b8cfb8b3bec85cc3", 0 ], [ "000000007699589244ef2cc09903aba91032e7e86d4e773ef639ff0150db2fe7", 0 ], [ "000000000000fe9483a6e60c2589355742d93b30406a2d273e10ec58558cf34c", 0 ], [ "000000000000b02a657620c34404936792268e4453882f203ae7add74573083b", 0 ], [ "0000000000002c7630b21c1ec243de251dcee944ef0ec3cd9d559c34f1fa7d4f", 0 ], [ "0000000000018c93ec3fde56ceea4839e6d08856aca055508c562d96e16dbd71", 0 ], [ "00000000000015db7b745e849b5a05399ef66a96d31809fcd79556ed7479965d", 0 ], [ "00000000776cbc7a008bdfa21270d6f18c58d8a794dbf7355cc71ea8a0c8c063", 0 ], [ "0000000000002ffc1ec2f0f2a757d589e452794b76e50366a452ad3d318f15c2", 0 ], [ "0000000000002d727d581f7f0a93b74f5585e62dcc6fdc9cc3b8b19eff10f700", 0 ], [ "00000000000010bb6ada118d713af3ca721db8622bf222477c3d208f8b3061c9", 0 ], [ "000000000000e03cf9e4498fba163d2bcd2646991441169481b940d291fd075f", 0 ], [ "00000000000026e1da1de58906d3d12221a79250f10abb77623a230c8a62fdea", 0 ], [ "0000000000002475dbf8647609a128ac7211c4ca7b728a989f1b5481e626a328", 0 ], [ "0000000000001f2e545dd92ec4a9410e3d6a5bf11a6bccf97f049310538ded2f", 0 ], [ "000000000000ac4ecb8bc7ff25f1aa9633a9cfa28d0bef19870c51e0a9431f30", 0 ], [ "0000000000003c493ecc2121c5c3bdca88a141444b2631d6a9b720a504bf455a", 0 ], [ "00000000ad4ae3258584eca0ac04c31af2ea25d1d2b811279994c017136d160c", 0 ], [ "0000000000edeb17c5a1443fbd6f9f9ff20a1691bde5fa42ced3fbc6413a5e01", 0 ], [ "000000000b61f092a5c1660d0f8f6fb3294c9b065c4ef2943a2955a2d03c3708", 0 ], [ "0000000000000ece5c82d89b13143a1a28db9b12bac561928c0a1d709faeb479", 0 ], [ "000000000000000929103fb8a1aa3f8215c61e990f02cc6085627a6cbe197e00", 0 ], [ "00000000000002c9b7ef56519aeb047fa614198081393923ce8f78db40e7ff43", 0 ], [ "00000000534f7159b8c190680b93775e91460d5203160789615a92e821e3beca", 0 ], [ "0000000000003d1a6c7cf11ac53c59a12fe127eb9c2058cc6a2e5229136c8c3a", 0 ], [ "0000000000001749960590ed08ebd150823d21b32020ad8b219adce32e7344c8", 0 ], [ "0000000000003384f9d5b10cb0321baf1a99b5f37458155ab794649b12eec2a4", 0 ], [ "0000000000000bd9def7511faf3303d889daae01de92f5716a6409c44f9ccdba", 0 ], [ "00000000000014b324e5afdc4b5899942a9bc776d9cb00ec2af5b795c3a74fda", 0 ], [ "000000000000415e1f654f779786094eab0dc703010b3f686baf7defb83343d0", 0 ], [ "0000000000001c9b4a7c7e351e10f47852dc9d5c6b7c7c4518591fb6ddaab3a0", 0 ], [ "00000000000052b7b9fea051b3fe3244d1b86516aaea33f2d5a55e977fe9c026", 0 ], [ "00000000293536c90c630e207de478560eaaac89ae8afc33333aa7963dd8b7b2", 0 ], [ "000000001a45d126188b331059d04a98df3588887adf6a2b520225a2a2b03567", 0 ], [ "000000000010a96f7c94770392a3fb38777d9d75ff755b5043919475ac396121", 0 ], [ "0000000000007e19b090529e49795e4c115c7f00d327da0568fec93d542ec878", 0 ], [ "00000000000016a2c2a305974b890779ed07afecdc45e9de20397f52a88efe36", 0 ], [ "00000000001588ca17c8d2f9051d75f085daaa519b85c7e25d14b5871c4cf25a", 0 ], [ "0000000000000e31ba2c3036fb984c5faf3e2860e41799aedd75b469287e8f34", 0 ], [ "0000000000003236b8dea540661d16c62aed83879951331d7ce97290a761006a", 0 ], [ "0000000000001f30b54b080cec64d819fb2270932883cc048b7e614ff3405b1f", 0 ], [ "0000000000002d5f3ebb70abd3119117802b2878af6fcefb58f43794a152b6fe", 0 ], [ "00000000000001ba1b76793d0ef69a1cf35b8af6421daba0224db06bacafbc1e", 0 ], [ "0000000071289acecc04f86e0d71e4dd9af80c1d62d7d3d8d2a4730b458c1694", 0 ], [ "000000000077d6b5ce95ac4bb2607afebae36fae00f4d9f668275a1d42025a1d", 0 ], [ "000000000001f970e0880b7f3f9456e5b9211d2e9a407a664a0cf5d79fb0d07e", 0 ], [ "0000000000000c5cda8f16ddba06e9307903d6e75b6f0fede174f9d1214e85af", 0 ], [ "000000000000339de4dd4e26cfeb620ee6958b460f30d7bfb31f027736a51fbf", 0 ], [ "00000000000039e58cfdcc21226f2499dc5f88a8c28e39c20687d2ad5d4f41ef", 0 ], [ "0000000000002db5a8e0bdced8ffd6e4803bc51a425436e2c05f90a1c69cac03", 0 ], [ "000000000000c76e1a3b84465e3ef6de278a563e602e2e9040c18a19053bec8c", 0 ], [ "0000000000002cd1ffe2aec0e5d9d6147b21e89f638b3960daac6c5ff98f6083", 0 ], [ "00000000000016305181caa8d88541b5495065c6231b67cc3df5430b4d0b8d46", 0 ], [ "0000000000000161c619a838b35752b87b780f702d0e314a6acf517af3e36232", 0 ], [ "0000000023588058c8895766d31ad2c44ccee1150dcfdb6218add8711b205544", 0 ], [ "00000000004acb3364caf13a892af294d3ac17340c3713f38e866647e7d2c2d9", 0 ], [ "0000000000c238dddd6860a7b597f0974e5f870b39f79f00035734e287d8891c", 0 ], [ "0000000003f232ff7c873bd2668c438e93974a157e8e44dd6057a23c406a04bb", 0 ], [ "0000000000003684f0ccaceb330a90045b3f5b6e790c55fd9214adb694bc6151", 0 ], [ "0000000000000ca6a9286942ffa849e82cf75b15f785bcc0be82009625341703", 0 ], [ "0000000000002a7dfd1764890b04221aae58e78638dad039a952be3aa2b270f4", 0 ], [ "0000000000006b39c0cef1f968bfa7373478222c7804c47671e92b81df352e0f", 0 ], [ "0000000000007f70368bff3515e745dbd8da7bc9bf5846bbec997c76c4e9f598", 0 ], [ "00000000000016ba9493ea10dfdc68483cdd3a1246bfe5a42c3042fe46e31452", 0 ], [ "000000003315cf62b16bd31e5ffff48a77c4332466e75653de573596dc9e6811", 0 ], [ "00000000000112b139086612b6ab6d98836eff016c28da0b68cf51a63d1c5ab8", 0 ], [ "000000000000740c7bd5664ec75f595399815676a1cb31da175f55b216c0489c", 0 ], [ "00000000000040e34183c66e6d8803b523f132aaa0c53eb0d27581f1cd202fdc", 0 ], [ "0000000000212b96599a158f45a8f8ace26be06d8c77ca7b5dc4f4cd5d2479ee", 0 ], [ "00000000000047ec63694024c7f4a742b506dcdf87dd6d99ef5166fe070ffb20", 0 ], [ "000000000008bdd2ec2fc909564ad5770d9ab7f88c95c7a2b0efd6452b7ffd12", 0 ], [ "0000000079fcb43bb7271e55d15c27b718cefec8d1d22a1cead4061896d25f12", 0 ], [ "000000000000e0d307898c7a1ef362c31bb7c4436075444c6f8cb235f02239a5", 0 ], [ "0000000000001a9256df4ffa52b4e184e713b5a0f5315b0d4ce1a3e77c916464", 0 ], [ "0000000000006b3b9c9117ab64f197146a313f0874535fa4c9a3de4fc37b0461", 0 ], [ "00000000e14400a3e77585e044d3a526fad991db8af00392286b7fc143e69b12", 0 ], [ "00000000000e8b4bd2bdd5ad3c06cb2fa234c271ec25ca87ce3941dcbc49afa8", 0 ], [ "00000000017ed9122f0371f08682c5fe6aa8418b265efbab0993b1ed00a89f45", 0 ], [ "00000000006f039a8997a1b95777b781d9650bb184cd80eabab31aac94a278f1", 0 ], [ "000000000000e4b80abe3cd2441ea4eb0a5cdb6b89c0e8ff3f369cfcedd0cb68", 0 ], [ "00000000000026117fc0fe66f1a60bd42bc2d091d13ffb349dc60277da717fb5", 0 ], [ "000000000000085133f8602b7408ee4d7b9e777174d520da980bac6a2bc746ad", 0 ], [ "000000000000b0429a45f601bc84f00778ce1e7f5c3636a7c215ad6721c7927c", 0 ], [ "0000000000001cf9d3f7f37d9c3999b05d52e6ba5434931741aeacf262bcb9c1", 0 ], [ "00000000000007f291ae30aeb2f85877116865e06e3f10658c3b3d8de7ba00fe", 0 ], [ "0000000000000c460fafe518178f959512c3966bbf6569869e51ca81d69533ba", 0 ], [ "00000000000001ded7aca43353f5258fabb96db1b785fab0daef4c427e9fe170", 0 ], [ "0000000000000002bb51deb7934ee1abaddb6607b267a9f7947f279c2848f064", 0 ], [ "000000000000004b2fac327d5bdd9b7e12512547ec70fd7c3bfa0e73c8bc17da", 0 ], [ "0000000000000002b5ae6d256f769f4c445f595277e679ff34b78dee498566f9", 0 ], [ "000000000000000760ab2425e4fbdbdb5fd588b5e2bbc15d57e1431e90699fdc", 0 ], [ "000000000000000047c410d3799304f0e61c423993f6442291ae77cd06bdb113", 0 ], [ "0000000038cd3783bb8bea435b42e3870544e610dcd642da655faf8c4011d455", 0 ], [ "000000000000048d4df1b36218595e7626bced446edf8efb8394b68a3fbd1e6e", 0 ], [ "0000000000003fb4daabfe5cd01b7da1808668ab18c68353d1ee0546453682fc", 0 ], [ "000000000000277e15671d375ce9a4499b66f1f8b33c4f6afd74b5ea20158614", 0 ], [ "0000000000002ab11ec0ab446ac363dfc93a9bf09a28d83a85b9bc163d5fa5fd", 0 ], [ "00000000000002d74bb89f8320bd80b07f7b8bc4b77987039e6fce894d63c966", 0 ], [ "0000000000000f1c86e38fec5fb04c19440c066205f89f0ba151cfad2a41cbfb", 0 ], [ "0000000000000168baff0b9e9c678a5153004f7f241f52ef1e49d88c1bad2ef6", 0 ], [ "0000000000001b3bd58011210021d92656719030cfde3c033bedb3c1e6a81eef", 0 ], [ "00000000afe6a4017bfcd2c3c9a21efe4c41951888a9c97c5c3d3bba9f30ea68", 0 ], [ "0000000000000836cb2bd14463313660efed9d0e23339fcaa97c54295e9716fd", 0 ], [ "000000000003e336698dce2d615f693645b4d04f8e5f98d02d94fca99bd5a60d", 0 ], [ "00000000035b7a00de4f346dfda49c16db195867f3c395d5d83785acf51908a5", 0 ], [ "00000000000018ba5cb649b9e7a5458467e7275e4e3284dd1d2dee9f0a64a2f2", 0 ], [ "000000000000239369e70e689f53bd3ab435c3eb6e5a26cccd52dcfdab18b255", 0 ], [ "00000000000030694285ec9b1b6349dd97757b16355fe07de048f969119575d9", 0 ], [ "0000000000000657b62914a9c16779754d11d1c444b3bd0965a571f7afdeab21", 0 ], [ "0000000000000e1429ee6f6266eaa4a21b71335503eb73bdfb4f736a1d9fcd3c", 0 ], [ "00000000000021d4eb583b65086025e1147e7783b71b61e94b0b1d01dac32479", 0 ], [ "0000000000000afd677ed22ae10ce1ddc3c9f3e73e1c1d1398cdfb581cd43cb3", 0 ], [ "00000000000003fdd00758300566d8f27cc9461e70c64f3b1ed8e20c1c2f26ff", 0 ], [ "000000000000003057478e768b1d129fe5f1acb1a99818df164966c68cf22471", 0 ], [ "000000000000000072bdeb5162f6c77003778bf3c77520028687e0137a58d8dc", 0 ], [ "00000000000000070be95e5fb581cd857fee7617219ce4047d31aea097cf9fb7", 0 ], [ "000000000000000737a7a678467547316706f3ceaf9d8d0a56e50d63bb834a74", 0 ], [ "000000000000000591b541ed7088c4ce52fd10a0b99a4b5db377a3c1ab198756", 0 ], [ "00000000193b4863d9b143d45b6db44ab8706e1eb4cf76e960b6390d8654f317", 0 ], [ "000000000000d99c10079c94e38f6153dabe29d5b0315968016e70049f0ba72e", 0 ], [ "00000000001603022822335be28b6e82ae1761ff293913016f577fc6db2e2d46", 0 ], [ "0000000000002ad9d4f5d7246b599fc9acdffd10b30eeb1adea05d38ddb9986a", 0 ], [ "000000000000050756381c62b7009c293513d5141798b5061530fd74aac98bb1", 0 ], [ "0000000000000dfe8be3b1e0dd83a4babb473a79242f40264f21936baac8facb", 0 ], [ "000000000000f77717ca2bb34b74dacd3ee6cda1e660f18498a386da0a884bbb", 0 ], [ "00000000000a110d1be3a230af9e48d99457e00a17ace1b3c5d425e95611bf05", 0 ], [ "000000000003d50b2c17cbf2d5df1556f0849a33096d72bdb3e64c426202961e", 0 ], [ "00000000000067dc944a93268621c566e31b879bdab5c1966510d73254aa48c8", 0 ], [ "00000000000035fabad3658e0d84928ec43f7b66b9efb872d907e1af0a86dde3", 0 ], [ "0000000000000a1a7a49cee85178df151625b1f9fe1857ad2bf0d47aabe0c2b5", 0 ], [ "000000000000021c7bed7852ba91de97ffea03f5f3ae1d6b7dfdfd002ff6b01f", 0 ], [ "0000000000000031bdc46f3de83a4d5a9b164348aa33393ff74fb67c22f73dd9", 0 ], [ "00000000000000388320f1a185ea53ee5bafeb4bb7b23ae05355db3ec3b9f3d5", 0 ], [ "00000000ea0ddfa9fcf128f566b08d4341b608b85d2d0cdd05156a6b5b49b663", 0 ], [ "0000000000016eb90eda23984b9615fa7f989aef9b339d8521624793c32aec32", 0 ], [ "00000000249b02d570ff2e7a1886eab612d591c58c15caffac878f9d7e69a2a2", 0 ], [ "0000000000001601a74f55eb45449c945c069cd228bbd28b662a4cf929a07b26", 0 ], [ "0000000000001d54ce071b6e1adc6489ead4869d27cf4467e09865417d7ede04", 0 ], [ "00000000000006b263d940ab956c185c9e8a39806c5d3ac9f04dda3b8cade6a6", 0 ], [ "00000000000018eb028b23c464eda0f6addbb9c36adc134fd7300f6c3306df95", 0 ], [ "00000000000008e0b67636b199f21f04fd3d413e9678c39d27e30536a1b249c0", 0 ], [ "00000000000008df86828e07c01137240d45cf9ed5fff249d071d45786d1f4b3", 0 ], [ "0000000000001bba9b529e6aa2cf8cdc83264c806e079f5127831fcf44166bb8", 0 ], [ "00000000000025e87dc84bc04654c2b30a87d862c0a3420ecd3084670b647d7e", 0 ], [ "00000000e916d01f1d6d4f170d88cd65a778a940ce11c55996a397f21030dfcf", 0 ], [ "000000001ea4d2da640a2a166e8a51a3cc5557fca1c2de40acc810ed9f9d29ad", 0 ], [ "00000000000018a01a5f01b8b543f5c211f55d532a03c9a04384ae4257bf61e6", 0 ], [ "0000000000000690cedd07d7bdd3ca2bd2029abe58919600b4fabe99fe4e412c", 0 ], [ "0000000000002ee49f8029332f30be5783dc2edca08ad279f114bf6c231d1dbf", 0 ], [ "0000000000003aca3ef92013e9c559c124b27d9df45bc5b9c620f30987cd6d71", 0 ], [ "0000000000003b73bb514fba5f8f063fba66c91534f1f6ec4ad49ea89bcaeb4a", 0 ], [ "0000000000001e461eef655c3ee4f013a079ff05e750578cf3195399f274688e", 0 ], [ "0000000000001d16126b33bf95b244af4526a6f2af6afd2d96797f4b592624d5", 0 ], [ "00000000000020dba1c8f9785e2279ea96a7885a13124af2baeeadc75927ec9d", 0 ], [ "00000000000003e1975d0b4a32af071ea8584ef8b575527e2d81cac453e6a4ce", 0 ], [ "000000007cbf5b51fb38ed9058deb8e967476ec30fb04487496f1f7802a9eefc", 0 ], [ "000000000000041a6a7ad96bda602e7d528542ec9694ab3ef72908d59a660150", 0 ], [ "000000000000afa91fc6839e2d12370cafca0ab3679e3906a436e1d6e5ec038c", 0 ], [ "0000000002b73257cf72d60433d542fea3fdb0362158a238338e49d6beb109b5", 0 ], [ "00000000000018fe0e90bfafc82833f8993636604d963ff226d6605a3e81c887", 0 ], [ "000000000000028245c822181fcd9bf9e1f22d6a547fea3ca46de3a3070fc28f", 0 ], [ "00000000debae16db51d4ef186b18f53de6749b95fd75efe3e5f628e85cd1d42", 0 ], [ "0000000020703f051f8a4d4fbfdd21f7a857570dfc0617ffb7287a7b259cafd0", 0 ], [ "0000000000000225bc005b876850b2cdea99b82bd6a8637ecc7f45c49d4848de", 0 ], [ "0000000000000178dee2f53404f5a999e1c60913658ce963a671ba778ec88fb0", 0 ], [ "00000000000013acc2e31eb20cc2f99d213fc962710967d20aaeb2976b6bfdc5", 0 ], [ "000000000000042447cf56d117295a3117a19412861e7b3987846c31ef6b0797", 0 ], [ "0000000000001c84c583dc747fe32cac73edd483cab5a3119f68baa4110929b7", 0 ], [ "000000000000011875b33bcaaf4dc9535a659684fbe4c780ddf88ad607d922e3", 0 ], [ "00000000000017e9433e75fedbf0da7bcfeab74ad1f8ced7dff6dc2bfbd12d96", 0 ], [ "0000000000002cff5e0c5e2d765b92df11d26d4b308146af9c3d971945407061", 0 ], [ "000000000000083c1306d75a0c18b0942d0ad0aecb878e24c164a9caa3fb2ad3", 0 ], [ "000000008addb7a8e8081084ce3290e7f3806ec3cccf747d487f1e1540f7c398", 0 ], [ "000000000000048f3edca928245bf247645385b9a493abfd1b5800ad16cc0d1f", 0 ], [ "000000000000103e9be4a5421fbc8bfcb1551b0f24857ce6c37a1c343aeae48c", 0 ], [ "00000000000015a51f35436fb51d0fece5cc4e436e8a064b03d8605e47bcd16d", 0 ], [ "0000000000000f080e1a6f704a41308db272cde3db3ce7ecec09a95b41964e83", 0 ], [ "0000000000001fe64f07e3a57ba7f1f3a621ce247fbb352b80b2d0c5b70ec6de", 0 ], [ "00000000000026ced680e9c4cfd857f9e3620c09853402e3adfee74143bad265", 0 ], [ "000000000000090b4e9b558c597679770325c9b22acc7561662002db0cf7e710", 0 ], [ "0000000000001e1214657a7da88bd6370b3dd6f4ab346f0a8d94e4e987f87503", 0 ], [ "0000000000000a15c42118c002d59514deb80f9f5466d27963c04330e0e21012", 0 ], [ "00000000000007a242146cd528e009c348cc08eb1f95f4d670c7dc158f2bbfd3", 0 ], [ "00000000000001e36bbd55b594405000f74d9617538306d1a80beca516598c19", 0 ], [ "0000000000000039a5021d0c7f2786e5809c059d1bbc282c75f1b23aab96a88e", 0 ], [ "00000000fbd8390594a4ba46a5bdabdca857c83875b424d25a77b0eea3dc3237", 0 ], [ "00000000062932ff37ac56e6fa111466872a8dd63a40e075cf5ac17711cfc54d", 0 ], [ "000000000acbd85b88534f49001b7ece8fd3bb6a7d9ac1579e266c504d4806b9", 0 ], [ "0000000000000c69ee7072ca34b8e0de031d0e1a94ac4808fa9e0d5d54bfe09b", 0 ], [ "00000000000136712b53c3fa040965e914ae2c77cc575b3043ca721288720a7d", 0 ], [ "00000000000013bbcc926582af7d0a9916b373a8a915c5db4b7fc5384ecc57ec", 0 ], [ "0000000000001d67d8a253a97e007bc9b7610d837a4b152012dcf33e8ec52677", 0 ], [ "0000000000020090573bf3f7cecba4b23ab397d04fa40ff74fea9689841969a4", 0 ], [ "000000000000045744fc4904ecdf15ad85d3069df9992d4077511bc8cf476149", 0 ], [ "000000000137d3cf68eae26792414e81cabe328d7364b5c407e0b4ce0a2f5a4c", 0 ], [ "0000000000000419e67c1b77993aec325ee5315eec3fe8b4d165898e5c77fe96", 0 ], [ "0000000000001cf0d2c8b2717072cd15f060e622bc73414e357324c792e1aa59", 0 ], [ "00000000000018c48cf760d66c867ffcd0e113a4eef5b5f85d0dab65b727b460", 0 ], [ "0000000000002f9b1c3e3dd47a73bc9711b6f755d72514ec5d81ffb9524bb626", 0 ], [ "0000000000001bc7822a17596f813039ba79ddd9e114f81a3dcdc596533cb493", 0 ], [ "0000000006202b1fafe78be2c564ece74bea3a1ebc294497935854cbdcf2533c", 0 ], [ "0000000000005d7567c3851bdbc75d1c6c9372db838ad0f1e6bd472b5a100825", 0 ], [ "0000000002590ac2a684ff9da31826f8354865c081b243ee04abac8e799f74e4", 0 ], [ "000000000256d70b8e6030ee6b56ff47872fb089a5d007adb5057e7c4930d0e3", 0 ], [ "0000000000030d7ea2ccfee60b00002337ba48466d40bf541c27b01b5b40d0ce", 0 ], [ "00000000000002ea99316525bd7497de5d9fb6ae98d6e69008e4036b9f1a96f8", 0 ], [ "00000000000017633c6c08c6e76e58397497d88a74a77ca1428e7daf7260c9ab", 0 ], [ "00000000000017b937aa4b9327600dd33804deeddcdf6abfd847455c3365fda1", 0 ], [ "0000000000001bbbc95f689697902e961c1ae60b0ba48630cb275292128d457f", 0 ], [ "000000000bccd05d924bd0583dbbb9f2b09cbbd04633e2905e44d9fa9347a150", 0 ], [ "000000000d269ad70d5bef52c953079d690c8c20a3a9265c0dd4c37ab311f47d", 0 ], [ "0000000007406099654ec641983c8ba1027fbf288eb23e7352302dc0670ed259", 0 ], [ "0000000000003e6d99ffbf2efbfec8eac8a595214064640922bfa725967de2c5", 0 ], [ "0000000000002107d0e8507d9168c684c45d4ee62dceaa757890d8e34320ef99", 0 ], [ "00000000003493e7933f834a0f38e56e46cfd0dafba38c4d972ddb5a2e491e10", 0 ], [ "0000000000001be81450f65ba6bf3274e4681762d78f22827e7ebf30e6aab8b1", 0 ], [ "00000000000024530945d47086a0f0f15e2063d70fe5435f40796b9d754406bb", 0 ], [ "000000000000ab099a527c022454155a19f1c6ca6065201817400bfeed39cc4d", 0 ], [ "000000000000081b516b4f53c442ead11bb3e64e1d3f6fb628d3d6428f88371d", 0 ], [ "0000000000000d2d06a76e91d8f3e6a3634de9f1e53c4ecfdc94122b696bd4c6", 0 ], [ "00000000000001e9af5fb09645618a1ea84b537f0532511d921e0bd03e79a34a", 0 ], [ "00000000aa98879ff0193e4d5b5fccda8a680513bcf062330aec102cfddebbed", 0 ], [ "0000000000002bb5eaa52edd3ae8571422d99a23a71feb313866f1c177c5fa52", 0 ], [ "00000000037d95e70f0026b2747c3302f84668ee3e112cd335efe0b71de93742", 0 ], [ "0000000000fcc1b88fd91cf4cf853f6a2218f750d1f5fd4b7c3d0c4309c2d48b", 0 ], [ "000000000005bbb90407d1cd8864a4e0acdca910cefcecc42d90b2b80579a233", 0 ], [ "0000000000002f7c9303dfb193c69d0807f9bad18bd1ee5b705680afc2ec4a23", 0 ], [ "0000000000046dba12423dc70bff9d5c8257c5acb4bd0118d459defc904c84e3", 0 ], [ "000000000002b9f9c12c825135a280583c0b838b18e45da81aa44c3adc60fe49", 0 ], [ "0000000000001681ca0ed67c0cb2c566c238ea955a4699e281de06d36285a51d", 0 ], [ "0000000000003a24f300fdc9cf9bbbd23a151ee9e2a79afcb6e9414663addad9", 0 ], [ "0000000000000d1d80f15f3afe5c108688f0fd8266f7fe06dbb6234ee4c9d66e", 0 ], [ "000000008c6a6332a8c9351fd6e6af767a849eaa31838bdefefc36e8905ee111", 0 ], [ "000000000d2ec08ea27ca63313014891f00250ab2d0e9e8a1368541766a3637e", 0 ], [ "0000000002fe57f4c0d782dae857905dd16c6ca3eab6cb68f14ac96f9c5e903b", 0 ], [ "0000000000934f789f1d061a0f385037de57c46644929ceebf8ae7a7e8a797d5", 0 ], [ "000000000039aee8e147cd4bbe399ecb938d22e2b0d403d4b81ad3a36d2ebf0b", 0 ], [ "00000000000626939a469fdcf2109460bed2634dfc5a799b267c3053761cd78b", 0 ], [ "0000000000091af9d3c8ea0d3a35a15376126f4a3cda6905c7feab16db260ede", 0 ], [ "0000000000008fe017eccdd0f1e295efd15813211320952b9649df11cdd099b7", 0 ], [ "0000000000002edf8ddf0646a8e66768a7067d866d6feb3c7832cce16087d4a5", 0 ], [ "0000000000001f290b0b8f50d90e11aafcddff259207a8234e2764be0a6102d3", 0 ], [ "0000000000000039501569acd824c3e514ecc1046076c62756c850696a27aea1", 0 ], [ "00000000000002d87f5b96dbaa8ad6c601c886c1a5a3cab8ea2c8732201aaaf6", 0 ], [ "0000000050d5bb27724bd6a99a31160f8554f2e4868631c719ae0a2ec0b73aac", 0 ], [ "000000000feaf2eaf424a989ebdd2ba9f5ecac93ee5cd495cc9645f3af3d4702", 0 ], [ "000000000f79be6b3023a12b57ad8132f0987442e2e7f40d2e04d3a2699f8699", 0 ], [ "0000000002c329dfd4f21d40ecec041beba86df7a01c39d0775133278b88a980", 0 ], [ "00000000000016229b9dafbf5fabb6dca805de40ac63709bba75352e6f662352", 0 ], [ "0000000000b9ea4cfabe7ac942aa7bdd91689b03fbce3035cda3371e57b3c140", 0 ], [ "000000000b510434311994128aa067b94020fee893e67dc72998eceeb4650d1c", 0 ], [ "0000000007b3d6eca2c9d0a8d05c884412cfede2aede572076c7263a38d6306f", 0 ], [ "00000000006dd9fff347f4c9490b29544fec60a3e0504883d13ec43cea0e5260", 0 ], [ "000000000000e2719af18f948f63b60d06fc7f35bd7cae88ef43d2c9edd611d4", 0 ], [ "0000000000004ff5733da92e83ad0970b7eefa8719ebe7346f65c8da62be79e8", 0 ], [ "0000000000000247d49cdb1e1ce982cfafcd8ceedb9bb1090d2cf8dd52fd7362", 0 ], [ "0000000007aab1ae7198606046c23cb4616edafd19c0410660dba6ef065dc522", 0 ], [ "000000000d6a048170bb18c0421b153bf709cc85e090602c2a5b0a626dbcc3f4", 0 ], [ "00000000069a824155cbf3f02f60363bf5b41ab7e8a83fe647fc4169b34f2808", 0 ], [ "00000000009d32bba8dd11262ada78187ff56417fa1df97b3444196c987683f9", 0 ], [ "0000000002468dbde6d879e2027447918f4bfeba25ff32a4f19edd953d42aba8", 0 ], [ "0000000000e92e9a687fc2fd40b221a02f76ad72228503660b5b6702db9757a2", 0 ], [ "0000000002549c36360bcdf2c495f9093f117ac4a28e205d954ec803dfe7aa57", 0 ], [ "0000000000e1deed95098ca573091dbb6b696787ba265277bc2ff65ae73cfde0", 0 ], [ "000000000bfbb8b7a1941598009350c5d741e05c6575323591a04d57c206e205", 0 ], [ "000000000dff311ba823e48522c286a4962bf9e9bd7a0dde66aa5e5d3d9796d9", 0 ], [ "00000000033b0dc90fb39c5c46e144dd08a52572967a712988a7b543195185c9", 0 ], [ "0000000000003830feb05937c05d8250b9a465c6a5d29dae49b127dd9abc99e4", 0 ], [ "0000000000014d788b90d6a52d49e8e74a054e26429c25ef8a4f3cddf9463e6a", 0 ], [ "000000000000007cf539b80a7e7f7d38a511f7c3fc94161cfb3b960d70c54359", 0 ], [ "0000000000003c7c0f340bbd67e0dc9dd89a340829de0c4a3f1d53db38499a79", 0 ], [ "00000000000017e131be27185a58f3f9f0e239b1fd15571f500bc01aabf3640f", 0 ], [ "0000000000002f688fa71606e8feea09acf4d97493f71adab27bf782c1c11658", 0 ], [ "0000000000000b38da817e3cd6a112626414b5a7af0cd8dede33f8facb87e58c", 0 ], [ "000000000000020f5ce46cceddf4b42457687c20c2fba80ec2c797354ae32deb", 0 ], [ "00000000000000b1c5d067792aae5ae74e4cf30b0f28caa064b5d736b2b9ea08", 0 ], [ "000000000000000bd96df6cfb597259a34b8abed672dc344c6de48c4c26e4b6d", 0 ], [ "00000000000000061b79bafcf1a446752df3217b60b83bdd768a3030f04b21bf", 0 ], [ "0000000008f3bcc3504c464581b257b1236e0bfed8b0472b58281a161fd1287d", 0 ], [ "00000000000019b757e0f7768982ecb439e31b022efd60bfb09509e2c920d063", 0 ], [ "0000000000001e5a69bda72ad2c5f4bea6018f11ec4e3bfb6f974e08c81814b9", 0 ], [ "00000000000002bdd9c3c6c9359582934a594ca82d6c9104f15277ed10349bb4", 0 ], [ "00000000000015b7c29f74c5dad9d6121d0d4d3af9f44333b2008fe5bcbe2eed", 0 ], [ "0000000000003638e630bf33aedb0b0152837f695224c199ef96c248a6b22f16", 0 ], [ "00000000000011be4e7ceacd148ea83abee5b61063cf679c879d0e13d0a40655", 0 ], [ "0000000000002542493ba1cf5396d5209244f266a2c5764d4bdd843c5986d82a", 0 ], [ "00000000000039197e58f1ef6c98eee126a41ef156036ea51114b15708401701", 0 ], [ "00000000000004b893eeb6b43cf0723b5f0fc2c6e245e6671f543421ef8c5f69", 0 ], [ "00000000000024bc586bca1c86f4254b3d74666f22ba2f5742f588d7d50ca6bd", 0 ], [ "000000000dd0843d947ba235aaf2cb1b5e2320f1b15d1f79d32ccb01040735f0", 0 ], [ "0000000000002e9524d79e1972ecece3c432eae5e8c2aba1e5d61584c8b3c780", 0 ], [ "0000000000003c02f0d0743d391caac03c631ebb5518a7e4261487482586587f", 0 ], [ "00000000000015a72bd8520f0d8fb9d7d658d0e62cae6830bf94a2efdfd85e65", 0 ], [ "0000000002f4dcab6bbc94ae9a1b0761ab7b52cd56272b6183715fa1a959101e", 0 ], [ "000000000000005ca13d4fc8bebb498e46dd764ec5f727ffff7ac9c90da8ed28", 0 ], [ "0000000000003ba5816aaac8c6efd085bc08676ff2b51e7c3d94263b4a555c17", 0 ], [ "0000000000000881711d6afcd9ad84823ef6cce98b1cd8a90422867f23644a37", 0 ], [ "0000000000003d627ca2ad44d333543e86cc377e63ca507ddea5aca7ab4a0829", 0 ], [ "0000000006ae50b5cb9404746f3702b2400a3743a05893cbac1b372aeb122834", 0 ], [ "000000000000197307176984fc5e3b612c9b6a89510e5a3604711f90739846d7", 0 ], [ "0000000000001709130852704239c0328a00d406ef2914dabab52a137bf59aaa", 0 ], [ "0000000000002d8039783ae0208445429241d2f5b4485192e1c50f6921579daf", 0 ], [ "00000000000028ab4cf7ef62580408801e9c4a3b955f6f1dcfbf339f08f335ac", 0 ], [ "0000000000001730f6260b57593f30c6a24ad49f5afde41daf8dad5bb83cd737", 0 ], [ "00000000000026800054645b160790f1d226f87a2fd809acbe06d018ffc13fae", 0 ], [ "000000000112993137d21171ba742be3beef4af19eba815f326fd6ecbf54b547", 0 ], [ "000000000000236d85b62dda3bfe9850e4e523e3876684461b7e1107153556a2", 0 ], [ "0000000000003fe1c0f4a23adfb74f07bbd4cfbce44db35119332a5c43500bf8", 0 ], [ "000000000000381ff5b96685ca16305b427c4a1dcba2d410e6ee0598619f75f9", 0 ], [ "0000000000001d8eea8071cc6e93b6dd058924a6fe171438974c304374e1f1cf", 0 ], [ "0000000004da6faf93f8594e82f9d282d95583e72f79365083c00fc2f4fe249a", 0 ], [ "000000000000254e1d80fc91204b555a4269d18d892c60a845c907175fbc018d", 0 ], [ "0000000000001a945f9ea3b8d08ea7befd5074bacd9732a05d1dda579b197538", 0 ], [ "00000000000028ca1712b95cf77145ff45e9049c5c39458d18223b3e17812d60", 0 ], [ "000000000b06ec0c9b1443bfddc2b3376e71789298ebe3575be1b0f5a34a37f2", 0 ], [ "0000000000006562376c1164d7b6f5281d64fa7dd0112bd34e4ec9c7537698f6", 0 ], [ "00000000000027ab842f086d6c6691aca8f51967c1c88281a4e49ad89878a806", 0 ], [ "000000000000ed2292a4f779dcc4ea3315b855243a3b70804da3168377f408ca", 0 ], [ "000000000000c287b24e25768d8524cf3d8b892da291f868446cff373f878777", 0 ], [ "000000000bf32a8711ff9536b7ccece30a2d6ac40538123206862e01a8ef2350", 0 ], [ "0000000037bfd22d6171e4f1c7c7cb1cd73b91a19a3cb473bd9f33335150e001", 0 ], [ "000000000000045f2524a3a3faee2a2d36cdfc9531f429670106622d0c1bb558", 0 ], [ "0000000000007e262915f1b1ac2bbfb97ef84bfece7bb8ae3340fe4eafe7017c", 0 ], [ "000000000000727ee42b831c566ed9c2ad92e9b14db0c8058b9d6d6066752e71", 0 ], [ "00000000000002227876ff70290cd31e6b08ecdbdc2dd34ff0c69412ad863f4a", 0 ], [ "00000000000013b9729fe455d8409547ba3dd30d62eb1661ce6a38bfcd358d02", 0 ], [ "000000000e61c29a6e384bb7f4685608774b790cb7dcaea51fb99f8a10e2e03a", 0 ], [ "00000000075f63db5180b1afce047b919bd4c06cd81d6dbe5224189cb6fb3e51", 0 ], [ "0000000000a83265ca1a81e9d610bc6de821e07739c89a137b93812b545f5488", 0 ], [ "000000000003ba66b509ba5ab7cdab0c705c8b4145e1aade97b86e0f8a1ec6c1", 0 ], [ "000000000002b0ad2b5fd6f15946ba21f819f074af0b96cbcd9a58f5be2ff6d0", 0 ], [ "000000000000103e6ffc413e63a85bcbba243a9c2acf45b433d6dc36acb6ae1f", 0 ], [ "0000000000002226a204b75f5086a8ef6576e8e79af7e904bc9a75287d518b81", 0 ], [ "000000000000738f4fc499a14bcd7bca5c16273f59d7695decaa9b177cea0873", 0 ], [ "0000000000ec7ebd6c07b47fdcdb2d924e0659a22d2b2bbc0f5baa15cf940825", 0 ], [ "000000000002c9d4f581fb0fe2f40a9a24db42de1a85064b4a304a25338a62d2", 0 ], [ "0000000000003ede1becaaff56312310787669e07c38decfb43fc6df9f7950d9", 0 ], [ "0000000000f76fd9b371fb6231cbf696f0e02967dcbbbbc32558b08a6bdf1c0d", 0 ], [ "0000000000009c6e722f7908de61abcf6edb2e530e13c97b4b226a2e1dd9cb27", 0 ], [ "00000000000008730a8476c058a573bc8100dff1da03b3d942a8164a107a00b0", 0 ], [ "000000000a2ff7a14ba1fddfe3239d3202f310490841a5d1294d53df83a471af", 0 ], [ "0000000000a5584c9d4f74046f9fe4e3c9080a0bf3025330065b9607c2142a2b", 0 ], [ "000000000000d55a8cc2e86b72a1a0502a25962b257e0cac2203b79270dc54ae", 0 ], [ "0000000000a2f8ff2e196c0def66a352622984d03216ef0610b17813ca4accfd", 0 ], [ "00000000077267090ddd0a5287bff41a19e4aff8772befcf9f6147d5dddc96c2", 0 ], [ "00000000001c56edcba9d4ae7b7372bcc7f8ed6db3c3f7fcbbed555028cccf27", 0 ], [ "0000000000009c304f5a5cc26c3bce7378205db954646d85049dcf77ef98eead", 0 ], [ "000000000000987179da28a7bbe0cda559b26e25249d826406f002c65a5ffcf1", 0 ], [ "000000000c08930caf906884d095bf68c98781a191bdff9b8677fe25660173e8", 0 ], [ "00000000000070168a5836c8e1676f6b9fa8e316fce6c38b91d6ce7226e7e897", 0 ], [ "0000000000001d7cdc82f066c2adf9b6b8658d531a6dce864bab36660c7e7518", 0 ], [ "0000000000000213c8fac899b3ceae3b36a66a63e94e570b3642c8bc4459fea9", 0 ], [ "000000000000803753b883992776bcd9b0f4c3553ad19e524e82362180a777d5", 0 ], [ "0000000001286d5f36e3d987a6d6ee4c2990799738733331b53e1606e5db55b9", 0 ], [ "000000000000d656dbf9b01aa7cd45bbc7ca206e5fdee253c1138c8d01dde543", 0 ], [ "0000000000004ed5d68da0abdc500801ecbcb13c2a0d0cd22c664c48cf1fea0e", 0 ], [ "0000000000aa80295620b2da59ab5af04caf0bc3a91660a61458b08d9a0ee5b8", 0 ], [ "000000000000f9389fa442251d5fa38a34a628b26d3dc573da88427c0e330067", 0 ], [ "00000000000072ab2052ab08cad2d2e73a46c385a828682335828607d9237d09", 0 ], [ "000000000000f6fd78689cd91839bad209db4d373546fd657801b00f3186fce5", 0 ], [ "000000000ef3589b0434e77221be33e79b0751e3063b0d0ee03bd70ae43adee5", 0 ], [ "0000000000009c9c3db06deb8bc77fab2765817b2293aab7393e527a5ef33e5e", 0 ], [ "000000000c17c570e13201f910b29ec68a66a21f4a2ed7866987905012c1330e", 0 ], [ "000000000000095a4b6c94eaf359cbc8301ae2568927eb9b151266a0286748a7", 0 ], [ "0000000000004cb69211ccf74e6d84ccb6dd421c8dcf16795e8d3b476ff7473e", 0 ], [ "00000000000006db56d24caed9018565840fce8363a7cd9e912504114d0b4c3b", 0 ], [ "0000000000004a15f1dc5253af6641c141a8f4566ac14fac2842ca6ba53bddb6", 0 ], [ "0000000000002aa51c0812c9a1438b0c3bd8e95330a8c4fac62439d2797d168a", 0 ], [ "0000000000002af6ab6d23bf33123e2393c1c9df7e7c2e4f722aa130b0f0db97", 0 ], [ "000000000dc6e875c0a047d3d05dd885c93ca1de0d9cd1a08ee6523e321ca9eb", 0 ], [ "0000000000006a0b4e485b2ad027de8edecfa97a822a87e9623b21be8353cf91", 0 ], [ "0000000000006ccdf09196936d0cf74da3a3c433487cd13e3386bbf0e84e64a6", 0 ], [ "000000000000134acb738b7e99f9991d63519e28da5a1c50c60a31b596f39034", 0 ], [ "000000000000bb7e53229e6557a53be46d98a9dbdc4c827b2b094492d775d1a6", 0 ], [ "000000000000e8f54f39df60e09d81653a5f80707d084edcd6e8bfdb7427c588", 0 ], [ "00000000034b27b5cea0fe55f4dac79fc32e25b4a09ce643f99bbad2e30e9335", 0 ], [ "00000000000042edd05861e88b20e03ec95a7a5ba8f60e595ac044d77ac5198a", 0 ], [ "00000000000021bb194f73f614ab1f4216227f09789edaa2677da691ef5d73bc", 0 ], [ "0000000000009b2dea9b3b6c8025c7e632fd7918316400a79170d7114fde31cc", 0 ], [ "000000000000b6e17bb1f2574590815ad6d20456e9d7f219221c58e3aa3767fb", 0 ], [ "0000000000009c762fd6211c015e5c6a42723136e950cb2333d30504ca2b0173", 0 ], [ "000000000000733c889bfdba15a7e9880890a8ae6814d4310ee6896e8e7c1dec", 0 ], [ "00000000000022d2fcb2ebb92f8cea3f174969805f69de7518ebd8bf26140255", 0 ], [ "000000000982ba146e85fa32f2c3ecc0321d865a7960a6cda8928983ea679b0c", 0 ], [ "000000000000e004dc4ba124a02643e3a2cc4f36665c75d97c2590be07d1e304", 0 ], [ "000000000000a7d0dd7994be5e5acd32495145008362e072eb02b297e1431207", 0 ], [ "00000000000011af0549ad5774dcb338f3c8e6757ff7ba2bf4da4f6932651862", 0 ], [ "00000000000016a682c3e571c0fa6f982f5193f4e416f474c8a7ddc79d88dda6", 0 ], [ "0000000000009d072bc459efeef121a836c353d88a8dea2e73ff17c2f2933c67", 0 ], [ "00000000000091d01b9faa03a2f5076d194b5f889425e2a85b2064aa4b4a8258", 0 ], [ "0000000000004b035f4693d1d32e518c7977a8ea2e77b7b6f4f51b10763ff505", 0 ], [ "000000000000c330f00b190b1ee5e60aa8cc3871472a02d98bade6dada6351ed", 0 ], [ "0000000000003f970f8df4843d6a66b74793b5b7ff3bfe0dc128f6eb00b242ed", 0 ], [ "0000000000000a649d33eb372d40599bdc493d6baa50cfdd667073897c09487a", 0 ], [ "000000000000026b4a6c9b6241aaa2b3f5927e6fff5fa0f2c2fdfb4ef39f00fd", 0 ], [ "00000000000000789d9b77f871481ec9cd1b8b877fe885c49eec83e97df988d3", 0 ], [ "0000000000000028abd92b26a2328ff5bf15d3e9104d2491d23d5f789bed2c1f", 0 ], [ "00000000078b55bf69f546804a25e86ab88ab12b3aa0d614952c23e82f7b2c99", 0 ], [ "00000000000030ca4bda5ab3c07f5476eacb61ed54cf7080b3ce112efbb0703a", 0 ], [ "000000000000175f0ffd64634d958ebc5ea9659e5b9c0e91c2b31cbc9db3a8ab", 0 ], [ "000000000000355f3794e3a81787699b8fd649eded08f829d1e6e47e5f0b9daf", 0 ], [ "0000000000002e5396ffff0cb008c6a5c773ac7b56456f3574442ff22da9c8c8", 0 ], [ "00000000000013f7a1e25a9fd016d1ec5abeba0dc69d1af2b656e86f19fc73bc", 0 ], [ "00000000000018cf2326052e78cf74dd3671c63bf64b58c5dc4b1ac964b712fd", 0 ], [ "0000000000000f321300ff44eb5bc2806eb15ac3e73ecd8dadc08e00921d35a1", 0 ], [ "0000000000000b3af62155af09d7d3612eed2522fbc7f9eb1751c67c56e20903", 0 ], [ "0000000000002cbf583243f17ff8b6ad7e4477d3e58e41cb6589882fbed15406", 0 ], [ "00000000000001b336201e7d8876fde8def3923ceb44bad051cde464431d4937", 0 ], [ "000000000000000cf89bb1e492b07c8e771b880995d293bbb5a2b6312d9a1584", 0 ], [ "0000000006b21f9e08db1b242dec441df0a12a8e5b93d5fb39fe6069cebd197b", 0 ], [ "0000000000002d81bfd983a54e8fb65d48868e361c5524fed9646bd999dbe894", 0 ], [ "00000000011b6d38162fa8128f2a69ce0ab991bbbc619482312665414d32ff2c", 0 ], [ "0000000000003ae808923e1323e4790f207895a1e04353a6dc5c74502401ca17", 0 ], [ "000000000000253ef210a4ee95c1950a3095825114dd67ea0fb7a7bf0f75006c", 0 ], [ "0000000000001c6a7b8d2fa299c8ed9433e00defee0f97b5d93427afc753edeb", 0 ], [ "0000000000002c54c1b035f0d6768d24899d2c79933aba096074302075389ec9", 0 ], [ "000000000000373ab0c2c8fddb53cc8c1890448d11deb2abbb3e36c986d2cb17", 0 ], [ "00000000000031898465c27a7055012064a6184c41f8cbc89700419b802744d2", 0 ], [ "0000000000003aa31991360c4b63ed0760d83ea3eb3279789b406498db586bea", 0 ], [ "0000000000000e5f6c671e63b383a90347b7d95313fe06570e5641442d30025a", 0 ], [ "00000000000000eb1edebd17248bc5dbdb02eb7010fd41eb425a6c78975dadea", 0 ], [ "00000000000000c2d84b76a602b417861cfe4580194ea4076405872b1e954808", 0 ], [ "0000000000000013d788f8faccb96b3d702a1a2a94f547be4552946cb04d6b02", 0 ], [ "0000000000000007ddb93d3d5d2cf8521ad8d8bfeaa031138e4271669a0000b6", 0 ], [ "000000000a43bfdeb9b9f513518ed0b4a523075c3ead0b049913821466f40624", 0 ], [ "0000000000001b9b9cff2195dc6868da50a4ae6eb4f3661f31feeeaf2a5fed7a", 0 ], [ "000000000000346a1dfdcfc23f8bf474030c42683288ef5e6419eac5e55720f4", 0 ], [ "0000000000001274395c0a6d1b509d8b242ef570c99167f388848999c9d00b87", 0 ], [ "0000000000000684d1b654d0c2a3c9fc1919d2f20948106d48d2365bf1d52d11", 0 ], [ "0000000000003d31b55f30b2d91979b6c4aa249ab5e782bfd61c6cd99a199962", 0 ], [ "0000000000003b4e4f3fe1eb3f17922cdff9538b862fdd4be332b38825132504", 0 ], [ "00000000000002b62a4334c696eab73339ab3cc497716b163beaa5c568be70d3", 0 ], [ "0000000000003b0a1b1ddad166962bd5d29335590c53ef5d6079fabc90e238d3", 0 ], [ "000000000000289da5bf12022ca27bf0b8b1f097a9f1e612302a64714f737c35", 0 ], [ "00000000000001b05c01730ed74a9dfd86fdfcaa28f97f9128867b5d6dddaa87", 0 ], [ "000000000000016a9af16f51a69f5ca833eb52c97cb062c4255f97296850f8a8", 0 ], [ "00000000000000576c2c704125e6704e9e1b1b0b5269f0ad7e14291f8bb875ba", 0 ], [ "0000000000000036675b4282e91cbcf341e0b5c68085b5c002f7c623afb01506", 0 ], [ "000000000000000029531ba2b1f27a4a1ac57a946ef322848417ec83d7cebf96", 0 ], [ "0000000000000000764a389696612dc41f5ac2c5ecebb887e296c226303754d0", 0 ], [ "000000002c3a2e26f7891923807fe41d14ff232de4a9496bf99afbc53af94006", 0 ], [ "000000000a27812c8ea52565338cd4881b783d18f0d4bd9a7ba8f4ea4b8d1e6e", 0 ], [ "00000000000044c58e6a1e82f51dc23b11ba4b6c4eed7ad317654198e9a51022", 0 ], [ "0000000008c67913ebb49ff07a2776c081113fb20adb56b1f8c8fcd1094721f2", 0 ], [ "0000000001cdc1f92512c05d201fb32ea48b23e1d2568899ab06f14ffa329848", 0 ], [ "0000000000003eef51183eb12076427c8c223ab73760373c163bb4da036f7cf2", 0 ], [ "000000000000743bc379610841698c4d333e5cc721b8ad95d027b4231a365b1f", 0 ], [ "0000000000005e9274e286a066e145689530db6183a175f8e6721c44171cdd16", 0 ], [ "00000000000021a56fbf607f3805c158393c0d8b3683633afb0d0ac7d64d0496", 0 ], [ "0000000000001e223329a43536dfaabc1467f2ad7d44e55478f62fd80d9e48b6", 0 ], [ "000000000004c61719fe71b56a2d8706b5119e2df77da7eb9330aeda1825fa4b", 0 ], [ "0000000000001e89565a9c898bd51bede84e2f231aa29a38f3b47b987a2ab0e1", 0 ], [ "000000000000712743a8df968020c48e3955170166e90ee9a2e02cb460349ddd", 0 ], [ "00000000000019192acf1ff42fd9fd8d9aac23e51837f4698a8f02596f6cb7e9", 0 ], [ "0000000000000f4f49cebf0d2f8731f5136903bbe9c8bde1b1ca94048119fd3a", 0 ], [ "0000000000000141958bef54ea397bde360d8ac3eb117c1dd785f8006856ac92", 0 ], [ "00000000069650f051d27f2cb54045c19304eb99425c042e010ee982daaa9002", 0 ], [ "00000000000087523d9819f2661f47140dda70be7ae7af0b2e75324dc3d6fdbb", 0 ], [ "000000000000384f0be3649578b7ff6b578e5976e18f79829e5a40c975f54ca6", 0 ], [ "0000000000001a9f45b1b809fe1000a7177d284475c0db05c0bb18645e1f04a8", 0 ], [ "00000000000374404f82c755ad76a64a96ff4ff449c365ed7769ed50437bb09e", 0 ], [ "00000000002b4291a859b5489268c232ca7f86f76944690b668d82b94538e0f8", 0 ], [ "00000000000aa4227550ccf273d27412aba102a2a90520140b715c95fb4822c9", 0 ], [ "000000000001cad06fc2b9a27871b4e6b8c643d6fb3dc559073c2e2aa51ea4cb", 0 ], [ "000000000000ac7c3ceff73b328f172c1e1906f0d706885cfaa4f60b6bad42c0", 0 ], [ "0000000000002c0c170a510d880aae66dc7beb2ff0f4d5db3effd802e0a2e89d", 0 ], [ "0000000000000db55eafce7adecec679b6793d81a60f33c19a7e9055ed47ca41", 0 ], [ "00000000000003201aa00eb05c7b2bef9b4972ebc640a04be522fb749a12afc6", 0 ], [ "000000000000004faab34097be7f67a51b3ff1e7b2527d750697691b2753a996", 0 ], [ "000000000fc33ffa85423a62215dd3707a31dd5b13f2b9ba868060670d5c456b", 0 ], [ "0000000000030b22b40f979c6104cdb59113cf76a15fdd676317c721c1c9a4ba", 0 ], [ "00000000000096a0202c2467783d606246faae84a897c96e5c05c42602d2bd72", 0 ], [ "0000000000013b2316c180c52c60e4f38866430e94e46d2c7f0b63a6cad57616", 0 ], [ "000000000002bd04bc3dbd8a3a91e1d658ee83acf1a7a66f1c0a5cccc70e88d5", 0 ], [ "00000000000026d791eacacc6c13689a83e057c77cf2986a3da3a43e1c6924ba", 0 ], [ "00000000000a03a783e832ace54ae9a71ceb9888102e1f91332005c5ef5dd139", 0 ], [ "000000000003d078e9ac9a2a9cc3b31190e1ce78bcc449a6b471ccb4cb6fceb7", 0 ], [ "0000000000007d94fd9860466fc526d3f9b327ad9653ac7297bfb9991a2fdca1", 0 ], [ "0000000000000f7bfcc5d12df9a0989837a98d371f495627660321ad857912e5", 0 ], [ "0000000000000303be9bf59b1161dbf359d6c65d99a38eb03619428d0d0c090f", 0 ], [ "00000000000002dcad26269a9f62413ec0a71f0fb228872030f1617cf634fe3c", 0 ], [ "00000000000000bc9f405c6a6088973fae16ee48061ab01e97c8e4193c88509c", 0 ], [ "0000000000000040d4a88b68cc37ca890f3ab23958f567253ef2ce87eaa36bb0", 0 ], [ "000000000000000f961ed7d57a81e0c232669ff9e8ccc4867632545016c78ae2", 0 ], [ "0000000000000003c996d48410bf2660521bfa1151fd316d65600f72c4adf778", 0 ], [ "00000000001c7e8824a780a745fd728e6106a49a50b19b45e0653fccdde06b90", 0 ], [ "000000000000094011e26b89dfbd3355d001db8b07b29474a5ced7b5883ed2cf", 0 ], [ "0000000000002ed4eb9f36f5bad0ac0f0c4c0e6a0b02d9e6267472f0ec96a06f", 0 ], [ "0000000004fe1536748aef5d7286f1699f4cd0f8afc845aae9ff90435e81713e", 0 ], [ "0000000006d789445291bccf72dd7f126f3e1d3d03319df61683079826b94f89", 0 ], [ "000000000dfaf0283c12eee49338b0fd14181a50302074e51d928ce2280b8814", 0 ], [ "000000000117b2c05c27806291a5596c1d65e784ca67e886701df617cae96bc1", 0 ], [ "0000000000001db99ab74f6cae68d350a095d5967636070247363b6b2f9fe87d", 0 ], [ "00000000000010047f90a66416b399f265a55d9dbeed30cd320ae2c1cfe4ace3", 0 ], [ "00000000000021366ac9446eed3b71b3d437ac3fdc6647dafc52803fa8b1e920", 0 ], [ "0000000000000dec6efe565871e1b9212c8b6463cc644c4e79e32ac2758bf4a9", 0 ], [ "0000000000db56b3d3e67e1581d443fbf695834694b7af2c7d9f9e96f017659f", 0 ], [ "000000000af313478dc82a2e3a71147d678b41a83149cd0c1b0016c796e666f6", 0 ], [ "0000000005e838486868ee22a713d261e0bab20414bd5805fd914075906e27e9", 0 ], [ "000000000000343b7029da5dcf93316a95e2b83a34647370529efb7a1f3fbe96", 0 ], [ "0000000000002797189eb8edebadad2192491f56fc93ab08424e5e00161c3fca", 0 ], [ "000000000000948cfcdc2645c13d25093500954bd8c49fd3534f695e31d0f383", 0 ], [ "00000000000015db4d59d0de0af7880fb9f710f20909ec39c42eeb49e96eb5cc", 0 ], [ "00000000000002ee9a047e4a3123d527298be5eb4a1b4280b98937f85ac908b7", 0 ], [ "0000000000009260261421c980b8cf2dcddc6a18c49bd1bc804dfa6f0df359bf", 0 ], [ "0000000000003b963afa52c2187dee9fb9b2cceb799994f7e71f4aefe9255e84", 0 ], [ "0000000000000d9d821f0edd2940e911b16ffb7d21ef2f43b089c18f0152c1d4", 0 ], [ "000000000000004117fdabe3fad201f486169db3445064231bfbd78a74fa7f3e", 0 ], [ "000000000000007ec434b48b3414447b10656380c1ad15ca0caedeccf95c5cdb", 0 ], [ "0000000003c5bf907909fc3656595f18ec47a05c4cfdf3b682d004a333a1d9ba", 0 ], [ "0000000000ac007e356a1bedb70fe31ca56a970f746e3b65d6f54d002b4d439a", 0 ], [ "00000000014b2c8f10765caec75f6fc14888d231b1acb22307f7a13c926e28db", 0 ], [ "00000000079cea540986fd74da98751311b2735eb07a745b7c683eaa3f15b830", 0 ], [ "000000000d27fe2305af6080e0d5eee7344b0489cb1c0da19e2d9c537f698126", 0 ], [ "000000000a1ab2ea2304c980c0e84e2920317b554e638cb5da8ed1a6bc6f30e2", 0 ], [ "00000000006e744c182c88b16e773daaa57c890c3bdfa3ba32590acbecdee7b2", 0 ], [ "000000000379c716212dcfba61597cfc096cd8c73d38b1fd3e2ee5e10eec7776", 0 ], [ "000000000000f0051043df801de031dac8d58b7309a586ad22f94467e8192532", 0 ], [ "00000000002354f15017f05b70a8d4cf2ced4e9c699e4e729cee091579dcb332", 0 ], [ "000000000004dd9488c9451e76542f6b5100d0b01998f9c50691916c62b32d76", 0 ], [ "00000000000297f0a1b8437c1c361cb06c230a7fca5a16cc10fc5a9fd5444d3d", 0 ], [ "0000000000003dd884b7190ae6d8535515bb332b4726e45fbe9309e12bd824f6", 0 ], [ "00000000000004aa0500e9668e48ce227ec9d9ae2f68fac9edfea161c2f653e3", 0 ], [ "0000000000a0cbcd8a3d3106851c1f0bb80890c8040b8204f9b89853f646b800", 0 ], [ "0000000000399d0689f2c25d5b15321a28e3ff7a4ef283522b8cbafb9b2d4015", 0 ], [ "00000000004b2624e3a7089c4e438e32193f42a7821455e9d3bc82d525002eaf", 0 ], [ "000000000b219e11ff7d9ca4bab7bc32b30cf6c07fe8b757bba4f1ce7a4ba782", 0 ], [ "000000000000672ec8538e66c5e7880d89d47166238043bd05cfb2b6975202d1", 0 ], [ "0000000001b26f4ec0022ba8732d14e031adf2529f6ace8b2c4c6f44500bd102", 0 ], [ "0000000000b667df345a9d121c978608de555b7bc028c89841dc77aaee768b88", 0 ], [ "00000000008b87d9107d8d72e57191e3482d0cc563988c0a505c40545c960090", 0 ], [ "00000000003842b24eca3193c245c6d9548ab6aa12f585375f24c571ac92a6ed", 0 ], [ "000000000000b40b38ed6426cdf09de75db790198c862dfab1910cd3a180c823", 0 ], [ "00000000000166d9973adc0bf4f130ac15f29b10a6c04cb8e25ce9e6bf07b148", 0 ], [ "0000000000006bb52313e489ba4ced2480a5b39764c76eee634a1d997d1ea92d", 0 ], [ "0000000000003e2b6d802c3854c819713661bd5437374ceea6193de52f69ee84", 0 ], [ "000000000be0c0b056526b6b891040109d45adfb3f34b65c05169695f64b989c", 0 ], [ "00000000095fea1fe858606e16d44802a2a2ea55a4cf8c20e4f489a8ef840866", 0 ], [ "00000000012593712d8d9c7cc07edb52cc2926dcd3cd9d5776c256d9059facad", 0 ], [ "000000000000c427ba2ce29f13fdafc925c19929c5512bc0e542f7d201de6c50", 0 ], [ "0000000002bd5cabd8b2b9c2c1122e681f6e0d19aa8ac8733fa1e89a544a726a", 0 ], [ "00000000007f2a119a05c0113ed25aeea6d6f42c29eabe17fd4fd0eed2218857", 0 ], [ "0000000000a65e7bc4ea0927f20073536ba2bb15185487eb3653ca382c2529bd", 0 ], [ "0000000000026e2c50b815fe655f39ddd2a5ad67363eb346030c72ee468fd861", 0 ], [ "0000000000cb8615b605a76852358420fec223cb1f45b58f36fef593feb775a6", 0 ], [ "000000000000a9d62c3c9e45990ee3e3a0afe8ba09ee76ea8508f3501490f752", 0 ], [ "00000000000d6d07bf649e10f2a43b6f3ffbb3d3b8d44d88dd56968bda1e4576", 0 ], [ "0000000000016cdf6d13beb2a804607bba4cb410019bc15a0033d428f0885dad", 0 ], [ "00000000000046d0925f6aa3cb1035ce6baa03c895dde5869b63770a95135f25", 0 ], [ "0000000000000acc63320accbe3cfa9536987b7f46e995cbc9b6edd22631587c", 0 ], [ "00000000000003492c43af082403eb2bd11e02087afb5c590ba8af828c6a4b08", 0 ], [ "0000000000e1a369418931a7c1be9dca780253897ba64322ec598ad0ff68cfef", 0 ], [ "0000000006b04f242391578d9aa5233c26e6a18557ef7440ea8b05d4e7554180", 0 ], [ "000000000c92c04b11e0408a047f4b3d78b420dfd05668f78cc077e03ec81093", 0 ], [ "0000000003574dd81c51433aac32302d0e30e39703522f39a254207ef5e64dd9", 0 ], [ "0000000000db3d7596d805ac36b95ff841bdb7f95f60ebe2422c2c6ce1b76ed5", 0 ], [ "00000000000bb41a19a11b12d37b2a5cf561ae02d7ff916d37f79d8adc6f2d05", 0 ], [ "00000000000067675fe5b48bb05cc19526474522d1ebf71a3bbe991d95156e57", 0 ], [ "0000000000002a8601d3b33b5341689ebaef6e99051f5e34330af0775953714b", 0 ], [ "000000000000cded0095c064ee88824555bf50916806d36d27a5d5975abf8c7f", 0 ], [ "00000000000007d4f762e90fdb571facd1c5bcfe43f501bae3294c604427cbc0", 0 ], [ "0000000000000837e0ed6ba79a36a447df9395d7220c177a7dc44489766383be", 0 ], [ "000000000000024903240e9236ffa291ae98d057f09c79792bba7c12f49ee3eb", 0 ], [ "0000000000000000237351eac112eff88756b58451d6a272477690b1c2e50b40", 0 ], [ "0000000000000030bb4840680a28b8fa5d6fdec773404f12751b9311815a8dcf", 0 ], [ "00000000069d1ee42ce15859a6c76d7b3e5c4c7b76caa80b26933758b0e55f9b", 0 ], [ "0000000000006369a1181c0496b55cbcf0ed789c7e851b4bc7311246dee03229", 0 ], [ "0000000003fc3d65eae8617cdc1efc6f903bb17fdd30e6a0556fc785e33b0741", 0 ], [ "000000000053dc70c6af06084c6ee584e753ae6bfd18e4f748226ec8e7d8f146", 0 ], [ "000000000165d59df8044f95ae2ffdea9a6d3e218e5543a4ac442d90fcd1116c", 0 ], [ "000000000015c229ad9ccf607263dec26e90ba17b011c993a31b521047295b98", 0 ], [ "000000000009b5115bb2cf88e82053430e3bda4bf7a5776100b647d11739effc", 0 ], [ "000000000001e854436057811dd868b4be38acbfd420d90abf59e88cca893921", 0 ], [ "000000000000e89f85eedc448bf69d39ded3ec12829776e4512706983d678faa", 0 ], [ "00000000000003aeb06e89a38668d5b16d0dce97815493cc4a964f3a4c4cb208", 0 ], [ "000000000000079d25e51546fdac3e854af63829cf1956ca82477fbe6e373bcd", 0 ], [ "00000000000001a4644c28121e5b90a9dc5f574ee7a6b1531ab2a2a73bb027e0", 0 ], [ "000000000000010d3b4c4e22444c3bbcbe32d95462da935e94df0600c36d5d66", 0 ], [ "00000000000000223ce8299ac8293241676dde2a26dc673ce3aaa0d603300226", 0 ], [ "000000000ece8881ee79a62cd6a94669b5529ebda8ea35cb8a5b16f22157bff4", 0 ], [ "000000000051d022d139ac89c2b6adda09b91ae70bd9dcadd963cdef1ba3b69a", 0 ], [ "000000000339ded080a76b132a9ce123ed47b91b849809887179947af7013ec9", 0 ], [ "00000000002f914fdbdf00942b25df72be09c52a35fe5b718da39483543ddafb", 0 ], [ "00000000016b02a598d4e3e3b3cd7a31660f5cc96c68286b5e228c79ed4c5d23", 0 ], [ "000000000126e5cd7cef8f07840a563c23c28b60d043cedf8b4cd9ec0c72a648", 0 ], [ "000000000b0f3b81a581d5407f719ad5eae9eb3f9294403ad85e5f1555af22dd", 0 ], [ "000000000d3061ac9ed91baebf222a531a5473e7d8f979e1bee457e3e1cf6dc6", 0 ], [ "0000000000000f1528cfa6f357f44f677bfd4c92f77e863e1945a86da556f20c", 0 ], [ "0000000003b0f3fc912d574cc0bb312cb35859cd829c54a9db605cbb9bd52675", 0 ], [ "0000000000035d15430eeea3107cf09fbe0bb6ad2a7b21553b7c3f578d2c2b92", 0 ], [ "0000000000005cb1be53a0827995ac577379392c904740854d6f48c2b6c38936", 0 ], [ "00000000000026834560e7c8db380e9c26ecd5f8d6a7c1baae85385f7ba79f03", 0 ], [ "000000000001d0bec9f90d83e118c9e6685eda9c57b2337f4b149f2174138085", 0 ], [ "00000000000003b8d4d3278ae3147815537a87f9265ec6653798f30e45be92ce", 0 ], [ "000000000000038c72c5f71a607f5ed676253259f02b6022603b75f51a02d622", 0 ], [ "0000000000000d3dc9970c1feef03569f332a1d7ecbac5b980f382e49e5c6218", 0 ], [ "0000000001ffe2564d69c58d1440f82873b47bcf513c85df334b4f1ffa537f0b", 0 ], [ "0000000000886705178f570b099f8b2841312d41e819884d32eb76a2d928841f", 0 ], [ "0000000003cee4fba7e8d113973432642840f3a914a79164694cd752e06bc4fa", 0 ], [ "0000000002f1db982508e0bc313b7268f000587bfd1b247e28e553ffbd5ff58f", 0 ], [ "000000000026008cf3dff1367884185aa740203ea5149785fbdbfe40d4ed2851", 0 ], [ "000000000014798b39735397575ae0f4f389907d296fd63c9fb7029b0f7d7891", 0 ], [ "0000000000088f1729049693bfe1d19d256e2951970df4bf0b102f716899b894", 0 ], [ "000000000000f29d3ab2228ccce57b3f1b3ece81937bf780bb7fdd853ce2753f", 0 ], [ "000000000b021a42f081054616a39f815bb183eefff8cc0f852a28ab8a531103", 0 ], [ "00000000023bf7dd7ed3005de65b537c2c8876f2fbba8848132397f7c2e19b8a", 0 ], [ "000000000235d39c23216dfa2f37e027e5cfaec163e0f932bdb2f16cfcd6f59a", 0 ], [ "0000000000021dc9864764ff14afdee090b2e338f414d4e51b652b8102c00f21", 0 ], [ "0000000000000a15856916bee7d7605138b2baabd2e30c124b41b1061196a5ca", 0 ], [ "00000000000017d756a7caa1493256118efe69d7505f1add42ab9f66dca8e8bf", 0 ], [ "00000000000007e7c316d7143d8c4ecd91aa79405907215b87635f1aeb1b54c0", 0 ], [ "00000000000010b865a46467e275a653fbe7db33a3295f9586c969f7af4d91cb", 0 ], [ "000000000000eb5a46ee76387df02b00449911bde789a09ad2817e5f8f768771", 0 ], [ "0000000000001c8fccc5f507a27f1ebe034fedf98135f291096b17de6e490d73", 0 ], [ "00000000000005cf2c8aca82ce281df04e64b8191f96711394a96df2a49ae7c8", 0 ], [ "000000000000027621b9a7ee46c8632ee23b4963a27baab60c8e2b2874ca9e34", 0 ], [ "000000000e9f58dc29cc60e6afae3b3b6603c69f888e2b88569103926dbfac77", 0 ], [ "00000000000012f42df0d44cd1e5bfa062f3bb2f0760cc581d659cbd07c1bd96", 0 ], [ "000000000e2fbad37dc42819b05e83fe3d302c87265c46d82f62eb7dff3efd7f", 0 ], [ "00000000008c95ceb4b1b636e726c3e7cdf80c74dafab1c3d13cb130cee40664", 0 ], [ "00000000000277a72404c15eeca384bbe32fd4b546e5de1d0d39ed6a5a9a3519", 0 ], [ "00000000000dffbab9cc1c0744c8585aec07ba68ab4df86628e079cffadc4ff4", 0 ], [ "00000000000cea01bccef6472e11cf5080901900592072b3c7dc109183ec60f4", 0 ], [ "0000000000010e6f1b85517945fc164c74b6e60d85c9948484f9047caee44de4", 0 ], [ "000000000000386c87e9fa493f75937b7654721e8e411e8899b18e49e79c4f8f", 0 ], [ "00000000000000ec39027769af5367e61b3589d7f665296f5a01aea47a367837", 0 ], [ "000000000000021fef1e6512646e72fee67cb9678d7a3d3208a108f0d7a11c22", 0 ], [ "00000000000001b04b9a0c761f676bfeb77f47f7270799abd749afb210335acc", 0 ], [ "000000000000003e289ce157208586cb8a0ecfc45984c217ce6ae38b28101396", 0 ], [ "0000000000face4d3828727f7b30d3d7c7ba25e80f39076942d64f2b8266524f", 0 ], [ "000000000244a130e6c88855ed65f6e08cf1e24dd05a4562afa9e0aeac869dde", 0 ], [ "000000000104f9420a1ceb288ce3da17e22d3bbc4a427727a44438c4df52afbf", 0 ], [ "0000000002e43dff8a2776df4b712344e210bfbf92e06d90661c59576488cb04", 0 ], [ "000000000083c35652af9b1bab1e9e6cf7ee37ce9912cddb887ce29787aa115c", 0 ], [ "00000000000ffb2fca7f2c1713f8737863bbe8d7ca9bcec97d4ae783b782a1ba", 0 ], [ "000000000001416d9ae0fef049dff1ecfe2cc84573a80a8414254cfc07d9b539", 0 ], [ "0000000000006988c41ed46e245865a8ff5f8837cd31443cb2ba89c75d90b8e6", 0 ], [ "0000000000009d35aa98fa14329c525c8b5065beeab1a5e902611fefbe3f37f6", 0 ], [ "00000000000031fcc3682ae1e909d1d3b4efd1e58cafbe6eef8d3ae5ad4c7665", 0 ], [ "0000000000071a3ae9a9912d030e3c724dd325ddfdb0349a0e0babd1a20f5e5f", 0 ], [ "000000000fcb06fa14d45c00dbb937cb5e456d37862814a97d79527e44ef75a9", 0 ], [ "000000000000586af55ff52933e66d8fa10a4b87b0a85a369cb1679f216361d3", 0 ], [ "000000000349ccaf247304093a4bc06d2699b1c08600375ac4532beb386bf056", 0 ], [ "0000000000001d3c3190d3ffa359605719bc0007d750667b693ecccdf2ae4a53", 0 ], [ "00000000000047a5bd3d3a42273f93dbf0dd06058bf90e256c9ed63e911bbcc8", 0 ], [ "0000000000005910b434e32db0053bb9eac75e1aa1f707cf6e04e1513be209b5", 0 ], [ "00000000000017c0df4e9859b84a83e36bd23377792a7ab218e19bc1712fb0d7", 0 ], [ "00000000000022140a4f1b51dd23bb5e3a33611941628a88a38441457dcf6b70", 0 ], [ "0000000008700121530a0b7cecb70331bff9678570e9791a3ddd1a4f3d7bb4ea", 0 ], [ "00000000000005d11acd598fe7a7382712449e4dfc942469d762d87c7648c7de", 0 ], [ "0000000000000d245ef989d5ae64249984177dd10e1c0b2f04077276d89a0b09", 0 ], [ "0000000000003b8f9bade81533ef7ec7133274c55d8e29f6db4bb6545c2f97d2", 0 ], [ "0000000000a0489089b019a7fae1ca64ec8ef179b352e84561ac0a055675dbca", 0 ], [ "00000000062041b268ffc19bba83c5db5fbbec734c4c2f969d0f02cb33dd22ae", 0 ], [ "0000000003ffa51fff511505a918462d890a0d8e9b3038b43e01ced914ee023c", 0 ], [ "00000000062afff83211b48e13f909c97c8a6cbb0d702df73ac553a7871c2e57", 0 ], [ "000000000280223436db85632d21bb9673ea9ad270260b46b77fc6d8c2b23e6d", 0 ], [ "000000000000097ffea9886e82d5fed96fcb91c1b3c337b445aaaa47966cd15e", 0 ], [ "0000000000005f791780e489a7dbca3f372bb36ad7bad1f1d4d8584f7fba980e", 0 ], [ "0000000000002b8b3a8285b208bc7cba07e08f42c9705367cb5d2cc99604c346", 0 ], [ "00000000000002cd2f944504f4aa6c2e20dea005ca70568b18607a91c590ef11", 0 ], [ "0000000000003eacdfd49b651e0871ecbac1edea9e8c07014c19f2b888d28b08", 0 ], [ "000000000000274849103a544fe5e64a0d97ff81f0cb4fcbea269ffe01babdea", 0 ], [ "0000000000000279b536695aeb598fd6c01adc9f19e5b5bfb9de513a477adef0", 0 ], [ "0000000000082feae81a5123d7562cacd2b60476acdc27687b57cb0cb023bbac", 0 ], [ "000000000000db4029f289ef085ce1e55939b3225ca0e3297d0a66abd9d54c39", 0 ], [ "0000000002583614b4cf8c846f81f4452602f5a3663fb684a7f8d2382f83e9b3", 0 ], [ "0000000000d62998c90ece4773c2251143d3b16891fff7066a3fe7fe42fd6978", 0 ], [ "0000000000ce441f798e9188f58d9075e7a6b54010a69b8320d106584581c929", 0 ], [ "000000000063c5f940edd14e3d91bcdb145ed07d983d72dfe0776474fd5daf93", 0 ], [ "00000000002fcd1ab04af227ead9b7db9c87758953c4dacbff4b6428f6d0c6d2", 0 ], [ "00000000000bd2a500f81c5500a50af4c59b04b6c73dac9bbee911ac8b5e688c", 0 ], [ "00000000000b362c449fea49d85cdbecf81ca83e55b71476e06da440ec60dc81", 0 ], [ "0000000000036d40982a6d48c72de13c448f6796c55c52fc19d58b3372ec253b", 0 ], [ "0000000000002f469ca442dcc594acaa75b0c4cef2f4bdd4c5460ade34f956c3", 0 ], [ "0000000000000225af0f1a97bb54a20992fddef86ef259a52629abaa28b45e1f", 0 ], [ "0000000000000be9a2718132ed7891e27316eac5fca42dc196bcaa0e317e80bb", 0 ], [ "000000000000013c85749bdbe405cb0dc0bdd4e361281ba7a198ce820ab0405e", 0 ], [ "00000000000000f7939565cafaca6f31c9f61f8321ca0fa69211594cb96e7117", 0 ], [ "00000000007d22b331772cf0dd919000ff5ec754e1b9254b90fea7d694501a85", 0 ], [ "0000000000c41b7447a1a24841abdb7c3f2b3d68bf7707966b3124ff594c1da2", 0 ], [ "00000000007976d00aac373b41e6f3e574ff607beba8551dcb7fe6e0a6a5990c", 0 ], [ "0000000000014c8c2b476c525b656908b1ad6cc5f7593f63147b882ca38c5870", 0 ], [ "0000000000bd0a837703bb57aafa84dacb49a264b5335c42442f0e83fcac2841", 0 ], [ "00000000003ea8019a2641d1778bb416f6d9e0ab9b068323654bc55cf7b2d304", 0 ], [ "00000000000d0a442a0865a501b427e6425151f5923606bed29e94a72f35a07e", 0 ], [ "0000000000030dd08b942ffb56e9a8eda95a7e6f28a03b9d1f488a29c7fc14cb", 0 ], [ "000000000629ebd7d6c56c34e858ac6ee105aec8d8db05db5f4b1e7bfec8033a", 0 ], [ "000000000038d3dc0b68fcd3275c38b3225bb99bca973a513afb2fe505362d6b", 0 ], [ "000000000072b439db6364b048becc5254682c31214f44522566290171447c9b", 0 ], [ "0000000000eb455b495292a97133957356fb0e145a593c9920e99bbc1fcbb91d", 0 ], [ "0000000000f3c0c53d7b41b7366a44090d09af626aab3f207f94e0506eb2abd0", 0 ], [ "000000000014463bee2df062f3a0d84752b55b96c6cc16595899fb2452f23960", 0 ], [ "0000000000089435da00b1ff30d48724160cb59877b19bcb3f449d9c279824b9", 0 ], [ "00000000000033ddc62f3518f87c4acdf8c0962e5e0753c86e023537bc73906d", 0 ], [ "000000000000cfaf02a175dc7a7efdb4ee10eb83d3a04678f0adb5c300b9acfe", 0 ], [ "00000000000014a5fd14c81bd67ad64fb81d9672b8d03877a9d3824c97a715c6", 0 ], [ "0000000000000b79c643a371a491bb6a27726102f8797c7d08ddf87ed6b334af", 0 ], [ "00000000000000499f0d78b9f43fe1f8d4a981828049a2546059d1392f2babc7", 0 ], [ "000000000000006494d55236e0ced5ddb0a42dade401b3d77d456e447f666ddc", 0 ], [ "0000000000000003ad1c7710bc02bcdd76fe6fca54869899b9048091cccb93b0", 0 ], [ "00000000089b0706b3a6b5808ddafb57be5f77e51009afbc81aba91ead1af1c8", 0 ], [ "0000000000038dffc62eb25ad2301fc45ccc82a9ca0f3a730d7d25f4cc7695f9", 0 ], [ "0000000000007cdc062f569a2227502e75afa99c7ff7c9fd3e043ee61f5e2899", 0 ], [ "00000000000050f82f9fe703482a89ba92aad0be11aa686fcafba4c7ca0c4b01", 0 ], [ "0000000000005d08ba1d46fef300bd60382be096d46c036622a9c52466fba6fc", 0 ], [ "0000000000001c124114694c9c3bbce398d3470bbd8d0795152541a5de1e8189", 0 ], [ "00000000000047ae2c50d65efcc15af2014d5b0049d244d8dc78b482151f0e62", 0 ], [ "00000000000064a0f4a34ab095a76b9c394069f55bd6bd2b783b5de028b9f87c", 0 ], [ "000000000000f33234204685893b980d24bac914bc8b1978c69d8580fb63f542", 0 ], [ "0000000000003aac07cd725764eadfa1d887b0d4508858f31e46e711aa801e8a", 0 ], [ "00000000000005c12a0970101bbe661f57f63686c48825136bd298b951e86e2a", 0 ], [ "00000000000003c4ea51aa96323c5708289580f8e7b6e89d0d3bdc08d0db94ff", 0 ], [ "000000000000016b84c8036b3b3fcd2f31928c949d4d35b4400ae80ba23b247a", 0 ], [ "00000000031da21f07983eac7205006b839fad1e14f339c065c3ba0789f47a8a", 0 ], [ "00000000010a23cc7ddef199edbe37e42acd2461bc9848710e467739423ce2dc", 0 ], [ "0000000001ce777eefe9a1f1aee20f33629aba343e5782b3f4ec02e41e282a85", 0 ], [ "00000000017972967a46f08c4e79bc29283566c539ff8590247819223cf3f1d0", 0 ], [ "000000000000fdb8834f2e67f6bbb4409935c299d46392c8db426359deeffc27", 0 ], [ "00000000000023f8eba6eb6d59206adf050dc76b2dbf41b66421df4060c7007b", 0 ], [ "000000000000327a31e6e0bf7261edf25cba59c81b895c9d1568cfb9e12371aa", 0 ], [ "00000000000033faab4075e74d8b908cc5b586bee4b99464163798ebde37925c", 0 ], [ "00000000000062006eae8582aa0d3b6877216793ddd70a1028d65e0cb2c475d3", 0 ], [ "0000000009cde9229b24519ef150d00e78249587cd2be3a293b2ccf807f7b1a8", 0 ], [ "000000000fcbf2f23f11d11aec4df1375ee8ad149fe2c32b52615fe01b327b15", 0 ], [ "000000000a26326c9fe65505e8dbb95f2d10bc5915912be2cf3a3c66f4617c2b", 0 ], [ "00000000005e1f0b9aad53b290a662096d88bb719b451007ea9e23ef990a42b7", 0 ], [ "00000000000061bdb1e6cba904842454e0eaa79cfef1d2de098cdf62fae9f67e", 0 ], [ "000000000000238380880d6af14a72d9f5cfae0a6032eaa0a4ef1291161a83d3", 0 ], [ "00000000000007f7088a701efba448b2a3465fb0a03ef2a83215bf5cd9138fdb", 0 ], [ "0000000008b6f1711e2cad14256e563ca3e7927957c3d6779ff381f5c5cd3220", 0 ], [ "00000000081b05a0ae2b231dc43bddea3c258f70a3ca20500773e29a7b5d75ad", 0 ], [ "0000000000000003a9eb8f76429fa078e2e20af75ce5b75da0937ed06cea43a7", 0 ], [ "000000000bcaea5fa7f75beb66951d22d9fb7330bad2b84b0e2039a61f9d5a4c", 0 ], [ "0000000000000558d535b20ba51c6212e683e0414f8430ce4a9a5aada7c7ac4a", 0 ], [ "000000000000379c6eeca100e6f171c71df430611b187043c2b422449560dce1", 0 ], [ "0000000005c7361afbd59825f0243a94c47f62dfc2856f490ce11df9f449b787", 0 ], [ "00000000076248cbffb87ade5efc8be5dfe3329a2ce1d2a488fdb23d48efb75a", 0 ], [ "000000000e283546c050293ac632a61036c18dee9bb810835b661fdfb08f6635", 0 ], [ "000000000321148782953d6b0b23eebf7b7feda7df39204ae52e28d224752b65", 0 ], [ "000000000077be32cafd14823697979dfcb4843cc11d8d44f2afff9bad1b6d0f", 0 ], [ "000000000000567176a554c5a19cab87b7f0a86d98402a09d6613c0f389ca567", 0 ], [ "0000000000000b1996700d0427eb92eb52986a3f4639c9e11b8ce37f2dda66aa", 0 ], [ "0000000000001b3e599b0d31519e75ec45aefd549df5b6028aac6d4d69cc829e", 0 ], [ "00000000000046df035e6fe190b203dcb62c9a1a7cdb08389c79d3987c6dd8d7", 0 ], [ "00000000000003e9e7a602aaebb90b73d5004844d5f6f24925b14188e34afc2e", 0 ], [ "0000000000000e6263183a20de2b098d31b20c4b857f6d11c523755f5c287fa2", 0 ], [ "00000000000002cb548427aef898e94253e0a0ec8ee53c000813551229eeaf6c", 0 ], [ "000000000000004ccbf0d92b3aa2285be79b17fbe0fe261ee1c682cd1dd927e7", 0 ], [ "00000000000000314c5668fca293d377ad4715bd2b5379318ae2e81fadcb1502", 0 ], [ "000000000073f9d5a07e50d1701103c1c12506c54af4e2d96736b267e3b8b3ae", 0 ], [ "0000000000ed2345b418284bb0378152d0bc8f360844955f1bdc1797f557a621", 0 ], [ "0000000000001da2c63c314956dd2c633f40f8cfefc034479bd60089fad02b88", 0 ], [ "00000000091985d3f909245582e26d7836edecc5eb47515e590a5d0b08940de9", 0 ], [ "0000000006a654ca1a6bb988584b478197fd0eddbfa9edebae55e2f419712212", 0 ], [ "000000000c36097cf9734e7a3fde243870fc9122121e34a9a22fd235c0ce3bdd", 0 ], [ "0000000000001ed546608547145905444cf18e7d27d537bc3253b295ebf6164c", 0 ], [ "00000000011c08a153b85244c93e601cfd715845ef7bfb8678d52d72eeb33fe2", 0 ], [ "00000000003950ad79520b26976725c64556e65d21c7b5029d467decc056d30f", 0 ], [ "0000000000003ea7f0a49bb8e6be6c09de704de3a8d329e018d5f34cd88433d7", 0 ], [ "000000000002048815f8a57a4688e0ff2ec7b0c6dfd5a855082097e16c1b4f0a", 0 ], [ "00000000000021f8ee9909f7102cab8462d8d0ac43a451a864bc792dba0541c5", 0 ], [ "0000000004515238ac80d8e7ad1403ef3c97a9651493c9de89a4d6ce06bfe858", 0 ], [ "000000000ed4662b59df94409e92509ea67f0753396010c136f5652cca559592", 0 ], [ "000000000faacffdbbc3459c6490f7d2d058c40913ec9b1efe2ce0765af4c993", 0 ], [ "0000000000001adb00aa71e333150e5ed06bb90e92cb871d74e7fad5c076071a", 0 ], [ "0000000000b3091c11666e2e25f66ceb5cc89c43fe4cad628ea96bc5688ed556", 0 ], [ "0000000000001796f7dca239170f18edea5d2eba4ad7ace947c4986c9bf5ca06", 0 ], [ "0000000003504db8c65ba8631a483894da912c31ab48805d9d75a69456e6cf93", 0 ], [ "0000000003f643be16ab8974aa0dc202a16c77a333918b715dc068859ed60ff1", 0 ], [ "000000000ff7209cd7acc0e8be21437ec4afce623a80a44c05a655aa3423eae7", 0 ], [ "0000000003d1fa3d1c9a8082d1c12da56b3f537b5cab23295dc03187923dc33c", 0 ], [ "0000000000392fc59c41a98a986cd160c1ca491fbe3d638bf9acfc9f565344c3", 0 ], [ "00000000000077d2ef23f91f9eaec66263551275f1cd58e42f212cbef1f1514c", 0 ], [ "00000000005360c2362e89b2001957e8563f2f29bfc5c67453643f972603d457", 0 ], [ "000000000ec466e23f09fad26ed11d1611d948e82327c21392bf1ecc761d9dfb", 0 ], [ "0000000000bd6cf941aa95726e74c57689dbacda79c61f61bd099a3da98ca08c", 0 ], [ "00000000006dcdae735bd55c3a6472c3d8b62d6ada8734b396ef768dc160edcc", 0 ], [ "0000000000e8d3a687d796d63c201570bf58e3a2bfc56ff5b2b98dba0590e8c9", 0 ], [ "000000000077c5459d889081076e7bc3596d1afff69401e3ba9a747a41527075", 0 ], [ "0000000000288bc04badcaeb4dad4c95188aa2af23b9713ab516acb7a2ba08a1", 0 ], [ "00000000000018f0c4517333aad28a6bd17bf6599b23f30e736f8b81453c91b5", 0 ], [ "0000000000002bf35f1ddf7f173451b8fdd99b09ca7e7acdaacd741a2ac4ee78", 0 ], [ "0000000000001b73317bb3e44d0e0c478b607ffca894cc64c4cb7aa46372e395", 0 ], [ "00000000000036f9c27a5fb4a1e3408c0b5af23433a8fb914d1ecd980a78cd8a", 0 ], [ "0000000000000db0c1c9f82751891801d681a1c45fb1c41595c81443f0a1e950", 0 ], [ "00000000004a8225a3861f2d8fd6a2c4f2c97f9580269a3d47c4fd36e1f835e1", 0 ], [ "00000000005bda823369e5f94c49834cd98af02630cf33bf63a5126d81aeedad", 0 ], [ "00000000096bb045d967239d012773b9c74995439fa29169586283b9771ffe11", 0 ], [ "000000000fbb4587b5a097ed17d5a3f4611a55c5697df344759e90b2074df8be", 0 ], [ "000000000a34e610a3b5a3cea87bf4dc50cf5ef4f4256fbbbf713850a1931750", 0 ], [ "000000000c7115c2136182db49423e85b3f03b72df0095d8b48d41a6e7a5c4c9", 0 ], [ "000000000dc75a70535dd6998830eab1201680703badef3cc0c506845b05b411", 0 ], [ "0000000006209e2c1585e816bf6ff79aaa52d709e3e15d8c79b907b4b9af482c", 0 ], [ "000000000df4fa6c4fb5d4277ca22c2b3432aa3d3d4d34c93fae21c5c6e18f13", 0 ], [ "000000000cc1645c4f9dcba4ab347a632d3c3d00b0ae56f72f9a8791c2589ca7", 0 ], [ "00000000035de13d1b950ba799262cc730f65cda1ab33c86cfed67c14ab59b12", 0 ], [ "0000000000003c0666573e408040e408df8265c1b24d4ce53cf6efa00c289d51", 0 ], [ "00000000002a301f000f086912f45dfa1a6608d6d2a14f1eceb23f852cae290e", 0 ], [ "00000000000774b67925ebdf31b1f455db3ad534fd9aec0e43bb79925c77b221", 0 ], [ "0000000000001b8f1f932360a5936ec6f007ea22ccea5579cc8667f7616977cb", 0 ], [ "00000000000035a0698b037925a892e4766d659ff3f7979209552a9ca646203c", 0 ], [ "00000000000001437edac6d3e1bb669d907b93ad506f8c096a99b8a0f8cca1b1", 0 ], [ "0000000000000e1daa4bf888d63a83097c13cc37318174868d6cf47fd5951b38", 0 ], [ "0000000000000067094d226dce682adf9b13225891e3c4bb800cf7087bd20804", 0 ], [ "000000000888d73db90a3cfc554070f42ab6e66d7ed603ac31dcf75f56ea2496", 0 ], [ "0000000000dc11380abc06c3c9919c140ff2f79ee854457b7ec6219111b24a07", 0 ], [ "0000000000002519e28eaeb2a1c9f7dbab73f220408b2cadffc4edf2ba252e4c", 0 ], [ "00000000005aaf523f208621a67f1d79bbc49a55d23fc174fd51d5b20ab046b2", 0 ], [ "00000000000017eb2b540a7ad338edb2291d3a718624670f0e1eee89e78bab4a", 0 ], [ "0000000000000d3119520427d81eecb014cc8911390560ccfaa21932801a5e91", 0 ], [ "00000000000d6e620685d7f868d6164e548c0296b8e32263cd557cb3a38826a1", 0 ], [ "00000000060ba49b218cee56dd5fa41d66c45476fd9f3da6ec338df98f8f3af8", 0 ], [ "0000000000180f7317cc1d110cb221b6767d908b0c6582c859e78c9f04dfb800", 0 ], [ "00000000072fea2d890c6368162518bb9ad345adae68b106cf15cb20ff241350", 0 ], [ "000000000026fe3d8e277d69708a69cef90a7f2a761796602ce9112644518dae", 0 ], [ "000000000045c77f25f1807d143280fa8464ac6df9c7a4b56bc4e34a9416b9ce", 0 ], [ "00000000000542f8da30137f2283a54e81f586985f924561e558d097bf972a9e", 0 ], [ "0000000000001c3c44d10ea8bb958ca80dbfa5733ba66b8bed179b05d6106d5a", 0 ], [ "000000000001c1c53fede30f9b35b3cc05605514e2ab909f4bcff86bde884f7c", 0 ], [ "00000000000009b60a8790fe0524faa3c2399a3791fdcc217d21d02cd935c287", 0 ], [ "0000000000000d86b1ab286a7d2b41db146575311106f58cf3117d831ca81ddd", 0 ], [ "00000000000002b444cde3ce10aff0f4557d918ed7f7d3d33e217e2c4b4d6ef8", 0 ], [ "000000000000006c1bcb0b8650557494a2dcbaebb6372dd6a893f372da0cad56", 0 ], [ "0000000000000032288278d76032074a25b0f45b2b470a99b08641a0b789b085", 0 ], [ "0000000000412b6c96e0c0382e0d8d154f567562dbdd6d515ed3b008fe4c8d7b", 0 ], [ "00000000032d4d5006d3753adb0a9c6e0a0a320adb6de9a14fe89c184977fedc", 0 ], [ "000000000b8aa72c0093b8dbf56ea257c2aa752467ce63be33b4009429932b3b", 0 ], [ "00000000000036cb2a9820fe0fa9cd6b28a0226ff4d7c0e06f3f9de336746dcd", 0 ], [ "0000000001010dfc436ee032a16952fd8ad1342752e7ff2b0244d150dc6118a4", 0 ], [ "000000000023567623e752cfe32267a6ed9fbbff4f167d88f1e3f9c3528cfb54", 0 ], [ "0000000000001ad1b428da9f6e81ee3ad109b4c31532842f59dcd03fa1d830fc", 0 ], [ "000000000000555f57a00b2f4ef10748595582e34ad13241838217e295eb9d46", 0 ], [ "000000000000bafd77624538518bee5f02e497de67285b907c7e7a1de38905a5", 0 ], [ "0000000000000aa8dd2bdc793b12bbf8e548fba5e8b35944631c326d4eb4b0ee", 0 ], [ "0000000000000579a497cd1f9e7bde38f0baf65366cc6ad2073159977fc9f332", 0 ], [ "0000000000000030ba581e96eee41480ed631a7c2f6d67503f316760491079ed", 0 ], [ "000000000000007ce65511b1c34e1445607ea53bdecf9fae9b2e0eca10a6064c", 0 ], [ "000000000721e295cbeeb7e1d460d55d6f48396139e3bbe5000b76256d87e936", 0 ], [ "0000000004db6aaa2d703469e774cdc2dfa758072c3b32bc1498494b7333ee8f", 0 ], [ "00000000010de49ec83b2a5ad987e222639c489a53bf7f6b1a9e74baee07dc05", 0 ], [ "0000000006380beeb928bed1a6b15d78f1456b775a4fc40ddbe98b7189dc000c", 0 ], [ "0000000001699320ce4fabdade058c18d404468378882bb8e2e3130502210956", 0 ], [ "000000000086702466b4b3d53a45c78acd02e6fcd4060915482f7bcb8ab18825", 0 ], [ "00000000001fd44a586b1f3c45d531d74a21364cbfa32922a5dff0747295c201", 0 ], [ "0000000004600d0aa3fae03ccfb2f52c6cda7a2f0c0377ac93f0ced5705c8083", 0 ], [ "000000000f3524398295e430afabf4f947faceaaa913e7a50d52bbd04fdd945c", 0 ], [ "000000000fa978185c0f6b6b08a5d3c920528c3044da2af5de67066adf8c20d8", 0 ], [ "0000000003285aa7311c3522e58954a630cd15df159f312bbf77fe30572c6680", 0 ], [ "0000000000000fb07884c4ff829424937d02586452ba4eb6285feaa27d68d59b", 0 ], [ "00000000001944317acf0e191bd6dc7e743cdec33fe09ee3c0d4b540e36a8202", 0 ], [ "000000000004af4f58f7974bf4434017104050fd14cfcde5473e84239c9e8507", 0 ], [ "0000000000002531b5caa3f5f0b58102a04c4462998dfe8790001b39823619c7", 0 ], [ "000000000000fd6e966ce4b55f4c8319fda3aeb7ccc7f0162879f85050454499", 0 ], [ "00000000000001f4b22e4bce9c874dc7f81fd81de30890bf5c0037c132f399da", 0 ], [ "000000000000013703a8d800d1e40ca4db69243003ce1f32eef75cecfbce34c6", 0 ], [ "000000000a9261a8d5f86317e4a4ad56ff96a95b3ded98f22ece31e9e7e1699c", 0 ], [ "00000000000ffc1562d4903732ec53396d298a488ed52b2d4099148721f47185", 0 ], [ "00000000000f57177045b1df6c0010e813089f7fccddf88ca9050008e043d5b1", 0 ], [ "00000000002f57d47c85dbe00927776e12bf3c332d7848e2668f7cc1412be086", 0 ], [ "0000000000e1cc3c5215f5a848f065ebb3a43d30bc4580681ccc32dd7093e82c", 0 ], [ "00000000001b961551a1797ace18d54443928afab8b53a9d883b4d33c7065794", 0 ], [ "000000000006eb5f5c91d53b92f2f237abfe4215e16aba860b9c83ff4531a363", 0 ], [ "0000000000028a7ed33b6625a08785a54f58c6e40c686a8868bec43ea07cf524", 0 ], [ "0000000000004c23f5e2291ea84114504ed53c0cdfe39fd0962ad51e3f44a71a", 0 ], [ "000000000000359cd2356a0ef9843a6b0b089469e20f39d86de6336167e3bb31", 0 ], [ "00000000000007e77bf73bf030331c461ab57c17c7b52e1789b1992a3bc6a036", 0 ], [ "0000000000eb8fd6531c52d17c8b78367415b12b946fc6c906d63239b85194cc", 0 ], [ "0000000005b862746087e30836e21d6169e88dcc891edc9965ea8d3c6a5d1ca8", 0 ], [ "00000000004c3948942914866eed7fdb5b21c8d6f7926e633d4ee463661f1236", 0 ], [ "00000000000f838dc5fe195485eefc05e57064ff5744c8ce5a40fb038d611ed3", 0 ], [ "000000000018fa4d5ad9133b828d1d4847017f9c28f266858380ac84aa48bf20", 0 ], [ "000000000034b78e1088b935f089917350ccd7183dde2e70482bbe70fb802377", 0 ], [ "000000000000023432a73d4cdb6a9b81e64cfb0e7465e2cdfb3df78140622485", 0 ], [ "0000000000000dbd6ea9ea2f6c0846f78890747eb7de4b83727a4e9f769a3bc7", 0 ], [ "0000000000004add97d7579bf34384dd3164cb25d4c5add6534401910de4f2c7", 0 ], [ "00000000000002053b7bf544342efc88c71785472fb1f4f92b592d9bba7ad26d", 0 ], [ "0000000000000c2e80a064b77191131706bebc1196bac9ac913aa63f44a3584f", 0 ], [ "000000000000008a76191fb3b5e341056c982b5a0631c81a7d23652224968841", 0 ], [ "00000000004fcd575e591cec98931fe9b35df1224b6ee9cda2915ef2d6a907fc", 0 ], [ "0000000000001d6b8d2caf598365e3f41789bde086859a9c093ef5c6ac4531f5", 0 ], [ "00000000000d778a5dcf7e84197ba476035f802a17562af2a6746cc46c6a8ed0", 0 ], [ "0000000000029a99f42369b9f1aea9bbd47390d2bacad09a40637536183ee2a5", 0 ], [ "000000000004e100d497e03ae45b7d039127985bb66f822f7d0b68a2919e0c1a", 0 ], [ "00000000000a961916fe000f2b7761f867a16f2e8a29bb6cc58dbac63c26b71b", 0 ], [ "000000000004e13f7bd19836f89f3a8757ce80ebc7c4ce2041dfe196966b9b76", 0 ], [ "000000000001d5530f7faa64ac828bac0dd66f058bdda86f320ccb4ea6b4705c", 0 ], [ "000000000000184818f0675a919869bce1ad0484f5a0734a8a3c6678716b35db", 0 ], [ "0000000000001d3cef6965a0591426ac446aec5e714b6da2f698f240a8c7b297", 0 ], [ "0000000000000a6251cb71b0e7c79d577ba70f502a0fc8a235188856f062148e", 0 ], [ "0000000001a0d7ae29d0fb16055f476831d372d3c3b9aa6ffd30fc3136df2618", 0 ], [ "0000000001b5c42166bf291e425ffe152401360ef771fe656a37612a6bc9c32b", 0 ], [ "000000000ee998ed1c31cd407a8ab2de2d119c1d38fe166f3a133e1b529162ec", 0 ], [ "000000000f6d809b34d3a42d9b4c93271c62e225e5a7e1fd20a9ed92af3bd988", 0 ], [ "0000000009a167f6d9d8cc6852aaf12be5f4b3819929a6e12a372f66296a8e8b", 0 ], [ "000000000345f1453151412ea96a82be32be8fb6ce6d646ee608b8bc8ae146b3", 0 ], [ "0000000000be58836d26e311860860cf884d9d098abc2cdd6423ba1b2b2e41d3", 0 ], [ "00000000000010d95c3b409b8ea4df1780d3c9f9c7ca087b24c7c0c141fc9c97", 0 ], [ "000000000b81334ce2ff301cfc3c94c86dd02bbf72cf3772278eff39cd71832c", 0 ], [ "000000000c2bf411d96473fa1c463950040d008ebedda1189a1cdb54d7bb31f2", 0 ], [ "000000000e7ece2a317027c09c9cc2210c177a865bf9b7c85012b9854f2bd850", 0 ], [ "000000002731fab33587e08e23ac94c1d373a15d7b9875766188845ee5a969eb", 0 ], [ "0000000008a30b71218412c43ed85a015fc4fbeb1dd2243992578b501407a0e5", 0 ], [ "000000000f7e4666ec661ed4ad11b4767cdfcce1208e8cba788d74a9bad05b1f", 0 ], [ "000000002503bfac95c7fbd3dcf3ff56b9d555c5e604f5f36d87f2e19eb0d353", 0 ], [ "0000000000006abbe507bdea8936ccfb72c08195aaa81dc11b1d3f550d4c54dc", 0 ], [ "000000000000ad055b6e8966c482af5305e07754af2d8d520d5c70bece826807", 0 ], [ "000000000b342baee465a347a4569cf50ab95376e59f87bdde38d9433f6428de", 0 ], [ "0000000008a12eeaba440680a497d1323f27c4a27342087ec4c0460b12a7805f", 0 ], [ "00000000086695a3bcdb7a9ad86f91f6da9da221197983bcfc7ee5a2ba69da9a", 0 ], [ "000000000bab17aa2bb225314742c07ccb247feef53a4cbf08d65622e7bac06c", 0 ], [ "0000000007f776d9cd5e718b2677c24596029fe9c8694a208b75e1170f534735", 0 ], [ "00000000000004ec0928faac1f3a1031b2ab5e462936419c0ae17124dabec4af", 0 ], [ "000000000006a2cc94305823c0a5d30a03f1340d45c36ce754cd299631bac67a", 0 ], [ "000000000acbd3a599053a3b03901ea2bb4cff6ca66824b064f2d901ad66bdfe", 0 ], [ "0000000000001148be4d8ab2acf66cf0dc68f636fa394a059cc6c50a57ac579e", 0 ], [ "0000000000eae9077d1dfe58b371c399d3e73f6dff18e9f7bd52fa5528734af8", 0 ], [ "0000000000343826ded7a6de205d98ba2b4f4f64ce05af8275e6bce684c62631", 0 ], [ "0000000000094454634a162499692b4b625fbfb119df402e2c42dffe54feadca", 0 ], [ "000000000029f9cd34b0f879107f884772c7a345a4e625cad45055f5df847629", 0 ], [ "0000000000067abee9818a634edc244ff42eb6b71cd6ee88d41b1791d7bac8c3", 0 ], [ "000000000000c4c96ca78a2607c59b175dbc83c6a386964230dd9a29d59f3789", 0 ], [ "000000000000715f720c3ed3e8a988499cb39f9b84f448fc7e14546cc62c44c6", 0 ], [ "000000000000358b18bde41a77fb5e97ee1185e1be83b832c18d0c2ae38a073a", 0 ], [ "0000000000000375efa661ab6166a51e19497512052940d8218b8d893fcacaa6", 0 ], [ "00000000000003af2ce5930d4c686f2630ad58447ef6e5da36fb6d0c5c73acb6", 0 ], [ "000000000000008ff35d010d7f94b841f37f2c9801291acd029e49975c6941c0", 0 ], [ "0000000000000011f4f1c7929b38eadfa067cf148026cf40a4cd0f87747c07b1", 0 ], [ "00000000027219ff91c48b554b87d0375e2231946d6eb4ecc547f4a7234f6d18", 0 ], [ "000000006354b8f3148131ffdec12cd0b63c76c0c2a0e037d0725d2c24511ef1", 0 ], [ "000000000000037c25c08ec40b55159f87e78cf00df700e2ecf9d5fe537a0e7f", 0 ], [ "00000000000d74e182a79f8ceda21b27f63c6622c66e75512b617e3ce8a736f2", 0 ], [ "0000000000cc362906f644f2a649a9f17a9e3a08e123bf08971d829cb92e448c", 0 ], [ "00000000001dd7a9253268e93bc0ca0cdda096cbbf14445a5a979a17607207cf", 0 ], [ "0000000000060fb6d03127b2d0669e1b061b730c20ead99200d5ec8f0028975f", 0 ], [ "00000000000008637fd1b69af2bf8b94065fa444382cac2b4745484b203e6822", 0 ], [ "0000000006c7f6d4c0bf9d2b313d3039a3864c9baedc9fa0f829b8c2c65690ad", 0 ], [ "00000000002512b588ec16f52ce7ea23ce183d5956b381a951f35373814b5382", 0 ], [ "00000000000c3374ff0eecba8f087b61d7e35d8e66b57d47b81e0c64c83ac526", 0 ], [ "00000000005e81310d1afe6822be62de5c2137334ae33ab48770023f07c59c84", 0 ], [ "000000000002802db6ef175423f18eaeb1a1b4a31b93ddb9508a78c37380ff1c", 0 ], [ "00000000000e324cc1ce853af182634f58096aaef2725d9220fd6a7ec349703a", 0 ], [ "000000000009a845400bba282bd1e2907e6423dcf74fb1fb6f96ebaf6abd9b84", 0 ], [ "000000000000d81431902f3998dc0283a54ef5652fc8df091bb65d80c996d98c", 0 ], [ "00000000000066bd25372ccecbe69d487fb2190d81a8e67679e484d541ff83f1", 0 ], [ "00000000000002a470b897fa91318a1f34ad9d7dfa1203cfa6b7d106bd8cca07", 0 ], [ "00000000000000a9846dea5cb42c990fc532488d38e92ee7faccf66f91fcdaa1", 0 ], [ "000000000000004084ee64746c686cb4a021ad22a97588f6c503188db6d1d917", 0 ], [ "00000000000000142ce2586d6439bfafd0d53d32ff41c1d56602e394f32edfda", 0 ], [ "0000000000000071fcc3a3cbd37aa7feb1c54580e532ef7219a9a24648b9ab09", 0 ], [ "000000000e155f0afa09985dc14fe86f8121819c6d5e455e7773e1b27290b2e6", 0 ], [ "0000000000001a44a33ef9aaa52457c6ba952ee215bf1c75afb31b51ec65a59b", 0 ], [ "000000000000001dab82737f9c30cd23319dc19f129f1a6a958e11443bf96c63", 0 ], [ "000000000000320acee35e29f344c28a6b0e61d930e3454b41160e02547fab38", 0 ], [ "00000000000023a7455cfc34eb472b2cf2e3cea92c075207aedb73a843683e5b", 0 ], [ "00000000000003cc17e82a58382a746c79ce383fe25387a18dc4a7162695b24b", 0 ], [ "00000000000004a50ed62cf7fb18ee8d8375e9df71fa6577ea66690d2560d769", 0 ], [ "00000000000011c599d9481072c314e1d6f9dd7ae862a8c8afae2777b71d0340", 0 ], [ "0000000000001b1e2885bf70ac03266d7bbd77d99da7216be9d7e3055281243a", 0 ], [ "0000000000002f9ffc72d5ed2db1dbd5d4421e848a486ec0740cc23099a1d6d7", 0 ], [ "000000000000181ad0f3edb9bd9dbff6a4bc4081c72e6882ecb0a7ced233037e", 0 ], [ "0000000000000360d9d309f32e14dc4dc2cccc57715bcd0afcfaab3c305fea59", 0 ], [ "00000000000001954c349d2b1dfe936f34ec475aad948cb5c1033ca5d0a7e385", 0 ], [ "00000000000000800aedc44c721f51e6ac502ecac49734e1e085d6f04f45d265", 0 ], [ "00000000cd7d09dda2b4d4116552fab663ad600ec316fb81fee153ec10e56da1", 0 ], [ "00000000000032520799302b06cec40a5181120451a0f61183a5c60d75c77a40", 0 ], [ "00000000000033982563fa344700e74b209035cadaa0ef965b99d1fb7b6dcd32", 0 ], [ "00000000000028b83e4a14503b6ec74f8333a77e02b4911af4fec814eef81695", 0 ], [ "0000000000006c0b30adc040e886bd7788d62a8b41ea1db9dcbc0799e6f2e737", 0 ], [ "000000000000014a918d5e373be7f26e3a140a7fae398f32479971242815aba2", 0 ], [ "0000000000002e992b9179f941e33aacc33e1fbc0d68f47ccf5df1f9156770b4", 0 ], [ "0000000000000d09de158157254cd426a88b8b61073a81775aa14b90df3d5133", 0 ], [ "0000000000006e2930da07f5be918af235aac0d8c51b20314399066a7ae9b8db", 0 ], [ "0000000000007d6168adc3577a27c3093c228d1d29b924e44ee6553d19627e13", 0 ], [ "000000000000195f3294a3e529151b1fbdeec735d0ca2f2787714dc387392753", 0 ], [ "0000000000000101d519d0775e4fddd0c73d80a210cbfe720899f8cbe41976bf", 0 ], [ "00000000000000133a76f07bc3fd48a608e48a3e4cda84e577c8b923bb224f47", 0 ], [ "00000000c577e64709e648ee42fa3dba41d59175acdcd99b644ba7b5ca2e5c40", 0 ], [ "0000000000001b8dbc7720c23866311b9599e45256b568286099d90661c90e3d", 0 ], [ "00000000000005caae0c34a171ff49b9e887357644f44a4917ffccdb1f42dbc4", 0 ], [ "00000000002f91f1dd7cfb25d8ee2f61a995e50d0ec22b077c042e1de7b5f6d9", 0 ], [ "0000000000002dec1164348f1b13e7198015ebb69d21d17f1db540f9f82254d2", 0 ], [ "000000000002082aabff07e386e279c12fe27d45e9929b13ee218bcd42cc917a", 0 ], [ "00000000000016e4462971bf4c30bcef675d766829063f337523bbdf39ad2dc8", 0 ], [ "0000000000000ff9b10fecfaf454ecc47fe228d9f8412b1e24aa3c9634109660", 0 ], [ "000000000000262c9ede4171783f97e7f559fd7dbdcb95e7d89414f3e91774e9", 0 ], [ "00000000000020a280fe02e039f9ac8f443afcc0f17869f2bbbe9f983b7ae4f8", 0 ], [ "0000000000000d214d8dbb194e92c0de0d817ccde5d3662b20bf41c39ba7c46c", 0 ], [ "00000000000001140f819a403f931fe6ead3c996781715ae1a2bae7108964dd1", 0 ], [ "000000000000006d98a1ac170738d4d9d31173f18d3c4eea36b8b2b39d6c4dc6", 0 ], [ "0000000057a23ba17eab7ea1ac8f5607497d80f17add1683166cb7dcc4bfb1ef", 0 ], [ "0000000000003a5db5ddfc931eb257c1fd8240f56289b987ee7e12563ece7e91", 0 ], [ "00000000000000ef1e4978348aa888a63d97fb20961e22f51b88e80c73a0adf6", 0 ], [ "00000000000000ded525277b70d1c22012dbef28f1abcc4519c9b1330d1c6b98", 0 ], [ "00000000001884ae12a9ddd3a274af90e28fe77c7d53d4e25ca614cde1aba3ae", 0 ], [ "00000000003db05cf4d82f61a1d52cc5d958c21f516aa3c348d39a4d7aa37f3c", 0 ], [ "0000000000121061c34c8807ba8036093628ada5e046d36d257a2b520d29bd30", 0 ], [ "0000000000001c7d96178988db6aa1450281a8208b67ba6da8d02e25a9ad9cd2", 0 ], [ "00000000000038a153341ef0baee829b4e5c6208710b145cd4492f644b825ddc", 0 ], [ "0000000000000d2f02bf068bd1344713ac3a52dae68b2431d860e85ba727dcbb", 0 ], [ "0000000000000535659de712f2344e83fc893955480c4e0d18a9d959c4ae35e2", 0 ], [ "000000000000033daebb675ae26fe3e3a2d0fcbf281f70fb40d1b1b1f41104bb", 0 ], [ "00000000000001373a277a18b25690db1720f038e373e45b56bfe4402418a293", 0 ], [ "000000000000002e92327500efa91d3c0352f394b2c6b205e1b2a8a0323f1ac4", 0 ], [ "00000000fdc454e72a6d98495fefe2197fea1ad03e6202c23843cce9a2962f09", 0 ], [ "0000000000009d979e5a4dfd64efe714f96e3a77f9296a917d6b109e76457c36", 0 ], [ "0000000000001cdafba80309177c007468e7a5e9512663ad5247659194f18802", 0 ], [ "0000000000018ff4e57f8865ea7de7725896df3ab4e5c6077b0a6c20df6511bd", 0 ], [ "0000000000040f4902037267b28a2ee8e87dca565d23f0e9723dd437782c77b1", 0 ], [ "0000000000004397345295d78ae41518a4af486f7ebc990dcc67bb3750c828ab", 0 ], [ "0000000000002174f6cdf300e2f9c2706850e0dea1f0119d9b0914328b93a4bb", 0 ], [ "0000000000039a00fb8ff791b9b2d83ba37485801d856bd9e7925ae540e4dc5e", 0 ], [ "00000000000004694e5fba3428cd48c9858cf6932f64be4f290c95fd9e393063", 0 ], [ "00000000000019a0ae2b62d65095889aa0fe28a6629f465e51cc06d56061d134", 0 ], [ "00000000000005be0e01d3cfb0e689bc7d9fe50a74f3dbb8e5cedc5792f8f1c7", 0 ], [ "00000000000000227b2f3dd83533e9fefb68bc59d62ec71244acf616552533ef", 0 ], [ "000000000000000bdf712efe085711c72387eaa76f5d79606d135d147bba6fa5", 0 ], [ "000000009f5b37f19334d54e6a778650ddfa021ed10b3f0948952c858f71aa11", 0 ], [ "000000000000389f3959130c192582eac8607121d2c8452166a4a715fb5c770b", 0 ], [ "000000000001b49aebd3997878570e8d1d78aa4062bdb83f72b178faea1e1599", 0 ], [ "000000000000ce62dcb191b681f6d552389dabb663c0cfe0680c6e308a443ac4", 0 ], [ "00000000000053cfbd088bd7c065ff1b24bdd0b343d976de7d5f0da1b2cd19d9", 0 ], [ "000000000002e5e6a1f262f0dffb01eba71ff07acb5ff901d8e977d8fc17322f", 0 ], [ "000000000001b494fefce702f5ea369d5dc91b4e517c570b656a83795bc69e44", 0 ], [ "000000000000c42a983d3e4a9d7b331ef07831d89e321cf28bfb9e049bffcaa7", 0 ], [ "000000000000d69faa189d6bece30215641349a60f7eef40c28b3e0d8fafdb3c", 0 ], [ "000000000000062f8df83420fc0db7bc81dcabafa3de817eae9a7988f727e4f5", 0 ], [ "00000000000001b2ef133ac5d1c35068e8a28a35bf39527ec6e9fe1c22faa94f", 0 ], [ "00000000000001b0a6a20b50dcab2725f528f062e1b8eef6ce04bf1398cca5b9", 0 ], [ "000000000000a1a77fd497e4166f9d49336e2fa1910be398e99e673a8c164db2", 0 ], [ "0000000000025e2694cc21be786db74fd365dbb0f4da514148ad99e4f2ebee1a", 0 ], [ "0000000000030422a5adcc3e6715937cf36b180d78a5171a786413afe5cf92c6", 0 ], [ "00000000000267cfac724ff60632456a778438c2754351694c8924f4bbcd5c79", 0 ], [ "000000000001c6b251f0b7a1dab3490af8bd59c132c47c92093e385a654d00b7", 0 ], [ "000000000002a52cea59e1aee38b14ab4df4a61d3c33dcf239c617ba0c2a8414", 0 ], [ "0000000000000035326f862d2769a4179098a9790328285e41c25c591f3d8b47", 0 ], [ "0000000000035473af9fbe2ddfbec0f70188b341f330329c5b0bcaa5159de523", 0 ], [ "00000000000007f012e2d4c8a02bc44092f4c2caf2a8b42db8111df9a40c301d", 0 ], [ "0000000000003dbab565a793a2174c70e3d9bd059a72d70d5962f33afb0508e6", 0 ], [ "0000000000000875cfa1b9ef3980e432e0b94e631fbbe26c5cf4be48d1f80d5f", 0 ], [ "00000000000000d7aac746ab42e51f5ea6bda208909be3b73ee35b1d529f0549", 0 ], [ "000000000000006b4a360564ade70833bc8ca4077e54693f58c7d658fa42060e", 0 ], [ "0000000000002d474eabb223a62c70ae60261eb7e1b60774ab7b89bd1b4e862b", 0 ], [ "0000000070ac5425cbc994b059cd7e379e600cd608256ca371c6bdcd80673e70", 0 ], [ "000000000000157983c64e148ee82a1b956b3dfc5961e4193c6ed9a01babf7aa", 0 ], [ "0000000000001bdb3697b4cf9c73e5494a6aceab8e26a767af183a14adfbee96", 0 ], [ "0000000000001024636de30482825e12a55ab284beac76dd32bd84008e7a56dc", 0 ], [ "000000000000077901852d1720e9fe55b317b1d707acfe7eeead14ca39d3e858", 0 ], [ "0000000000064673f804cd7023f8d6a4167c96a058fa2f6abeb8c52b23b8e48c", 0 ], [ "00000000000d74afb5c3e78596646e72c459064f491fcac9392f4662191fad85", 0 ], [ "00000000000284052da60a4948011dd9b8b47ee9600070c167a3fd38269089f9", 0 ], [ "00000000e8160e59aea361907f43ae001e0f57f81be07d8fae7eedaa0ac7e874", 0 ], [ "0000000000351cc9f437d474751a41873e425adfd90f1e04a685b62b22b735ef", 0 ], [ "000000000019ec9bd658f85ef2a8fb8b7117cbab8224b3f6da58ec84323283b7", 0 ], [ "000000000018fe65ad2c59fd8c42a9f8b8355e09339c68bed4dfb8dc7a96c464", 0 ], [ "000000000011ea14747c257f6a6b70fd8ffa1d1acb450ce487d75e0afdcc4a6c", 0 ], [ "0000000000112673666bbeb30ac2600f5c33f50dcd3f45a84a0b369c82b2265a", 0 ], [ "000000000006fc35a1b46ee63f58174344798c3945aeff136b18696fc8b37db9", 0 ], [ "0000000000041cabad64c90b9a6011cc275bc4274d4483c9d08c61354f61f87b", 0 ], [ "000000000000c49853970e4dcc57747e09c025b20a1b5399feea8e4da52e838f", 0 ], [ "0000000000005bdae23cd7d0fd132f647baeff5bcf27c128af9d4452918ab935", 0 ], [ "00000000000005f2a7320a92d69ca7a43fd50c0f33663a3620fd3b1997d7ee07", 0 ], [ "00000000000000fdb92b762b90c620b7fbcfd638d2cb5885970e0d0bc78b39dc", 0 ], [ "00000000000000f8d6ac6a65d665cd5dec111aa7ce9d35e8ec8e1f35838be3ac", 0 ], [ "000000000000000c4d57eea373335af6bd32da3ff748277e80fc8c8448a50363", 0 ], [ "000000004b67788b148a9ec401da418e9b3d498064158e9c05a15e88fff770ea", 0 ], [ "000000000133f2f73a74c399ffe67f7a3556db333a70eb3cba385e3c8a8a0c2d", 0 ], [ "000000000c4ed1736d5cce4a0ec7633800645cd1b9a5d360fb68df326f48df14", 0 ], [ "000000000950c87b961f40073b6b940765037a331dc0f7abbfd4c2a93db2def7", 0 ], [ "000000000000333379fa00b0b1ec72dfe6177075d25800caae3bbea5ce0ed6fb", 0 ], [ "0000000000002b26a1064652fe46da8356565adcc084ea5998dee2b19de66dfb", 0 ], [ "00000000000077c470e5a167d683ea0584a8f7a4b20c63ad5ce502f054cee10b", 0 ], [ "0000000000001583cd8eb780acf4ea25ff4d915d02ba99ca280b26ed5b4ca7bd", 0 ], [ "0000000000007e9819e4ef5116a0a1ecfd973e15de5786879a3538fe9070de0c", 0 ], [ "000000000000352b18af7591d822bc692a5a360cfee0d261d53f91e5d221bbe4", 0 ], [ "0000000000001e7f3b9aefdb3dd75677b1b182467941550bab8cbf7906a555cb", 0 ], [ "0000000000000fb3fd1f154a1669e779555793c5d1d1c18cab76cbfae70b27d2", 0 ], [ "00000000000003b68cfba858740b14b9493bad9e6eb3bea3a4fcbe08cca911f4", 0 ], [ "000000000000002a909cd56c02a14c89deb9acd9ce0e28a0bc34aecb4ad9921b", 0 ], [ "000000000000008316ea71503de71d0f539ee3cce6b1fb3106ec49a580cdf012", 0 ], [ "00000000c8cd6b15785e1789435a1fae16fc4b6528144c5d49c5b071dd16b0dd", 0 ], [ "0000000000001837a16eae4f4d934fba9bd58abee864ee10c1a6768f13e276ba", 0 ], [ "0000000000003559bab6ad82347a5bcd62b07572a7de146234a373b9d09b5bc8", 0 ], [ "00000000000020cbeb38ca68af925b736cd6a0694a8152b5ea9c6b06af972e9d", 0 ], [ "00000000000886977197c1d7ce749af9951e7cb08c448eb1c1034509048793b7", 0 ], [ "00000000002b307d0991eab1cc694be1eb3c607ebf1ff0fadfedbb038baabac6", 0 ], [ "00000000000c6570eaea754f2f6075ea71a68b46c90c08b271d08009eaefdfc6", 0 ], [ "0000000000004a6b77b80f36d0b4c843642c472b14060c659db079488394a548", 0 ], [ "00000000000026c176ab34dee51c7d6bcd101ace82b9d3a27cae9158fe4615e7", 0 ], [ "00000000000008740ccd0cd6f875599e851bb50daa188da402dc2e82cbe63432", 0 ], [ "000000000000027e5f3c4cc7fb4929504cf0a67192473b936ae4ffad6121eba8", 0 ], [ "0000000000000528bf19fababa130f28a8082d004750ff42ca1c7f2bde6be14e", 0 ], [ "000000000000014455b175ee9e2218fea02a4f64ca4219dda6cb8014c266fdc6", 0 ] ] ================================================ FILE: electrum/chains/testnet/fallback_lnnodes.json ================================================ { "038863cf8ab91046230f561cd5b386cbff8309fa02e3f0c3ed161a3aeb64a643b9": { "host": "203.132.95.10", "port": 9735 }, "03236a685d30096b26692dce0cf0fa7c8528bdf61dbf5363a3ef6d5c92733a3016": { "host": "50.116.3.223", "port": 9734 }, "03d5e17a3c213fe490e1b0c389f8cfcfcea08a29717d50a9f453735e0ab2a7c003": { "host": "3.16.119.191", "port": 9735 }, "03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134": { "host": "34.250.234.192", "port": 9735 }, "0260d9119979caedc570ada883ff614c6efb93f7f7382e25d73ecbeba0b62df2d7": { "host": "88.99.209.230", "port": 9735 }, "023ea0a53af875580899da0ab0a21455d9c19160c4ea1b7774c9d4be6810b02d2c": { "host": "160.16.233.215", "port": 9735 }, "0269a94e8b32c005e4336bfb743c08a6e9beb13d940d57c479d95c8e687ccbdb9f": { "host": "197.155.6.173", "port": 9735 }, "030f0bf260acdbd3edcad84d7588ec7c5df4711e87e6a23016f989b8d3a4147230": { "host": "163.172.94.64", "port": 9735 }, "02312627fdf07fbdd7e5ddb136611bdde9b00d26821d14d94891395452f67af248": { "host": "23.237.77.12", "port": 9735 }, "02ae2f22b02375e3e9b4b4a2db4f12e1b50752b4062dbefd6e01332acdaf680379": { "host": "197.155.6.172", "port": 9735 }, "034fe52e98a0e9d3c21b767e1b371881265d8c7578c21f5afd6d6438da10348b36": { "host": "23.239.23.44", "port": 9740 }, "02889be42fc32093d2dcbfa59369df262e3577b333d8a45e5859dcdd6a4139839a": { "host": "2a09:8280:1::42:a6f3", "port": 9735 }, "021713d5331898c206b57c4f7d40635079de9a97d97782646f31dac18a53f2d979": { "host": "2a09:8280:1::15:a57c", "port": 9735 } } ================================================ FILE: electrum/chains/testnet/servers.json ================================================ { "blackie.c3-soft.com": { "pruning": "-", "s": "57006", "t": "57005", "version": "1.4.5" }, "blockstream.info": { "pruning": "-", "s": "993", "t": "143", "version": "1.4" }, "electrum.blockstream.info": { "pruning": "-", "s": "60002", "t": "60001", "version": "1.4" }, "explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion": { "pruning": "-", "t": "143", "version": "1.4" }, "testnet.aranguren.org": { "pruning": "-", "s": "51002", "t": "51001", "version": "1.4.2" }, "testnet.hsmiths.com": { "pruning": "-", "s": "53012", "version": "1.4.2" }, "testnet.qtornado.com": { "pruning": "-", "s": "51002", "t": "51001", "version": "1.5" }, "tn.not.fyi": { "pruning": "-", "s": "55002", "t": "55001", "version": "1.4" } } ================================================ FILE: electrum/chains/testnet4/checkpoints.json ================================================ [ [ "00000000962a7fc2ef639196051fe181ed53ac6aa4cdfead14dca90f58aa36bc", 0 ], [ "000000002ad661157c553c0bbbb2490407adb1c8ac09f2b2a7174f87eeeb64bf", 0 ], [ "000000000be3ff43cde9eed4d6b2d4ad16c4f9509ccb94e1001af68e2f6647b3", 0 ], [ "00000000001ef2e4c2fc174354ed357cf313725fc336092733b2699d36342ff8", 0 ], [ "000000000025269f9fa4b0832ccbfef682d59c0fa8845b0c22cc24a1973f011a", 0 ], [ "000000000014b2d6b2ad804d5deb8d5b4a58caf152f6cea5600af0d9348dff29", 0 ], [ "000000000003c067c302d43c9499da6e382260252a2a29caf9748ee6972d5f01", 0 ], [ "000000000002180d23f15ba0b8161d9d38d03c61ab51d050c57928e1a7d98e0c", 0 ], [ "000000000000ed8722220a13b09d968a59686af5fc5c1e0a86371a498209fa72", 0 ], [ "0000000000003e82df3830ff7c05a58745a463a59d1097e160e47ac7aeb5323a", 0 ], [ "0000000000000c3f18b9a30269c4b53dd107bacf20482e4ec660e9970999a99f", 0 ], [ "00000000000002901853780dc8a63efd4d72359d8de7e14dc0398ccfc53d45cd", 0 ], [ "00000000000000a6ff1615113d25eeed8554813e4994f8ef7ce96458083d14cf", 0 ], [ "000000000000006e2d4fa8204c67c0986f9bb0214990b11043d0653d50755f54", 0 ], [ "00000000eaf8e0ea253d833614892aed70c55e5dc4b4d6709dd6420b8284debb", 0 ], [ "0000000000000063d3ca489d113ded6196c99f3785b61a8ded9254ebb96bc765", 0 ], [ "000000000000003f684cab6cdb7fe6e98cb13318bb45acdc2d2e2d7405b8bcbe", 0 ], [ "000000000000001f735b5a23732fb201cf6343b373c94a35f04e6b6075591889", 0 ], [ "000000001c247a1eb479ecc56ea7d7529f0c4afb6b7025f437a7d235454cd6a4", 0 ], [ "00000000acd1400a4801f361d675644993ad05e5b735a881f26746ece767521e", 0 ], [ "00000000542792e54a720567ba66157d48cdae7bfd01c1b678d0f07a2ed56e99", 0 ], [ "00000000ca301f565989627133247615bc937b52c68f8f4b342b6c2aeebff7ba", 0 ], [ "00000000e4ad2ec95dfddce6a554f626c9995e465b067e72528f6ae164fc58d2", 0 ], [ "00000000761cd6bff5e11258943e401e1bb094a8013e810e1d6031ce273a4b7c", 0 ], [ "00000000082032b915c151f1bb9892fe924013539924a34ec8b9bdea16eb7374", 0 ], [ "0000000000853161fa2a440407ce597524e85db6a27f5dffd35162b38e7627e7", 0 ], [ "00000000001c7ef1bace4a08448d9e462ff8b2e8c389f16026834c5d0c97252c", 0 ], [ "000000000ae11eebf9807d5ac0f9966282c59a1e613e229aa2bf7aea266d4535", 0 ], [ "00000000973e9926efc6a2a11413f7125242911ae10925e3616e872307a3e401", 0 ], [ "0000000000000003d66460a7e1ed89080427ac2004b2b3adae05e6e89725dc1f", 0 ], [ "000000009f8fe2dbafd82a0432d69d620d0ebeea033c8a2621bf06a6e6d3b5c4", 0 ], [ "0000000000000003ac6ce3118b61709d347142cc04daeb2c973b937904e4390b", 0 ], [ "0000000000003844aab8ac81fb69b47e83c037cee505bfd5cb6522aa4d2351bc", 0 ], [ "00000000000060397e5918c319db04ffac74f2a5c7724083112fca8958ea54de", 0 ], [ "00000000213b70c1bcec26b90a5503960a95d7a5fa5d9de498b9d794afeaebd5", 0 ], [ "0000000000000e5f474161d1b68932ab607035f5026f1ac4aa5816f24e76017a", 0 ], [ "00000000b56d4faf52043bfe98e126c69fb51671ba9bcea67c191620255d291d", 0 ], [ "0000000000052b94098008985919f6b525ead0d5cb5608fd60015f2647701ea1", 0 ], [ "00000000e5c06639c50bdb710b84ebf58d1df666db033157db685ec592932276", 0 ], [ "000000006f596242c0b5dd79b5f638e95a215252c44374133a5b263fcb5e9f89", 0 ], [ "00000000a2dc7b8c63c737543dd41e5a7a529d1824c32e2d454cae998c5a7298", 0 ], [ "000000008a995172d20119cc9b48a28ea4bf350711c2925849e5db760d0bde05", 0 ], [ "000000000bcec41b64702945a8cc7aceb06bc51fa93528b39619180ddc03e9bd", 0 ], [ "00000000003f767fd29141200c4b64fc0b224edcf28ef65f66c6345b76a60f32", 0 ], [ "00000000046883bd13b616c3525b243bbf3c7a9688a66f8ce46506ec25f6e798", 0 ], [ "00000000068670ee282a076d17900a07da93da50b3705405b1e7ca8616856535", 0 ], [ "0000000026b1f5d8ffa89385b048ca9004b97b412eee44000810ea8177a4bef4", 0 ], [ "0000000000000002e72644dda2132c93ac54edd6c155f47c51ca8cf4b918c0bf", 0 ], [ "00000000000000000a20f50c208d1ae3c3397fc6059b2cf1fe6b698e42023178", 0 ], [ "000000000a0b5c3e57dd4c3b751d13562a92953d794e19aff8f9e2ca6607604f", 0 ], [ "0000000003b9f6443b23bf86f200ef85005dfc508564960812eb85774327e6ce", 0 ], [ "000000000668f8e866d39a9ec07937e716a6035a02cfd0757ee716ac7cdca2fd", 0 ], [ "00000000041c239cab44d0afc2014a1351250375e796c692298ba8a8fcdb41de", 0 ], [ "0000000008c8148de5f2a96c0b82b32f8726ef7061386d370a68e078ade8e3e5", 0 ], [ "00000000089663c835fe83325ffaced64ad9c924468d1e1339f400bf8ed57883", 0 ], [ "00000000039229460de251e6b8b303841f76a1bc022025e8ca66a6ace20ffdf3", 0 ], [ "000000006b606bff2dfa618d2975fdfc2bea491f558ebe16be2ca7a0aa1c0181", 0 ], [ "0000000005dfc1e853b8d644598b77bdd97ef7578f9ccbb840aa3860fda9ddac", 0 ], [ "0000000002e9de570afba91146a43378199cc3b38fd52b2f4f1989734d22084a", 0 ], [ "000000000d1359300f79d95e0db59f4c14099684718838720db7569d98348844", 0 ] ] ================================================ FILE: electrum/chains/testnet4/servers.json ================================================ { "testnet4-electrumx.wakiyamap.dev": { "pruning": "-", "s": "51002", "t": "51001", "version": "1.4" }, "blackie.c3-soft.com": { "pruning": "-", "s": "57010", "t": "57009", "version": "1.4" }, "mempool.space": { "pruning": "-", "s": "40002", "version": "1.4" } } ================================================ FILE: electrum/channel_db.py ================================================ # -*- coding: utf-8 -*- # # Electrum - lightweight Bitcoin client # Copyright (C) 2018 The Electrum developers # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files # (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, # publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import ipaddress import time import random import os from collections import defaultdict from typing import Sequence, List, Tuple, Optional, Dict, NamedTuple, TYPE_CHECKING, Set import binascii import base64 import asyncio import threading from enum import IntEnum import functools from aiorpcx import NetAddress from electrum_ecc import ECPubkey from .sql_db import SqlDB, sql from . import constants, util from .util import profiler, get_headers_dir, is_ip_address, json_normalize, UserFacingException, is_private_netaddress from .lntransport import LNPeerAddr from .lnutil import (ShortChannelID, validate_features, IncompatibleOrInsaneFeatures, InvalidGossipMsg, GossipForwardingMessage, GossipTimestampFilter) from .lnverifier import LNChannelVerifier, verify_sig_for_channel_update from .lnmsg import decode_msg from .crypto import sha256d from .lnmsg import FailedToParseMsg if TYPE_CHECKING: from .network import Network from .lnchannel import Channel from .lnrouter import RouteEdge from .simple_config import SimpleConfig FLAG_DISABLE = 1 << 1 FLAG_DIRECTION = 1 << 0 class ChannelDBNotLoaded(UserFacingException): pass class ChannelInfo(NamedTuple): short_channel_id: ShortChannelID node1_id: bytes node2_id: bytes capacity_sat: Optional[int] raw: Optional[bytes] = None @staticmethod def from_msg(payload: dict) -> 'ChannelInfo': features = int.from_bytes(payload['features'], 'big') features = validate_features(features) channel_id = payload['short_channel_id'] node_id_1 = payload['node_id_1'] node_id_2 = payload['node_id_2'] assert list(sorted([node_id_1, node_id_2])) == [node_id_1, node_id_2] capacity_sat = None return ChannelInfo( short_channel_id = ShortChannelID.normalize(channel_id), node1_id = node_id_1, node2_id = node_id_2, capacity_sat = capacity_sat, raw = payload.get('raw') ) @staticmethod def from_raw_msg(raw: bytes) -> 'ChannelInfo': payload_dict = decode_msg(raw)[1] payload_dict['raw'] = raw return ChannelInfo.from_msg(payload_dict) @staticmethod def from_route_edge(route_edge: 'RouteEdge') -> 'ChannelInfo': node1_id, node2_id = sorted([route_edge.start_node, route_edge.end_node]) return ChannelInfo( short_channel_id=route_edge.short_channel_id, node1_id=node1_id, node2_id=node2_id, capacity_sat=None, ) class Policy(NamedTuple): key: bytes cltv_delta: int htlc_minimum_msat: int htlc_maximum_msat: Optional[int] fee_base_msat: int fee_proportional_millionths: int channel_flags: int message_flags: int timestamp: int raw: Optional[bytes] = None @staticmethod def from_msg(payload: dict) -> 'Policy': return Policy( key = payload['short_channel_id'] + payload['start_node'], cltv_delta = payload['cltv_expiry_delta'], htlc_minimum_msat = payload['htlc_minimum_msat'], htlc_maximum_msat = payload.get('htlc_maximum_msat', None), fee_base_msat = payload['fee_base_msat'], fee_proportional_millionths = payload['fee_proportional_millionths'], message_flags = int.from_bytes(payload['message_flags'], "big"), channel_flags = int.from_bytes(payload['channel_flags'], "big"), timestamp = payload['timestamp'], raw = payload.get('raw'), ) @staticmethod def from_raw_msg(key: bytes, raw: bytes) -> 'Policy': payload = decode_msg(raw)[1] payload['start_node'] = key[8:] payload['raw'] = raw return Policy.from_msg(payload) @staticmethod def from_route_edge(route_edge: 'RouteEdge') -> 'Policy': return Policy( key=route_edge.short_channel_id + route_edge.start_node, cltv_delta=route_edge.cltv_delta, htlc_minimum_msat=0, htlc_maximum_msat=None, fee_base_msat=route_edge.fee_base_msat, fee_proportional_millionths=route_edge.fee_proportional_millionths, channel_flags=0, message_flags=0, timestamp=0, ) def is_disabled(self): return self.channel_flags & FLAG_DISABLE @property def short_channel_id(self) -> ShortChannelID: return ShortChannelID.normalize(self.key[0:8]) @property def start_node(self) -> bytes: return self.key[8:] class NodeInfo(NamedTuple): node_id: bytes features: int timestamp: int alias: str raw: Optional[bytes] @staticmethod def from_msg(payload) -> Tuple['NodeInfo', Sequence['LNPeerAddr']]: node_id = payload['node_id'] features = int.from_bytes(payload['features'], "big") features = validate_features(features) addresses = NodeInfo.parse_addresses_field(payload['addresses']) peer_addrs = [] for host, port in addresses: try: peer_addrs.append(LNPeerAddr(host=host, port=port, pubkey=node_id)) except ValueError: pass alias = payload['alias'].rstrip(b'\x00') try: alias = alias.decode('utf8') except Exception: alias = '' timestamp = payload['timestamp'] node_info = NodeInfo( node_id=node_id, features=features, timestamp=timestamp, alias=alias, raw=payload.get('raw')) return node_info, peer_addrs @staticmethod def from_raw_msg(raw: bytes) -> Tuple['NodeInfo', Sequence['LNPeerAddr']]: payload_dict = decode_msg(raw)[1] payload_dict['raw'] = raw return NodeInfo.from_msg(payload_dict) @staticmethod def to_addresses_field(hostname: str, port: int) -> bytes: """Encodes a hostname/port pair into a BOLT-7 'addresses' field.""" if (NodeInfo.invalid_announcement_hostname(hostname) or port is None or port <= 0 or port > 65535): return b'' port_bytes = port.to_bytes(2, 'big') if is_ip_address(hostname): # ipv4 or ipv6 ip_addr = ipaddress.ip_address(hostname) if ip_addr.version == 4: return b'\x01' + ip_addr.packed + port_bytes elif ip_addr.version == 6: return b'\x02' + ip_addr.packed + port_bytes return b'' elif hostname.endswith('.onion'): # Tor onion v3 onion_addr: bytes = base64.b32decode(hostname[:-6], casefold=True) return b'\x04' + onion_addr + port_bytes else: try: hostname_ascii: bytes = hostname.encode('ascii') except UnicodeEncodeError: # encoding single characters to punycode (according to spec) doesn't make sense # as you can't differentiate them from regular ascii? encoding the whole string to punycode # doesn't work either as the receiver would interpret it as regular ascii. # hostname_ascii: bytes = hostname.encode('punycode') return b'' if len(hostname_ascii) + 3 > 258: # + 1 byte for length and 2 for port return b'' # too long return b'\x05' + len(hostname_ascii).to_bytes(1, "big") + hostname_ascii + port_bytes @staticmethod def invalid_announcement_hostname(hostname: Optional[str]) -> bool: """Returns True if hostname unsuited for publishing in a NodeAnnouncement.""" if (hostname is None or hostname == "" or is_private_netaddress(hostname) or hostname.startswith("http://") # not catching 'http' due to onion addresses or hostname.startswith("https://")): return True if hostname.endswith('.onion'): if len(hostname) != 62: # not an onion v3 link (probably onion v2) return True return False @staticmethod def parse_addresses_field(addresses_field): buf = addresses_field def read(n): nonlocal buf data, buf = buf[0:n], buf[n:] return data addresses = [] while buf: atype = ord(read(1)) if atype == 0: pass elif atype == 1: # IPv4 ipv4_addr = '.'.join(map(lambda x: '%d' % x, read(4))) port = int.from_bytes(read(2), 'big') if is_ip_address(ipv4_addr) and port != 0: addresses.append((ipv4_addr, port)) elif atype == 2: # IPv6 ipv6_addr = b':'.join([binascii.hexlify(read(2)) for i in range(8)]) ipv6_addr = ipv6_addr.decode('ascii') port = int.from_bytes(read(2), 'big') if is_ip_address(ipv6_addr) and port != 0: addresses.append((ipv6_addr, port)) elif atype == 3: # onion v2 read(12) # we skip onion v2 as it is deprecated elif atype == 4: # onion v3 host = base64.b32encode(read(35)) + b'.onion' host = host.decode('ascii').lower() port = int.from_bytes(read(2), 'big') addresses.append((host, port)) elif atype == 5: # dns hostname len_hostname = int.from_bytes(read(1), 'big') host = read(len_hostname).decode('ascii') port = int.from_bytes(read(2), 'big') if not NodeInfo.invalid_announcement_hostname(host) and port > 0: addresses.append((host, port)) else: # unknown address type # we don't know how long it is -> have to escape # if there are other addresses we could have parsed later, they are lost. break return addresses class UpdateStatus(IntEnum): ORPHANED = 0 EXPIRED = 1 DEPRECATED = 2 UNCHANGED = 3 GOOD = 4 class CategorizedChannelUpdates(NamedTuple): orphaned: List # no channel announcement for channel update expired: List # update older than two weeks deprecated: List # update older than database entry unchanged: List # unchanged policies good: List # good updates def get_mychannel_info(short_channel_id: ShortChannelID, my_channels: Dict[ShortChannelID, 'Channel']) -> Optional[ChannelInfo]: chan = my_channels.get(short_channel_id) if not chan: return raw_msg, _ = chan.construct_channel_announcement_without_sigs() ci = ChannelInfo.from_raw_msg(raw_msg) return ci._replace(capacity_sat=chan.constraints.capacity) def get_mychannel_policy(short_channel_id: bytes, node_id: bytes, my_channels: Dict[ShortChannelID, 'Channel']) -> Optional[Policy]: chan = my_channels.get(short_channel_id) # type: Optional[Channel] if not chan: return if node_id == chan.node_id: # incoming direction (to us) remote_update_raw = chan.get_remote_update() if not remote_update_raw: return now = int(time.time()) remote_update_decoded = decode_msg(remote_update_raw)[1] remote_update_decoded['timestamp'] = now remote_update_decoded['start_node'] = node_id return Policy.from_msg(remote_update_decoded) elif node_id == chan.get_local_pubkey(): # outgoing direction (from us) local_update_decoded = decode_msg(chan.get_outgoing_gossip_channel_update())[1] local_update_decoded['start_node'] = node_id return Policy.from_msg(local_update_decoded) class _LoadDataAborted(Exception): pass create_channel_info = """ CREATE TABLE IF NOT EXISTS channel_info ( short_channel_id BLOB(8), msg BLOB, PRIMARY KEY(short_channel_id) )""" create_policy = """ CREATE TABLE IF NOT EXISTS policy ( key BLOB(41), msg BLOB, PRIMARY KEY(key) )""" create_address = """ CREATE TABLE IF NOT EXISTS address ( node_id BLOB(33), host STRING(256), port INTEGER NOT NULL, timestamp INTEGER, PRIMARY KEY(node_id, host, port) )""" create_node_info = """ CREATE TABLE IF NOT EXISTS node_info ( node_id BLOB(33), msg BLOB, PRIMARY KEY(node_id) )""" class ChannelDB(SqlDB): NUM_MAX_RECENT_PEERS = 20 PRIVATE_CHAN_UPD_CACHE_TTL_NORMAL = 600 PRIVATE_CHAN_UPD_CACHE_TTL_SHORT = 120 def __init__(self, network: 'Network'): path = self.get_file_path(network.config) super().__init__(network.asyncio_loop, path, commit_interval=100) self.lock = threading.RLock() self.num_nodes = 0 self.num_channels = 0 self.num_policies = 0 self._channel_updates_for_private_channels = {} # type: Dict[Tuple[bytes, bytes], Tuple[dict, int]] # note: ^ we could maybe move this cache into PaySession instead of being global. # That would only make sense though if PaySessions were never too short # (e.g. consider trampoline forwarding). self.ca_verifier = LNChannelVerifier(network, self) # initialized in load_data # note: modify/iterate needs self.lock self._channels = {} # type: Dict[ShortChannelID, ChannelInfo] self._policies = {} # type: Dict[Tuple[bytes, ShortChannelID], Policy] # (node_id, scid) -> Policy self._nodes = {} # type: Dict[bytes, NodeInfo] # node_id -> NodeInfo # node_id -> NetAddress -> timestamp self._addresses = defaultdict(dict) # type: Dict[bytes, Dict[NetAddress, int]] self._channels_for_node = defaultdict(set) # type: Dict[bytes, Set[ShortChannelID]] self._recent_peers = [] # type: List[bytes] # list of node_ids self._chans_with_0_policies = set() # type: Set[ShortChannelID] self._chans_with_1_policies = set() # type: Set[ShortChannelID] self._chans_with_2_policies = set() # type: Set[ShortChannelID] self.forwarding_lock = threading.RLock() self.fwd_channels = [] # type: List[GossipForwardingMessage] self.fwd_orphan_channels = [] # type: List[GossipForwardingMessage] self.fwd_channel_updates = [] # type: List[GossipForwardingMessage] self.fwd_node_announcements = [] # type: List[GossipForwardingMessage] self.data_loaded = asyncio.Event() self.network = network # only for callback @classmethod def get_file_path(cls, config: 'SimpleConfig') -> str: return os.path.join(get_headers_dir(config), 'gossip_db') def update_counts(self): self.num_nodes = len(self._nodes) self.num_channels = len(self._channels) self.num_policies = len(self._policies) util.trigger_callback('channel_db', self.num_nodes, self.num_channels, self.num_policies) util.trigger_callback('ln_gossip_sync_progress') def get_channel_ids(self): with self.lock: return set(self._channels.keys()) def add_recent_peer(self, peer: LNPeerAddr): now = int(time.time()) node_id = peer.pubkey with self.lock: self._addresses[node_id][peer.net_addr()] = now # list is ordered if node_id in self._recent_peers: self._recent_peers.remove(node_id) self._recent_peers.insert(0, node_id) self._recent_peers = self._recent_peers[:self.NUM_MAX_RECENT_PEERS] self._db_save_node_address(peer, now) def get_200_randomly_sorted_nodes_not_in(self, node_ids): with self.lock: unshuffled = set(self._nodes.keys()) - node_ids return random.sample(list(unshuffled), min(200, len(unshuffled))) def get_last_good_address(self, node_id: bytes) -> Optional[LNPeerAddr]: """Returns latest address we successfully connected to, for given node.""" addr_to_ts = self._addresses.get(node_id) if not addr_to_ts: return None addr = sorted(list(addr_to_ts), key=lambda a: addr_to_ts[a], reverse=True)[0] try: return LNPeerAddr(str(addr.host), addr.port, node_id) except ValueError: return None def get_recent_peers(self): if not self.data_loaded.is_set(): raise ChannelDBNotLoaded("channelDB data not loaded yet!") with self.lock: ret = [self.get_last_good_address(node_id) for node_id in self._recent_peers] return ret # note: currently channel announcements are trusted by default (trusted=True); # they are not SPV-verified. Verifying them would make the gossip sync # even slower; especially as servers will start throttling us. # It would probably put significant strain on servers if all clients # verified the complete gossip. def add_channel_announcements(self, msg_payloads, *, trusted=True): # note: signatures have already been verified. if type(msg_payloads) is dict: msg_payloads = [msg_payloads] added = 0 for msg in msg_payloads: short_channel_id = ShortChannelID(msg['short_channel_id']) if short_channel_id in self._channels: continue if constants.net.rev_genesis_bytes() != msg['chain_hash']: self.logger.info("ChanAnn has unexpected chain_hash {}".format(msg['chain_hash'].hex())) continue try: channel_info = ChannelInfo.from_msg(msg) except IncompatibleOrInsaneFeatures as e: self.logger.info(f"unknown or insane feature bits: {e!r}") continue if trusted: added += 1 self.add_verified_channel_info(msg) else: added += self.ca_verifier.add_new_channel_info(short_channel_id, msg) self.update_counts() def add_verified_channel_info(self, msg: dict, *, capacity_sat: int = None) -> None: try: channel_info = ChannelInfo.from_msg(msg) except IncompatibleOrInsaneFeatures: return channel_info = channel_info._replace(capacity_sat=capacity_sat) with self.lock: self._channels[channel_info.short_channel_id] = channel_info self._channels_for_node[channel_info.node1_id].add(channel_info.short_channel_id) self._channels_for_node[channel_info.node2_id].add(channel_info.short_channel_id) self._update_num_policies_for_chan(channel_info.short_channel_id) if 'raw' in msg: self._db_save_channel(channel_info.short_channel_id, msg['raw']) with self.forwarding_lock: if fwd_msg := GossipForwardingMessage.from_payload(msg): self.fwd_channels.append(fwd_msg) def policy_changed(self, old_policy: Policy, new_policy: Policy, verbose: bool) -> bool: changed = False if old_policy.cltv_delta != new_policy.cltv_delta: changed |= True if verbose: self.logger.info(f'cltv_expiry_delta: {old_policy.cltv_delta} -> {new_policy.cltv_delta}') if old_policy.htlc_minimum_msat != new_policy.htlc_minimum_msat: changed |= True if verbose: self.logger.info(f'htlc_minimum_msat: {old_policy.htlc_minimum_msat} -> {new_policy.htlc_minimum_msat}') if old_policy.htlc_maximum_msat != new_policy.htlc_maximum_msat: changed |= True if verbose: self.logger.info(f'htlc_maximum_msat: {old_policy.htlc_maximum_msat} -> {new_policy.htlc_maximum_msat}') if old_policy.fee_base_msat != new_policy.fee_base_msat: changed |= True if verbose: self.logger.info(f'fee_base_msat: {old_policy.fee_base_msat} -> {new_policy.fee_base_msat}') if old_policy.fee_proportional_millionths != new_policy.fee_proportional_millionths: changed |= True if verbose: self.logger.info(f'fee_proportional_millionths: {old_policy.fee_proportional_millionths} -> {new_policy.fee_proportional_millionths}') if old_policy.channel_flags != new_policy.channel_flags: changed |= True if verbose: self.logger.info(f'channel_flags: {old_policy.channel_flags} -> {new_policy.channel_flags}') if old_policy.message_flags != new_policy.message_flags: changed |= True if verbose: self.logger.info(f'message_flags: {old_policy.message_flags} -> {new_policy.message_flags}') if not changed and verbose: self.logger.info(f'policy unchanged: {old_policy.timestamp} -> {new_policy.timestamp}') return changed def add_channel_update( self, payload, *, max_age=None, verify=True, verbose=True) -> UpdateStatus: now = int(time.time()) short_channel_id = ShortChannelID(payload['short_channel_id']) timestamp = payload['timestamp'] if max_age and now - timestamp > max_age: return UpdateStatus.EXPIRED if timestamp - now > 60: return UpdateStatus.DEPRECATED channel_info = self._channels.get(short_channel_id) if not channel_info: return UpdateStatus.ORPHANED flags = int.from_bytes(payload['channel_flags'], 'big') direction = flags & FLAG_DIRECTION start_node = channel_info.node1_id if direction == 0 else channel_info.node2_id payload['start_node'] = start_node # compare updates to existing database entries short_channel_id = ShortChannelID(payload['short_channel_id']) key = (start_node, short_channel_id) old_policy = self._policies.get(key) if old_policy and timestamp <= old_policy.timestamp + 60: return UpdateStatus.DEPRECATED if verify: self.verify_channel_update(payload) policy = Policy.from_msg(payload) with self.lock: self._policies[key] = policy self._update_num_policies_for_chan(short_channel_id) if 'raw' in payload: self._db_save_policy(policy.key, payload['raw']) if old_policy and not self.policy_changed(old_policy, policy, verbose): return UpdateStatus.UNCHANGED else: if policy.message_flags & 0b10 == 0: # check if its `dont_forward` with self.forwarding_lock: if fwd_msg := GossipForwardingMessage.from_payload(payload): self.fwd_channel_updates.append(fwd_msg) return UpdateStatus.GOOD def add_channel_updates(self, payloads, max_age=None) -> CategorizedChannelUpdates: orphaned = [] expired = [] deprecated = [] unchanged = [] good = [] for payload in payloads: r = self.add_channel_update(payload, max_age=max_age, verbose=False, verify=True) if r == UpdateStatus.ORPHANED: orphaned.append(payload) elif r == UpdateStatus.EXPIRED: expired.append(payload) elif r == UpdateStatus.DEPRECATED: deprecated.append(payload) elif r == UpdateStatus.UNCHANGED: unchanged.append(payload) elif r == UpdateStatus.GOOD: good.append(payload) self.update_counts() return CategorizedChannelUpdates( orphaned=orphaned, expired=expired, deprecated=deprecated, unchanged=unchanged, good=good) def create_database(self): c = self.conn.cursor() c.execute(create_node_info) c.execute(create_address) c.execute(create_policy) c.execute(create_channel_info) self.conn.commit() @sql def _db_save_policy(self, key: bytes, msg: bytes): # 'msg' is a 'channel_update' message c = self.conn.cursor() c.execute("""REPLACE INTO policy (key, msg) VALUES (?,?)""", [key, msg]) @sql def _db_delete_policy(self, node_id: bytes, short_channel_id: ShortChannelID): key = short_channel_id + node_id c = self.conn.cursor() c.execute("""DELETE FROM policy WHERE key=?""", (key,)) @sql def _db_save_channel(self, short_channel_id: ShortChannelID, msg: bytes): # 'msg' is a 'channel_announcement' message c = self.conn.cursor() c.execute("REPLACE INTO channel_info (short_channel_id, msg) VALUES (?,?)", [short_channel_id, msg]) @sql def _db_delete_channel(self, short_channel_id: ShortChannelID): c = self.conn.cursor() c.execute("""DELETE FROM channel_info WHERE short_channel_id=?""", (short_channel_id,)) @sql def _db_save_node_info(self, node_id: bytes, msg: bytes): # 'msg' is a 'node_announcement' message c = self.conn.cursor() c.execute("REPLACE INTO node_info (node_id, msg) VALUES (?,?)", [node_id, msg]) @sql def _db_save_node_address(self, peer: LNPeerAddr, timestamp: int): c = self.conn.cursor() c.execute("REPLACE INTO address (node_id, host, port, timestamp) VALUES (?,?,?,?)", (peer.pubkey, peer.host, peer.port, timestamp)) @sql def _db_save_node_addresses(self, node_addresses: Sequence[LNPeerAddr]): c = self.conn.cursor() for addr in node_addresses: c.execute("SELECT * FROM address WHERE node_id=? AND host=? AND port=?", (addr.pubkey, addr.host, addr.port)) r = c.fetchall() if r == []: c.execute("INSERT INTO address (node_id, host, port, timestamp) VALUES (?,?,?,?)", (addr.pubkey, addr.host, addr.port, 0)) @classmethod def verify_channel_update(cls, payload, *, start_node: bytes = None) -> None: short_channel_id = payload['short_channel_id'] short_channel_id = ShortChannelID(short_channel_id) if constants.net.rev_genesis_bytes() != payload['chain_hash']: raise InvalidGossipMsg('wrong chain hash') start_node = payload.get('start_node', None) or start_node assert start_node is not None if not verify_sig_for_channel_update(payload, start_node): raise InvalidGossipMsg(f'failed verifying channel update for {short_channel_id}') @classmethod def verify_channel_announcement(cls, payload) -> None: h = sha256d(payload['raw'][2+256:]) pubkeys = [payload['node_id_1'], payload['node_id_2'], payload['bitcoin_key_1'], payload['bitcoin_key_2']] sigs = [payload['node_signature_1'], payload['node_signature_2'], payload['bitcoin_signature_1'], payload['bitcoin_signature_2']] for pubkey, sig in zip(pubkeys, sigs): if not ECPubkey(pubkey).ecdsa_verify(sig, h): raise InvalidGossipMsg('signature failed') @classmethod def verify_node_announcement(cls, payload) -> None: pubkey = payload['node_id'] signature = payload['signature'] h = sha256d(payload['raw'][66:]) if not ECPubkey(pubkey).ecdsa_verify(signature, h): raise InvalidGossipMsg('signature failed') def add_node_announcements(self, msg_payloads): # note: signatures have already been verified. if type(msg_payloads) is dict: msg_payloads = [msg_payloads] new_nodes = set() # type: Set[bytes] for msg_payload in msg_payloads: try: node_info, node_addresses = NodeInfo.from_msg(msg_payload) except IncompatibleOrInsaneFeatures: continue node_id = node_info.node_id # Ignore node if it has no associated channel (DoS protection) if node_id not in self._channels_for_node: #self.logger.info('ignoring orphan node_announcement') continue node = self._nodes.get(node_id) if node and node.timestamp >= node_info.timestamp: continue new_nodes.add(node_id) # save with self.lock: self._nodes[node_id] = node_info if 'raw' in msg_payload: self._db_save_node_info(node_id, msg_payload['raw']) with self.lock: for addr in node_addresses: net_addr = NetAddress(addr.host, addr.port) self._addresses[node_id][net_addr] = self._addresses[node_id].get(net_addr) or 0 self._db_save_node_addresses(node_addresses) with self.forwarding_lock: if fwd_msg := GossipForwardingMessage.from_payload(msg_payload): self.fwd_node_announcements.append(fwd_msg) self.update_counts() def get_old_policies(self, delta) -> Sequence[Tuple[bytes, ShortChannelID]]: with self.lock: _policies = self._policies.copy() now = int(time.time()) return list(k for k, v in _policies.items() if v.timestamp <= now - delta) def prune_old_policies(self, delta): old_policies = self.get_old_policies(delta) if old_policies: for key in old_policies: node_id, scid = key with self.lock: self._policies.pop(key) self._db_delete_policy(*key) self._update_num_policies_for_chan(scid) self.update_counts() self.logger.info(f'Deleting {len(old_policies)} old policies') def prune_orphaned_channels(self): with self.lock: orphaned_chans = self._chans_with_0_policies.copy() if orphaned_chans: for short_channel_id in orphaned_chans: self.remove_channel(short_channel_id) self.update_counts() self.logger.info(f'Deleting {len(orphaned_chans)} orphaned channels') def _get_channel_update_for_private_channel( self, start_node_id: bytes, short_channel_id: ShortChannelID, *, now: int = None, # unix ts ) -> Optional[dict]: if now is None: now = int(time.time()) key = (start_node_id, short_channel_id) chan_upd_dict, cache_expiration = self._channel_updates_for_private_channels.get(key, (None, 0)) if cache_expiration < now: chan_upd_dict = None # already expired # TODO rm expired entries from cache (note: perf vs thread-safety) return chan_upd_dict def add_channel_update_for_private_channel( self, msg_payload: dict, start_node_id: bytes, *, cache_ttl: int = None, # seconds ) -> bool: """Returns True iff the channel update was successfully added and it was different than what we had before (if any). """ if not verify_sig_for_channel_update(msg_payload, start_node_id): return False # ignore now = int(time.time()) short_channel_id = ShortChannelID(msg_payload['short_channel_id']) msg_payload['start_node'] = start_node_id prev_chanupd = self._get_channel_update_for_private_channel(start_node_id, short_channel_id, now=now) if prev_chanupd == msg_payload: return False if cache_ttl is None: cache_ttl = self.PRIVATE_CHAN_UPD_CACHE_TTL_NORMAL cache_expiration = now + cache_ttl key = (start_node_id, short_channel_id) with self.lock: self._channel_updates_for_private_channels[key] = msg_payload, cache_expiration return True def remove_channel(self, short_channel_id: ShortChannelID): # FIXME what about rm-ing policies? with self.lock: channel_info = self._channels.pop(short_channel_id, None) if channel_info: self._channels_for_node[channel_info.node1_id].remove(channel_info.short_channel_id) self._channels_for_node[channel_info.node2_id].remove(channel_info.short_channel_id) self._update_num_policies_for_chan(short_channel_id) # delete from database self._db_delete_channel(short_channel_id) def get_node_addresses(self, node_id: bytes) -> Sequence[Tuple[str, int, int]]: """Returns list of (host, port, timestamp).""" addr_to_ts = self._addresses.get(node_id) if not addr_to_ts: return [] return [(str(net_addr.host), net_addr.port, ts) for net_addr, ts in addr_to_ts.items()] def handle_abort(func): @functools.wraps(func) def wrapper(self: 'ChannelDB', *args, **kwargs): try: return func(self, *args, **kwargs) except _LoadDataAborted: return return wrapper @sql @profiler @handle_abort def load_data(self): if self.data_loaded.is_set(): return # Note: this method takes several seconds... mostly due to lnmsg.decode_msg being slow. def maybe_abort(): if self.stopping: self.logger.info("load_data() was asked to stop. exiting early.") raise _LoadDataAborted() c = self.conn.cursor() c.execute("""SELECT * FROM address""") for x in c: maybe_abort() node_id, host, port, timestamp = x try: net_addr = NetAddress(host, port) except Exception: continue self._addresses[node_id][net_addr] = int(timestamp or 0) def newest_ts_for_node_id(node_id): newest_ts = 0 for addr, ts in self._addresses[node_id].items(): newest_ts = max(newest_ts, ts) return newest_ts sorted_node_ids = sorted(self._addresses.keys(), key=newest_ts_for_node_id, reverse=True) self._recent_peers = sorted_node_ids[:self.NUM_MAX_RECENT_PEERS] c.execute("""SELECT * FROM channel_info""") for short_channel_id, msg in c: maybe_abort() try: ci = ChannelInfo.from_raw_msg(msg) except IncompatibleOrInsaneFeatures: continue except FailedToParseMsg: continue self._channels[ShortChannelID.normalize(short_channel_id)] = ci c.execute("""SELECT * FROM node_info""") for node_id, msg in c: maybe_abort() try: node_info, node_addresses = NodeInfo.from_raw_msg(msg) except IncompatibleOrInsaneFeatures: continue except FailedToParseMsg: continue # don't load node_addresses because they dont have timestamps self._nodes[node_id] = node_info c.execute("""SELECT * FROM policy""") for key, msg in c: maybe_abort() try: p = Policy.from_raw_msg(key, msg) except FailedToParseMsg: continue self._policies[(p.start_node, p.short_channel_id)] = p for channel_info in self._channels.values(): self._channels_for_node[channel_info.node1_id].add(channel_info.short_channel_id) self._channels_for_node[channel_info.node2_id].add(channel_info.short_channel_id) self._update_num_policies_for_chan(channel_info.short_channel_id) self.logger.info(f'data loaded. {len(self._channels)} chans. {len(self._policies)} policies. ' f'{len(self._channels_for_node)} nodes.') self.update_counts() (nchans_with_0p, nchans_with_1p, nchans_with_2p) = self.get_num_channels_partitioned_by_policy_count() self.logger.info(f'num_channels_partitioned_by_policy_count. ' f'0p: {nchans_with_0p}, 1p: {nchans_with_1p}, 2p: {nchans_with_2p}') self.asyncio_loop.call_soon_threadsafe(self.data_loaded.set) util.trigger_callback('gossip_db_loaded') def _update_num_policies_for_chan(self, short_channel_id: ShortChannelID) -> None: channel_info = self.get_channel_info(short_channel_id) if channel_info is None: with self.lock: self._chans_with_0_policies.discard(short_channel_id) self._chans_with_1_policies.discard(short_channel_id) self._chans_with_2_policies.discard(short_channel_id) return p1 = self.get_policy_for_node(short_channel_id, channel_info.node1_id) p2 = self.get_policy_for_node(short_channel_id, channel_info.node2_id) with self.lock: self._chans_with_0_policies.discard(short_channel_id) self._chans_with_1_policies.discard(short_channel_id) self._chans_with_2_policies.discard(short_channel_id) if p1 is not None and p2 is not None: self._chans_with_2_policies.add(short_channel_id) elif p1 is None and p2 is None: self._chans_with_0_policies.add(short_channel_id) else: self._chans_with_1_policies.add(short_channel_id) def get_num_channels_partitioned_by_policy_count(self) -> Tuple[int, int, int]: nchans_with_0p = len(self._chans_with_0_policies) nchans_with_1p = len(self._chans_with_1_policies) nchans_with_2p = len(self._chans_with_2_policies) return nchans_with_0p, nchans_with_1p, nchans_with_2p def get_policy_for_node( self, short_channel_id: ShortChannelID, node_id: bytes, *, my_channels: Dict[ShortChannelID, 'Channel'] = None, private_route_edges: Dict[ShortChannelID, 'RouteEdge'] = None, now: int = None, # unix ts ) -> Optional['Policy']: channel_info = self.get_channel_info(short_channel_id) if channel_info is not None: # publicly announced channel policy = self._policies.get((node_id, short_channel_id)) if policy: return policy elif chan_upd_dict := self._get_channel_update_for_private_channel(node_id, short_channel_id, now=now): return Policy.from_msg(chan_upd_dict) # check if it's one of our own channels if my_channels: policy = get_mychannel_policy(short_channel_id, node_id, my_channels) if policy: return policy if private_route_edges: route_edge = private_route_edges.get(short_channel_id, None) if route_edge: return Policy.from_route_edge(route_edge) def get_channel_info( self, short_channel_id: ShortChannelID, *, my_channels: Dict[ShortChannelID, 'Channel'] = None, private_route_edges: Dict[ShortChannelID, 'RouteEdge'] = None, ) -> Optional[ChannelInfo]: ret = self._channels.get(short_channel_id) if ret: return ret # check if it's one of our own channels if my_channels: channel_info = get_mychannel_info(short_channel_id, my_channels) if channel_info: return channel_info if private_route_edges: route_edge = private_route_edges.get(short_channel_id) if route_edge: return ChannelInfo.from_route_edge(route_edge) def get_channels_for_node( self, node_id: bytes, *, my_channels: Dict[ShortChannelID, 'Channel'] = None, private_route_edges: Dict[ShortChannelID, 'RouteEdge'] = None, ) -> Set[ShortChannelID]: """Returns the set of short channel IDs where node_id is one of the channel participants.""" if not self.data_loaded.is_set(): raise ChannelDBNotLoaded("channelDB data not loaded yet!") relevant_channels = self._channels_for_node.get(node_id) or set() relevant_channels = set(relevant_channels) # copy # add our own channels # TODO maybe slow? if my_channels: for chan in my_channels.values(): if node_id in (chan.node_id, chan.get_local_pubkey()): relevant_channels.add(chan.short_channel_id) # add private channels # TODO maybe slow? if private_route_edges: for route_edge in private_route_edges.values(): if node_id in (route_edge.start_node, route_edge.end_node): relevant_channels.add(route_edge.short_channel_id) return relevant_channels def get_endnodes_for_chan(self, short_channel_id: ShortChannelID, *, my_channels: Dict[ShortChannelID, 'Channel'] = None) -> Optional[Tuple[bytes, bytes]]: channel_info = self.get_channel_info(short_channel_id) if channel_info is not None: # publicly announced channel return channel_info.node1_id, channel_info.node2_id # check if it's one of our own channels if not my_channels: return chan = my_channels.get(short_channel_id) # type: Optional[Channel] if not chan: return return chan.get_local_pubkey(), chan.node_id def get_node_info_for_node_id(self, node_id: bytes) -> Optional['NodeInfo']: return self._nodes.get(node_id) def get_node_infos(self) -> Dict[bytes, NodeInfo]: with self.lock: return self._nodes.copy() def get_node_policies(self) -> Dict[Tuple[bytes, ShortChannelID], Policy]: with self.lock: return self._policies.copy() def get_node_by_prefix(self, prefix): with self.lock: for k in self._addresses.keys(): if k.startswith(prefix): return k raise Exception('node not found') def clear_forwarding_gossip(self) -> None: with self.forwarding_lock: self.fwd_channels.clear() self.fwd_channel_updates.clear() self.fwd_node_announcements.clear() def filter_orphan_channel_anns( self, channel_anns: List[GossipForwardingMessage] ) -> Tuple[List, List]: """Check if the channel announcements we want to forward have at least 1 update""" to_forward_anns = [] orphaned_channel_anns = [] for channel in channel_anns: if channel.scid is None: continue elif (channel.scid in self._chans_with_1_policies or channel.scid in self._chans_with_2_policies): to_forward_anns.append(channel) continue orphaned_channel_anns.append(channel) return to_forward_anns, orphaned_channel_anns def set_fwd_channel_anns_ts(self, channel_anns: List[GossipForwardingMessage]) \ -> List[GossipForwardingMessage]: """Set the timestamps of the passed channel announcements from the corresponding policies""" timestamped_chan_anns: List[GossipForwardingMessage] = [] with self.lock: policies = self._policies.copy() channels = self._channels.copy() for chan_ann in channel_anns: if chan_ann.timestamp is not None: timestamped_chan_anns.append(chan_ann) continue scid = chan_ann.scid if (channel_info := channels.get(scid)) is None: continue policy1 = policies.get((channel_info.node1_id, scid)) policy2 = policies.get((channel_info.node2_id, scid)) potential_timestamps = [] for policy in [policy1, policy2]: if policy is not None: potential_timestamps.append(policy.timestamp) if not potential_timestamps: continue chan_ann.timestamp = min(potential_timestamps) timestamped_chan_anns.append(chan_ann) return timestamped_chan_anns def get_forwarding_gossip_batch(self) -> List[GossipForwardingMessage]: with self.forwarding_lock: fwd_gossip = self.fwd_channel_updates + self.fwd_node_announcements channel_anns = self.fwd_channels.copy() self.clear_forwarding_gossip() fwd_chan_anns1, _ = self.filter_orphan_channel_anns(self.fwd_orphan_channels) fwd_chan_anns2, self.fwd_orphan_channels = self.filter_orphan_channel_anns(channel_anns) channel_anns = self.set_fwd_channel_anns_ts(fwd_chan_anns1 + fwd_chan_anns2) return channel_anns + fwd_gossip def get_gossip_in_timespan(self, timespan: GossipTimestampFilter) \ -> List[GossipForwardingMessage]: """Return a list of gossip messages matching the requested timespan.""" forwarding_gossip = [] with self.lock: chans = self._channels.copy() policies = self._policies.copy() nodes = self._nodes.copy() for short_id, chan in chans.items(): # fetching the timestamp from the channel update (according to BOLT-07) chan_up_n1 = policies.get((chan.node1_id, short_id)) chan_up_n2 = policies.get((chan.node2_id, short_id)) updates = [] for policy in [chan_up_n1, chan_up_n2]: if policy and policy.raw and timespan.in_range(policy.timestamp): if policy.message_flags & 0b10 == 0: # check that its not "dont_forward" updates.append(GossipForwardingMessage( msg=policy.raw, timestamp=policy.timestamp)) if not updates or chan.raw is None: continue chan_ann_ts = min(update.timestamp for update in updates) channel_announcement = GossipForwardingMessage(msg=chan.raw, timestamp=chan_ann_ts) forwarding_gossip.extend([channel_announcement] + updates) for node_ann in nodes.values(): if timespan.in_range(node_ann.timestamp) and node_ann.raw: forwarding_gossip.append(GossipForwardingMessage( msg=node_ann.raw, timestamp=node_ann.timestamp)) return forwarding_gossip def get_channels_in_range(self, first_blocknum: int, number_of_blocks: int) -> List[ShortChannelID]: with self.lock: channels = self._channels.copy() scids: List[ShortChannelID] = [] for scid in channels: if first_blocknum <= scid.block_height < first_blocknum + number_of_blocks: scids.append(scid) scids.sort() return scids def get_gossip_for_scid_request(self, scid: ShortChannelID) -> List[bytes]: requested_gossip = [] chan_ann = self._channels.get(scid) if not chan_ann or not chan_ann.raw: return [] chan_up1 = self._policies.get((chan_ann.node1_id, scid)) chan_up2 = self._policies.get((chan_ann.node2_id, scid)) node_ann1 = self._nodes.get(chan_ann.node1_id) node_ann2 = self._nodes.get(chan_ann.node2_id) for msg in [chan_ann, chan_up1, chan_up2, node_ann1, node_ann2]: if msg and msg.raw: requested_gossip.append(msg.raw) return requested_gossip def to_dict(self) -> dict: """ Generates a graph representation in terms of a dictionary. The dictionary contains only native python types and can be encoded to json. """ with self.lock: graph = {'nodes': [], 'channels': []} # gather nodes for pk, nodeinfo in self._nodes.items(): # use _asdict() to convert NamedTuples to json encodable dicts graph['nodes'].append( nodeinfo._asdict(), ) graph['nodes'][-1]['addresses'] = [ {'host': str(addr.host), 'port': addr.port, 'timestamp': ts} for addr, ts in self._addresses[pk].items() ] # gather channels for cid, channelinfo in self._channels.items(): graph['channels'].append( channelinfo._asdict(), ) policy1 = self._policies.get( (channelinfo.node1_id, channelinfo.short_channel_id)) policy2 = self._policies.get( (channelinfo.node2_id, channelinfo.short_channel_id)) graph['channels'][-1]['policy1'] = policy1._asdict() if policy1 else None graph['channels'][-1]['policy2'] = policy2._asdict() if policy2 else None # need to use json_normalize otherwise json encoding in rpc server fails graph = json_normalize(graph) return graph ================================================ FILE: electrum/coinchooser.py ================================================ #!/usr/bin/env python # # Electrum - lightweight Bitcoin client # Copyright (C) 2015 kyuupichan@gmail # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files # (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, # publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from collections import defaultdict from math import floor, log10 from typing import NamedTuple, List, Callable, Sequence, Dict, Tuple, Mapping, Type, TYPE_CHECKING from decimal import Decimal from .bitcoin import sha256, COIN, is_address from .transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput from .util import NotEnoughFunds from .logging import Logger if TYPE_CHECKING: from .simple_config import SimpleConfig # A simple deterministic PRNG. Used to deterministically shuffle a # set of coins - the same set of coins should produce the same output. # Although choosing UTXOs "randomly" we want it to be deterministic, # so if sending twice from the same UTXO set we choose the same UTXOs # to spend. This prevents attacks on users by malicious or stale # servers. class PRNG: def __init__(self, seed): self.sha = sha256(seed) self.pool = bytearray() def get_bytes(self, n: int) -> bytes: while len(self.pool) < n: self.pool.extend(self.sha) self.sha = sha256(self.sha) result, self.pool = self.pool[:n], self.pool[n:] return bytes(result) def randint(self, start, end): # Returns random integer in [start, end) n = end - start r = 0 p = 1 while p < n: r = self.get_bytes(1)[0] + (r << 8) p = p << 8 return start + (r % n) def choice(self, seq): return seq[self.randint(0, len(seq))] def shuffle(self, x): for i in reversed(range(1, len(x))): # pick an element in x[:i+1] with which to exchange x[i] j = self.randint(0, i+1) x[i], x[j] = x[j], x[i] class Bucket(NamedTuple): desc: str weight: int # as in BIP-141 value: int # in satoshis effective_value: int # estimate of value left after subtracting fees. in satoshis coins: List[PartialTxInput] # UTXOs min_height: int # min block height where a coin was confirmed witness: bool # whether any coin uses segwit class ScoredCandidate(NamedTuple): penalty: float tx: PartialTransaction buckets: List[Bucket] def strip_unneeded(bkts: List[Bucket], sufficient_funds: Callable) -> List[Bucket]: '''Remove buckets that are unnecessary in achieving the spend amount''' if sufficient_funds([], bucket_value_sum=0): # none of the buckets are needed return [] bkts = sorted(bkts, key=lambda bkt: bkt.value, reverse=True) bucket_value_sum = 0 for i in range(len(bkts)): bucket_value_sum += (bkts[i]).value if sufficient_funds(bkts[:i+1], bucket_value_sum=bucket_value_sum): return bkts[:i+1] raise Exception("keeping all buckets is still not enough") class CoinChooserBase(Logger): def __init__(self, *, enable_output_value_rounding: bool): Logger.__init__(self) self.enable_output_value_rounding = enable_output_value_rounding def keys(self, coins: Sequence[PartialTxInput]) -> Sequence[str]: raise NotImplementedError def bucketize_coins( self, coins: Sequence[PartialTxInput], *, fee_estimator_vb: Callable[[int | float | Decimal], int], ): keys = self.keys(coins) buckets = defaultdict(list) # type: Dict[str, List[PartialTxInput]] for key, coin in zip(keys, coins): buckets[key].append(coin) # fee_estimator returns fee to be paid, for given vbytes. # guess whether it is just returning a constant as follows. constant_fee = fee_estimator_vb(2000) == fee_estimator_vb(200) def make_Bucket(desc: str, coins: List[PartialTxInput]): witness = any(coin.is_segwit(guess_for_address=True) for coin in coins) # note that we're guessing whether the tx uses segwit based # on this single bucket weight = sum(Transaction.estimated_input_weight(coin, witness) for coin in coins) value = sum(coin.value_sats() for coin in coins) min_height = min(coin.block_height for coin in coins) assert min_height is not None # the fee estimator is typically either a constant or a linear function, # so the "function:" effective_value(bucket) will be homomorphic for addition # i.e. effective_value(b1) + effective_value(b2) = effective_value(b1 + b2) if constant_fee: effective_value = value else: # when converting from weight to vBytes, instead of rounding up, # keep fractional part, to avoid overestimating fee fee = fee_estimator_vb(Decimal(weight) / 4) effective_value = value - fee return Bucket(desc=desc, weight=weight, value=value, effective_value=effective_value, coins=coins, min_height=min_height, witness=witness) return list(map(make_Bucket, buckets.keys(), buckets.values())) def penalty_func( self, base_tx: Transaction, *, tx_from_buckets: Callable[[List[Bucket]], Tuple[PartialTransaction, List[PartialTxOutput]]], ) -> Callable[[List[Bucket]], ScoredCandidate]: raise NotImplementedError def _change_amounts(self, tx: PartialTransaction, count: int, fee_estimator_numchange) -> List[int]: # Break change up if bigger than max_change output_amounts = [o.value for o in tx.outputs()] # Don't split change of less than 0.02 BTC max_change = max([0.02 * COIN] + output_amounts) * 1.25 # Use N change outputs for n in range(1, count + 1): # How much is left if we add this many change outputs? change_amount = max(0, tx.get_fee() - fee_estimator_numchange(n)) if change_amount // n <= max_change: break # Get a handle on the precision of the output amounts; round our # change to look similar def trailing_zeroes(val): s = str(val) return len(s) - len(s.rstrip('0')) zeroes = [trailing_zeroes(i) for i in output_amounts] min_zeroes = min([8] + zeroes) max_zeroes = max([0] + zeroes) if n > 1: zeroes = range(max(0, min_zeroes - 1), (max_zeroes + 1) + 1) else: # if there is only one change output, this will ensure that we aim # to have one that is exactly as precise as the most precise output zeroes = [min_zeroes] # Calculate change; randomize it a bit if using more than 1 output remaining = change_amount amounts = [] while n > 1: average = remaining / n amount = self.p.randint(int(average * 0.7), int(average * 1.3)) precision = min(self.p.choice(zeroes), int(floor(log10(amount)))) amount = int(round(amount, -precision)) amounts.append(amount) remaining -= amount n -= 1 # Last change output. Round down to maximum precision but lose # no more than 10**max_dp_to_round_for_privacy # e.g. a max of 2 decimal places means losing 100 satoshis to fees # don't round if the fee estimator is set to 0 fixed fee, so a 0 fee tx remains a 0 fee tx is_zero_fee_tx = True if fee_estimator_numchange(1) == 0 else False output_value_rounding = self.enable_output_value_rounding and not is_zero_fee_tx max_dp_to_round_for_privacy = 2 if output_value_rounding else 0 N = int(pow(10, min(max_dp_to_round_for_privacy, zeroes[0]))) amount = (remaining // N) * N amounts.append(amount) assert sum(amounts) <= change_amount return amounts def _change_outputs(self, tx: PartialTransaction, change_addrs, fee_estimator_numchange, dust_threshold) -> List[PartialTxOutput]: amounts = self._change_amounts(tx, len(change_addrs), fee_estimator_numchange) assert min(amounts) >= 0 assert len(change_addrs) >= len(amounts) assert all([isinstance(amt, int) for amt in amounts]) # If change is above dust threshold after accounting for the # size of the change output, add it to the transaction. amounts = [amount for amount in amounts if amount >= dust_threshold] change = [PartialTxOutput.from_address_and_value(addr, amount) for addr, amount in zip(change_addrs, amounts)] for c in change: c.is_change = True return change def _construct_tx_from_selected_buckets( self, *, buckets: Sequence[Bucket], base_tx: PartialTransaction, change_addrs, fee_estimator_w, dust_threshold, base_weight, BIP69_sort: bool, ) -> Tuple[PartialTransaction, List[PartialTxOutput]]: # make a copy of base_tx so it won't get mutated tx = PartialTransaction.from_io(base_tx.inputs()[:], base_tx.outputs()[:], BIP69_sort=BIP69_sort) tx.add_inputs([coin for b in buckets for coin in b.coins], BIP69_sort=BIP69_sort) tx_weight = self._get_tx_weight(buckets, base_weight=base_weight) # change is sent back to sending address unless specified if not change_addrs: change_addrs = [tx.inputs()[0].address] # note: this is not necessarily the final "first input address" # because the inputs had not been sorted at this point assert is_address(change_addrs[0]) # This takes a count of change outputs and returns a tx fee output_weight = 4 * Transaction.estimated_output_size_for_address(change_addrs[0]) fee_estimator_numchange = lambda count: fee_estimator_w(tx_weight + count * output_weight) change = self._change_outputs(tx, change_addrs, fee_estimator_numchange, dust_threshold) tx.add_outputs(change, BIP69_sort=BIP69_sort) return tx, change def _get_tx_weight(self, buckets: Sequence[Bucket], *, base_weight: int) -> int: """Given a collection of buckets, return the total weight of the resulting transaction. base_weight is the weight of the tx that includes the fixed (non-change) outputs and potentially some fixed inputs. Note that the change outputs at this point are not yet known so they are NOT accounted for. """ total_weight = base_weight + sum(bucket.weight for bucket in buckets) is_segwit_tx = any(bucket.witness for bucket in buckets) if is_segwit_tx: total_weight += 2 # marker and flag # non-segwit inputs were previously assumed to have # a witness of '' instead of '00' (hex) # note that mixed legacy/segwit buckets are already ok num_legacy_inputs = sum((not bucket.witness) * len(bucket.coins) for bucket in buckets) total_weight += num_legacy_inputs return total_weight def make_tx( self, *, coins: Sequence[PartialTxInput], inputs: List[PartialTxInput], outputs: List[PartialTxOutput], change_addrs: Sequence[str], fee_estimator_vb: Callable[[int | float | Decimal], int], dust_threshold: int, BIP69_sort: bool = True, ) -> PartialTransaction: """Select unspent coins to spend to pay outputs. If the change is greater than dust_threshold (after adding the change output to the transaction) it is kept, otherwise none is sent and it is added to the transaction fee. `inputs` and `outputs` are guaranteed to be a subset of the inputs and outputs of the resulting transaction. `coins` are further UTXOs we can choose from. Note: fee_estimator_vb expects virtual bytes """ # Deterministic randomness from coins utxos = [c.prevout.serialize_to_network() for c in coins] self.p = PRNG(b''.join(sorted(utxos))) assert len(outputs) > 0 or len(change_addrs) == 1, \ "sweeps with 0 outputs should not use multiple change addresses" # Copy the outputs so when adding change we don't modify "outputs" base_tx = PartialTransaction.from_io(inputs[:], outputs[:], BIP69_sort=BIP69_sort) input_value = base_tx.input_value() # Weight of the transaction with no inputs and no change # Note: this will use legacy tx serialization as the need for "segwit" # would be detected from inputs. The only side effect should be that the # marker and flag are excluded, which is compensated in get_tx_weight() # FIXME calculation will be off by this (2 wu) in case of RBF batching base_weight = base_tx.estimated_weight() # by setting spent_amount = dust_threshold if there are no outputs we ensure that # enough inputs are added so there is always at least a change output created # as txs have to have at least 1 output according to consensus rules spent_amount = base_tx.output_value() if outputs else dust_threshold def fee_estimator_w(weight): return fee_estimator_vb(Transaction.virtual_size_from_weight(weight)) def sufficient_funds(buckets: List[Bucket], *, bucket_value_sum: int) -> bool: '''Given a list of buckets, return True if it has enough value to pay for the transaction''' # assert bucket_value_sum == sum(bucket.value for bucket in buckets) # expensive! total_input = input_value + bucket_value_sum if total_input < spent_amount: # shortcut for performance return False # any bitcoin tx must have at least 1 input by consensus # (check we add some new UTXOs now or already have some fixed inputs) if not buckets and not inputs: return False # note re performance: so far this was constant time # what follows is linear in len(buckets) total_weight = self._get_tx_weight(buckets, base_weight=base_weight) return total_input >= spent_amount + fee_estimator_w(total_weight) def tx_from_buckets(buckets): return self._construct_tx_from_selected_buckets( buckets=buckets, base_tx=base_tx, change_addrs=change_addrs, fee_estimator_w=fee_estimator_w, dust_threshold=dust_threshold, base_weight=base_weight, BIP69_sort=BIP69_sort, ) # Collect the coins into buckets all_buckets = self.bucketize_coins(coins, fee_estimator_vb=fee_estimator_vb) # Filter some buckets out. Only keep those that have positive effective value. # Note that this filtering is intentionally done on the bucket level # instead of per-coin, as each bucket should be either fully spent or not at all. # (e.g. CoinChooserPrivacy ensures that same-address coins go into one bucket) all_buckets = list(filter(lambda b: b.effective_value > 0, all_buckets)) # Choose a subset of the buckets scored_candidate = self.choose_buckets(all_buckets, sufficient_funds, self.penalty_func(base_tx, tx_from_buckets=tx_from_buckets)) tx = scored_candidate.tx self.logger.info(f"using {len(tx.inputs())} inputs") self.logger.info(f"using buckets: {[bucket.desc for bucket in scored_candidate.buckets]}") return tx def choose_buckets(self, buckets: List[Bucket], sufficient_funds: Callable, penalty_func: Callable[[List[Bucket]], ScoredCandidate]) -> ScoredCandidate: raise NotImplemented('To be subclassed') class CoinChooserRandom(CoinChooserBase): def bucket_candidates_any( self, buckets: List[Bucket], sufficient_funds: Callable, ) -> List[List[Bucket]]: '''Returns a list of bucket sets.''' if not buckets: if sufficient_funds([], bucket_value_sum=0): return [[]] else: raise NotEnoughFunds() candidates = set() # Add all singletons for n, bucket in enumerate(buckets): if sufficient_funds([bucket], bucket_value_sum=bucket.value): candidates.add((n,)) # And now some random ones attempts = min(100, (len(buckets) - 1) * 10 + 1) permutation = list(range(len(buckets))) for i in range(attempts): # Get a random permutation of the buckets, and # incrementally combine buckets until sufficient self.p.shuffle(permutation) bkts = [] bucket_value_sum = 0 for count, index in enumerate(permutation): bucket = buckets[index] bkts.append(bucket) bucket_value_sum += bucket.value if sufficient_funds(bkts, bucket_value_sum=bucket_value_sum): candidates.add(tuple(sorted(permutation[:count + 1]))) break else: # note: this assumes that the effective value of any bkt is >= 0 raise NotEnoughFunds() candidates = [[buckets[n] for n in c] for c in candidates] return [strip_unneeded(c, sufficient_funds) for c in candidates] def bucket_candidates_prefer_confirmed( self, buckets: List[Bucket], sufficient_funds: Callable, ) -> List[List[Bucket]]: """Returns a list of bucket sets preferring confirmed coins. Any bucket can be: 1. "confirmed" if it only contains confirmed coins; else 2. "unconfirmed" if it does not contain coins with unconfirmed parents 3. other: e.g. "unconfirmed parent" or "local" This method tries to only use buckets of type 1, and if the coins there are not enough, tries to use the next type but while also selecting all buckets of all previous types. """ conf_buckets = [bkt for bkt in buckets if bkt.min_height > 0] unconf_buckets = [bkt for bkt in buckets if bkt.min_height == 0] other_buckets = [bkt for bkt in buckets if bkt.min_height < 0] bucket_sets = [conf_buckets, unconf_buckets, other_buckets] already_selected_buckets = [] already_selected_buckets_value_sum = 0 for bkts_choose_from in bucket_sets: try: def sfunds( bkts: List[Bucket], *, bucket_value_sum: int, already_selected_buckets_value_sum=already_selected_buckets_value_sum, already_selected_buckets=already_selected_buckets, ): bucket_value_sum += already_selected_buckets_value_sum return sufficient_funds(already_selected_buckets + bkts, bucket_value_sum=bucket_value_sum) candidates = self.bucket_candidates_any(bkts_choose_from, sfunds) break except NotEnoughFunds: already_selected_buckets += bkts_choose_from already_selected_buckets_value_sum += sum(bucket.value for bucket in bkts_choose_from) else: raise NotEnoughFunds() candidates = [(already_selected_buckets + c) for c in candidates] return [strip_unneeded(c, sufficient_funds) for c in candidates] def choose_buckets(self, buckets, sufficient_funds, penalty_func): candidates = self.bucket_candidates_prefer_confirmed(buckets, sufficient_funds) scored_candidates = [penalty_func(cand) for cand in candidates] winner = min(scored_candidates, key=lambda x: x.penalty) self.logger.info(f"Total number of buckets: {len(buckets)}") self.logger.info(f"Num candidates considered: {len(candidates)}. " f"Winning penalty: {winner.penalty}") return winner class CoinChooserPrivacy(CoinChooserRandom): """Attempts to better preserve user privacy. First, if any coin is spent from a user address, all coins are. Compared to spending from other addresses to make up an amount, this reduces information leakage about sender holdings. It also helps to reduce blockchain UTXO bloat, and reduce future privacy loss that would come from reusing that address' remaining UTXOs. Second, it penalizes change that is quite different to the sent amount. Third, it penalizes change that is too big. """ def keys(self, coins): return [coin.scriptpubkey.hex() for coin in coins] def penalty_func(self, base_tx, *, tx_from_buckets): if _outputs := base_tx.outputs(): min_change = min(o.value for o in _outputs) * 0.75 max_change = max(o.value for o in _outputs) * 1.33 else: min_change = 0 max_change = 0.02 * COIN def penalty(buckets: List[Bucket]) -> ScoredCandidate: # Penalize using many buckets (~inputs) badness = len(buckets) - 1 tx, change_outputs = tx_from_buckets(buckets) change = sum(o.value for o in change_outputs) # Penalize change not roughly in output range if change == 0: pass # no change is great! elif change < min_change: badness += (min_change - change) / (min_change + 10000) # Penalize really small change; under 1 mBTC ~= using 1 more input if change < COIN / 1000: badness += 1 elif change > max_change: badness += (change - max_change) / (max_change + 10000) # Penalize large change; 5 BTC excess ~= using 1 more input badness += change / (COIN * 5) return ScoredCandidate(badness, tx, buckets) return penalty COIN_CHOOSERS = { 'Privacy': CoinChooserPrivacy, } # type: Mapping[str, Type[CoinChooserBase]] def get_name(config: 'SimpleConfig') -> str: kind = config.WALLET_COIN_CHOOSER_POLICY if kind not in COIN_CHOOSERS: kind = config.cv.WALLET_COIN_CHOOSER_POLICY.get_default_value() return kind def get_coin_chooser(config: 'SimpleConfig') -> CoinChooserBase: klass = COIN_CHOOSERS[get_name(config)] # note: we enable enable_output_value_rounding by default as # - for sacrificing a few satoshis # + it gives better privacy for the user re change output # + it also helps the network as a whole as fees will become noisier # (trying to counter the heuristic that "whole integer sat/byte feerates" are common) coinchooser = klass( enable_output_value_rounding=config.WALLET_COIN_CHOOSER_OUTPUT_ROUNDING, ) return coinchooser ================================================ FILE: electrum/commands.py ================================================ #!/usr/bin/env python # # Electrum - lightweight Bitcoin client # Copyright (C) 2011 thomasv@gitorious # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files # (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, # publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import io import sys import datetime import time import argparse import json import ast import binascii import base64 import asyncio import inspect from asyncio import CancelledError from collections import defaultdict from functools import wraps from decimal import Decimal, InvalidOperation from typing import Optional, TYPE_CHECKING, Dict, List, Any, Union import os import re import electrum_ecc as ecc from . import util from .lnmsg import OnionWireSerializer from .lnworker import LN_P2P_NETWORK_TIMEOUT from .logging import Logger from .onion_message import create_blinded_path, send_onion_message_to from .submarine_swaps import NostrTransport from .util import ( bfh, json_decode, json_normalize, is_hash256_str, is_hex_str, to_bytes, parse_max_spend, to_decimal, UserFacingException, InvalidPassword ) from . import bitcoin from .bitcoin import is_address, hash_160, COIN from .bip32 import BIP32Node from .i18n import _ from .transaction import ( Transaction, multisig_script, PartialTransaction, PartialTxOutput, tx_from_any, PartialTxInput, TxOutpoint, convert_raw_tx_to_hex ) from . import transaction from .invoices import Invoice, PR_PAID, PR_UNPAID, PR_EXPIRED from .synchronizer import Notifier from .wallet import ( Abstract_Wallet, create_new_wallet, restore_wallet_from_text, Deterministic_Wallet, BumpFeeStrategy, Imported_Wallet ) from .address_synchronizer import TX_HEIGHT_LOCAL from .mnemonic import Mnemonic from .lnutil import (channel_id_from_funding_tx, LnFeatures, SENT, RECEIVED, MIN_FINAL_CLTV_DELTA_ACCEPTED, PaymentFeeBudget, NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE) from .plugin import run_hook, DeviceMgr, Plugins from .version import ELECTRUM_VERSION from .simple_config import SimpleConfig from .fee_policy import FeePolicy, FEE_ETA_TARGETS, FEERATE_DEFAULT_RELAY from . import GuiImportError from . import crypto from . import constants from . import descriptor if TYPE_CHECKING: from .network import Network from .daemon import Daemon from electrum.lnworker import PaymentInfo known_commands = {} # type: Dict[str, Command] class NotSynchronizedException(UserFacingException): pass def satoshis_or_max(amount): return satoshis(amount) if not parse_max_spend(amount) else amount def satoshis(amount): # satoshi conversion must not be performed by the parser return int(COIN*to_decimal(amount)) if amount is not None else None def format_satoshis(x: Union[float, int, Decimal, None]) -> Optional[str]: """ input: satoshis as a Number output: str formatted as bitcoin amount """ if x is None: return None return util.format_satoshis_plain(x, is_max_allowed=False) class Command: def __init__(self, func, name, s): self.name = name self.requires_network = 'n' in s # better name would be "requires daemon" self.requires_wallet = 'w' in s self.requires_password = 'p' in s self.requires_lightning = 'l' in s self.parse_docstring(func.__doc__) varnames = func.__code__.co_varnames[1:func.__code__.co_argcount] self.defaults = func.__defaults__ if self.defaults: n = len(self.defaults) self.params = list(varnames[:-n]) self.options = list(varnames[-n:]) else: self.params = list(varnames) self.options = [] self.defaults = [] # sanity checks if self.requires_password: assert self.requires_wallet for varname in ('wallet_path', 'wallet'): if varname in varnames: assert varname in self.options, f"cmd: {self.name}: {varname} not in options {self.options}" assert not ('wallet_path' in varnames and 'wallet' in varnames) if self.requires_wallet: assert 'wallet' in varnames def parse_docstring(self, docstring): docstring = docstring or '' docstring = docstring.strip() self.description = docstring self.arg_descriptions = {} self.arg_types = {} for x in re.finditer(r'arg:(.*?):(.*?):(.*)$', docstring, flags=re.MULTILINE): self.arg_descriptions[x.group(2)] = x.group(3) self.arg_types[x.group(2)] = x.group(1) self.description = self.description.replace(x.group(), '') self.short_description = self.description.split('.')[0] def command(s): def decorator(func): if hasattr(func, '__wrapped__'): # plugin command function name = func.plugin_name + '_' + func.__name__ known_commands[name] = Command(func.__wrapped__, name, s) else: # regular command function name = func.__name__ known_commands[name] = Command(func, name, s) @wraps(func) async def func_wrapper(*args, **kwargs): cmd_runner = args[0] # type: Commands cmd = known_commands[name] # type: Command password = kwargs.get('password') daemon = cmd_runner.daemon if daemon: if 'wallet_path' in cmd.options or cmd.requires_wallet: kwargs['wallet_path'] = daemon.config.maybe_complete_wallet_path(kwargs.get('wallet_path')) if 'wallet' in cmd.options: wallet_path = kwargs.pop('wallet_path', None) # unit tests may set wallet and not wallet_path wallet = kwargs.get('wallet', None) # run_offline_command sets both if wallet is None and wallet_path is not None: wallet = daemon.get_wallet(wallet_path) if wallet is None: raise UserFacingException('wallet not loaded') kwargs['wallet'] = wallet if cmd.requires_password and password is None and wallet and wallet.has_password(): password = wallet.get_unlocked_password() if password: kwargs['password'] = password else: raise UserFacingException('Password required. Unlock the wallet, or add a --password option to your command') wallet = kwargs.get('wallet') # type: Optional[Abstract_Wallet] if cmd.requires_wallet and not wallet: raise UserFacingException('wallet not loaded') if cmd.requires_password and wallet.has_password(): if password is None: raise UserFacingException('Password required') try: wallet.check_password(password) except InvalidPassword as e: raise UserFacingException(str(e)) from None if cmd.requires_lightning and (not wallet or not wallet.has_lightning()): raise UserFacingException('Lightning not enabled in this wallet') return await func(*args, **kwargs) return func_wrapper return decorator class Commands(Logger): def __init__(self, *, config: 'SimpleConfig', network: 'Network' = None, daemon: 'Daemon' = None, callback=None): Logger.__init__(self) self.config = config self.daemon = daemon self.network = network self._callback = callback def _run(self, method, args, password_getter=None, **kwargs): """This wrapper is called from unit tests and the Qt python console.""" cmd = known_commands[method] password = kwargs.get('password', None) wallet = kwargs.get('wallet', None) if (cmd.requires_password and wallet and wallet.has_password() and password is None): password = password_getter() if password is None: return f = getattr(self, method) if cmd.requires_password: kwargs['password'] = password if 'wallet' in kwargs: sig = inspect.signature(f) if 'wallet' not in sig.parameters: kwargs.pop('wallet') coro = f(*args, **kwargs) fut = asyncio.run_coroutine_threadsafe(coro, util.get_asyncio_loop()) result = fut.result() if self._callback: self._callback() return result @command('n') async def getinfo(self): """ network info """ net_params = self.network.get_parameters() response = { 'network': constants.net.NET_NAME, 'path': self.network.config.path, 'server': net_params.server.host, 'blockchain_height': self.network.get_local_height(), 'server_height': self.network.get_server_height(), 'spv_nodes': len(self.network.get_interfaces()), 'connected': self.network.is_connected(), 'auto_connect': net_params.auto_connect, 'version': ELECTRUM_VERSION, 'fee_estimates': self.network.fee_estimates.get_data() } return response @command('n') async def stop(self): """Stop daemon""" await self.daemon.stop() return "Daemon stopped" @command('n') async def list_wallets(self): """List wallets open in daemon""" return [ { 'path': w.db.storage.path, 'synchronized': w.is_up_to_date(), 'unlocked': not w.has_password() or (w.get_unlocked_password() is not None), } for w in self.daemon.get_wallets().values() ] @command('n') async def load_wallet(self, wallet_path=None, password=None): """ Load the wallet in memory """ wallet = self.daemon.load_wallet(wallet_path, password, upgrade=True) if wallet is None: raise UserFacingException('could not load wallet') run_hook('load_wallet', wallet, None) return wallet_path @command('n') async def close_wallet(self, wallet_path=None): """Close wallet""" return await self.daemon._stop_wallet(wallet_path) @command('') async def create(self, passphrase=None, password=None, encrypt_file=True, seed_type=None, wallet_path=None): """Create a new wallet. If you want to be prompted for an argument, type '?' or ':' (concealed) arg:str:passphrase:Seed extension arg:str:seed_type:The type of wallet to create, e.g. 'standard' or 'segwit' arg:bool:encrypt_file:Whether the file on disk should be encrypted with the provided password """ d = create_new_wallet( path=wallet_path, passphrase=passphrase, password=password, encrypt_file=encrypt_file, seed_type=seed_type, config=self.config) return { 'seed': d['seed'], 'path': d['wallet'].storage.path, 'msg': d['msg'], } @command('') async def restore(self, text, passphrase=None, password=None, encrypt_file=True, wallet_path=None): """Restore a wallet from text. Text can be a seed phrase, a master public key, a master private key, a list of bitcoin addresses or bitcoin private keys. If you want to be prompted for an argument, type '?' or ':' (concealed) arg:str:text:seed phrase arg:str:passphrase:Seed extension arg:bool:encrypt_file:Whether the file on disk should be encrypted with the provided password """ # TODO create a separate command that blocks until wallet is synced d = restore_wallet_from_text( text, path=wallet_path, passphrase=passphrase, password=password, encrypt_file=encrypt_file, config=self.config) return { 'path': d['wallet'].storage.path, 'msg': d['msg'], } @command('wp') async def password(self, password=None, new_password=None, encrypt_file=None, wallet: Abstract_Wallet = None): """ Change wallet password. arg:bool:encrypt_file:Whether the file on disk should be encrypted with the provided password (default=true) arg:str:new_password:New Password """ if wallet.storage.is_encrypted_with_hw_device() and new_password: raise UserFacingException("Can't change the password of a wallet encrypted with a hw device.") if encrypt_file is None: if not password and new_password: # currently no password, setting one now: we encrypt by default encrypt_file = True else: encrypt_file = wallet.storage.is_encrypted() wallet.update_password(password, new_password, encrypt_storage=encrypt_file) wallet.save_db() return {'password': wallet.has_password()} @command('w') async def get(self, key, wallet: Abstract_Wallet = None): """ Return item from wallet storage arg:str:key:storage key """ return wallet.db.get(key) @command('') async def getconfig(self, key): """Return the current value of a configuration variable. arg:str:key:name of the configuration variable """ if Plugins.is_plugin_enabler_config_key(key): return self.config.get(key) else: cv = self.config.cv.from_key(key) return cv.get() @classmethod def _setconfig_normalize_value(cls, key, value): if key not in (SimpleConfig.RPC_USERNAME.key(), SimpleConfig.RPC_PASSWORD.key()): value = json_decode(value) # call literal_eval for backward compatibility (see #4225) try: value = ast.literal_eval(value) except Exception: pass return value def _setconfig(self, key, value): value = self._setconfig_normalize_value(key, value) if self.daemon and key == SimpleConfig.RPC_USERNAME.key(): self.daemon.commands_server.rpc_user = value if self.daemon and key == SimpleConfig.RPC_PASSWORD.key(): self.daemon.commands_server.rpc_password = value if Plugins.is_plugin_enabler_config_key(key): self.config.set_key(key, value) else: cv = self.config.cv.from_key(key) cv.set(value) @command('') async def setconfig(self, key, value): """ Set a configuration variable. arg:str:key:name of the configuration variable arg:str:value:value. may be a string or a Python expression. """ self._setconfig(key, value) @command('') async def unsetconfig(self, key): """ Clear a configuration variable. The variable will be reset to its default value. arg:str:key:name of the configuration variable """ self._setconfig(key, None) @command('') async def listconfig(self): """Returns the list of all configuration variables. """ return self.config.list_config_vars() @command('') async def helpconfig(self, key): """Returns help about a configuration variable. arg:str:key:name of the configuration variable """ cv = self.config.cv.from_key(key) short = cv.get_short_desc() long = cv.get_long_desc() if short and long: return short + "\n---\n\n" + long elif short or long: return short or long else: return f"No description available for '{key}'" @command('') async def make_seed(self, nbits=None, language=None, seed_type=None): """ Create a seed arg:int:nbits:Number of bits of entropy arg:str:seed_type:The type of seed to create, e.g. 'standard' or 'segwit' arg:str:language:Default language for wordlist """ s = Mnemonic(language).make_seed(seed_type=seed_type, num_bits=nbits) return s @command('n') async def getaddresshistory(self, address): """ Return the transaction history of any address. Note: This is a walletless server query, results are not checked by SPV. arg:str:address:Bitcoin address """ sh = bitcoin.address_to_scripthash(address) return await self.network.get_history_for_scripthash(sh) @command('wp') async def unlock(self, wallet: Abstract_Wallet = None, password=None): """Unlock the wallet (store the password in memory).""" wallet.unlock(password) @command('w') async def listunspent(self, wallet: Abstract_Wallet = None): """List unspent outputs. Returns the list of unspent transaction outputs in your wallet.""" coins = [] for txin in wallet.get_utxos(): d = txin.to_json() v = d.pop("value_sats") d["value"] = format_satoshis(v) coins.append(d) return coins @command('n') async def getaddressunspent(self, address): """ Returns the UTXO list of any address. Note: This is a walletless server query, results are not checked by SPV. arg:str:address:Bitcoin address """ sh = bitcoin.address_to_scripthash(address) return await self.network.listunspent_for_scripthash(sh) @command('') async def serialize(self, jsontx): """Create a signed raw transaction from a json tx template. Example value for "jsontx" arg: { "inputs": [ {"prevout_hash": "9d221a69ca3997cbeaf5624d723e7dc5f829b1023078c177d37bdae95f37c539", "prevout_n": 1, "value_sats": 1000000, "privkey": "p2wpkh:cVDXzzQg6RoCTfiKpe8MBvmm5d5cJc6JLuFApsFDKwWa6F5TVHpD"} ], "outputs": [ {"address": "tb1q4s8z6g5jqzllkgt8a4har94wl8tg0k9m8kv5zd", "value_sats": 990000} ] } arg:json:jsontx:Transaction in json """ keypairs = {} inputs = [] # type: List[PartialTxInput] locktime = jsontx.get('locktime', 0) for txin_idx, txin_dict in enumerate(jsontx.get('inputs')): if txin_dict.get('prevout_hash') is not None and txin_dict.get('prevout_n') is not None: prevout = TxOutpoint(txid=bfh(txin_dict['prevout_hash']), out_idx=int(txin_dict['prevout_n'])) elif txin_dict.get('output'): prevout = TxOutpoint.from_str(txin_dict['output']) else: raise UserFacingException(f"missing prevout for txin {txin_idx}") txin = PartialTxInput(prevout=prevout) try: txin._trusted_value_sats = int(txin_dict.get('value') or txin_dict['value_sats']) except KeyError: raise UserFacingException(f"missing 'value_sats' field for txin {txin_idx}") nsequence = txin_dict.get('nsequence', None) if nsequence is not None: txin.nsequence = nsequence sec = txin_dict.get('privkey') if sec: txin_type, privkey, compressed = bitcoin.deserialize_privkey(sec) pubkey = ecc.ECPrivkey(privkey).get_public_key_bytes(compressed=compressed) keypairs[pubkey] = privkey desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey.hex(), script_type=txin_type) txin.script_descriptor = desc inputs.append(txin) outputs = [] # type: List[PartialTxOutput] for txout_idx, txout_dict in enumerate(jsontx.get('outputs')): try: txout_addr = txout_dict['address'] except KeyError: raise UserFacingException(f"missing 'address' field for txout {txout_idx}") try: txout_val = int(txout_dict.get('value') or txout_dict['value_sats']) except KeyError: raise UserFacingException(f"missing 'value_sats' field for txout {txout_idx}") txout = PartialTxOutput.from_address_and_value(txout_addr, txout_val) outputs.append(txout) tx = PartialTransaction.from_io(inputs, outputs, locktime=locktime) tx.sign(keypairs) return tx.serialize() @command('') async def signtransaction_with_privkey(self, tx, privkey): """Sign a transaction with private keys passed as parameter. arg:tx:tx:Transaction to sign arg:str:privkey:private key or list of private keys """ tx = tx_from_any(tx) txins_dict = defaultdict(list) for txin in tx.inputs(): txins_dict[txin.address].append(txin) if not isinstance(privkey, list): privkey = [privkey] for priv in privkey: txin_type, priv2, compressed = bitcoin.deserialize_privkey(priv) pubkey = ecc.ECPrivkey(priv2).get_public_key_bytes(compressed=compressed) desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey.hex(), script_type=txin_type) address = desc.expand().address() if address in txins_dict.keys(): for txin in txins_dict[address]: txin.script_descriptor = desc tx.sign({pubkey: priv2}) return tx.serialize() @command('wp') async def signtransaction(self, tx, password=None, wallet: Abstract_Wallet = None, ignore_warnings: bool=False): """ Sign a transaction with the current wallet. arg:tx:tx:transaction arg:bool:ignore_warnings:ignore warnings """ tx = tx_from_any(tx) wallet.sign_transaction(tx, password, ignore_warnings=ignore_warnings) return tx.serialize() @command('') async def deserialize(self, tx): """ Deserialize a transaction arg:str:tx:Serialized transaction """ tx = tx_from_any(tx) return tx.to_json() @command('n') async def broadcast(self, tx): """ Broadcast a transaction to the network. arg:str:tx:Serialized transaction (must be hexadecimal) """ tx = Transaction(tx) await self.network.broadcast_transaction(tx) return tx.txid() @command('') async def createmultisig(self, num, pubkeys): """ Create multisig 'n of m' address arg:int:num:Number of cosigners required arg:json:pubkeys:List of public keys """ assert isinstance(pubkeys, list), (type(num), type(pubkeys)) redeem_script = multisig_script(pubkeys, num) address = bitcoin.hash160_to_p2sh(hash_160(redeem_script)) return {'address': address, 'redeemScript': redeem_script.hex()} @command('w') async def freeze(self, address: str, wallet: Abstract_Wallet = None): """ Freeze address. Freeze the funds at one of your wallet\'s addresses arg:str:address:Bitcoin address """ return wallet.set_frozen_state_of_addresses([address], True) @command('w') async def unfreeze(self, address: str, wallet: Abstract_Wallet = None): """ Unfreeze address. Unfreeze the funds at one of your wallet\'s address arg:str:address:Bitcoin address """ return wallet.set_frozen_state_of_addresses([address], False) @command('w') async def freeze_utxo(self, coin: str, wallet: Abstract_Wallet = None): """ Freeze a UTXO so that the wallet will not spend it. arg:str:coin:outpoint, in the format """ wallet.set_frozen_state_of_coins([coin], True) return True @command('w') async def unfreeze_utxo(self, coin: str, wallet: Abstract_Wallet = None): """Unfreeze a UTXO so that the wallet might spend it. arg:str:coin:outpoint """ wallet.set_frozen_state_of_coins([coin], False) return True @command('wp') async def getprivatekeys(self, address, password=None, wallet: Abstract_Wallet = None): """ Get private keys of addresses. You may pass a single wallet address, or a list of wallet addresses. arg:str:address:Bitcoin address """ if isinstance(address, str): address = address.strip() if is_address(address): return wallet.export_private_key(address, password) domain = address return [wallet.export_private_key(address, password) for address in domain] @command('wp') async def getprivatekeyforpath(self, path, password=None, wallet: Abstract_Wallet = None): """Get private key corresponding to derivation path (address index). arg:str:path:Derivation path. Can be either a str such as "m/0/50", or a list of ints such as [0, 50]. """ return wallet.export_private_key_for_path(path, password) @command('w') async def ismine(self, address, wallet: Abstract_Wallet = None): """ Check if address is in wallet. Return true if and only address is in wallet arg:str:address:Bitcoin address """ return wallet.is_mine(address) @command('') async def dumpprivkeys(self): """Deprecated.""" return "This command is deprecated. Use a pipe instead: 'electrum listaddresses | electrum getprivatekeys - '" @command('') async def validateaddress(self, address): """Check that an address is valid. arg:str:address:Bitcoin address """ return is_address(address) @command('w') async def getpubkeys(self, address, wallet: Abstract_Wallet = None): """ Return the public keys for a wallet address. arg:str:address:Bitcoin address """ return wallet.get_public_keys(address) @command('w') async def getbalance(self, wallet: Abstract_Wallet = None): """Return the balance of your wallet. """ c, u, x = wallet.get_balance() l = wallet.lnworker.get_balance() if wallet.lnworker else None out = {"confirmed": format_satoshis(c)} if u: out["unconfirmed"] = format_satoshis(u) if x: out["unmatured"] = format_satoshis(x) if l: out["lightning"] = format_satoshis(l) return out @command('n') async def getaddressbalance(self, address): """ Return the balance of any address. Note: This is a walletless server query, results are not checked by SPV. arg:str:address:Bitcoin address """ sh = bitcoin.address_to_scripthash(address) out = await self.network.get_balance_for_scripthash(sh) out["confirmed"] = format_satoshis(out["confirmed"]) out["unconfirmed"] = format_satoshis(out["unconfirmed"]) return out @command('n') async def getmerkle(self, txid, height): """Get Merkle branch of a transaction included in a block. Electrum uses this to verify transactions (Simple Payment Verification). arg:txid:txid:Transaction ID arg:int:height:Block height """ return await self.network.get_merkle_for_transaction(txid, int(height)) @command('n') async def getservers(self): """Return the list of known servers (candidates for connecting).""" return self.network.get_servers() @command('') async def version(self): """Return the version of Electrum.""" return ELECTRUM_VERSION @command('') async def version_info(self): """Return information about dependencies, such as their version and path.""" ret = { "electrum.version": ELECTRUM_VERSION, "electrum.path": os.path.dirname(os.path.realpath(__file__)), "python.version": sys.version, "python.path": sys.executable, } # add currently running GUI if self.daemon and self.daemon.gui_object: ret.update(self.daemon.gui_object.version_info()) # always add Qt GUI, so we get info even when running this from CLI try: from .gui.qt import ElectrumGui as QtElectrumGui ret.update(QtElectrumGui.version_info()) except GuiImportError: pass # Add shared libs (.so/.dll), and non-pure-python dependencies. # Such deps can be installed in various ways - often via the Linux distro's pkg manager, # instead of using pip, hence it is useful to list them for debugging. from electrum_ecc import ecc_fast ret.update(ecc_fast.version_info()) from . import qrscanner ret.update(qrscanner.version_info()) ret.update(DeviceMgr.version_info()) ret.update(crypto.version_info()) # add some special cases import aiohttp ret["aiohttp.version"] = aiohttp.__version__ import aiorpcx ret["aiorpcx.version"] = aiorpcx._version_str import certifi ret["certifi.version"] = certifi.__version__ import dns ret["dnspython.version"] = dns.__version__ import ssl ret["openssl.version"] = ssl.OPENSSL_VERSION return ret @command('w') async def getmpk(self, wallet: Abstract_Wallet = None): """Get master public key. Return your wallet\'s master public key""" return wallet.get_master_public_key() @command('wp') async def getmasterprivate(self, password=None, wallet: Abstract_Wallet = None): """Get master private key. Return your wallet\'s master private key""" return str(wallet.keystore.get_master_private_key(password)) @command('') async def convert_xkey(self, xkey, xtype): """Convert xtype of a master key. e.g. xpub -> ypub arg:str:xkey:the key arg:str:xtype:the type, eg 'xpub' """ try: node = BIP32Node.from_xkey(xkey) except Exception: raise UserFacingException('xkey should be a master public/private key') return node._replace(xtype=xtype).to_xkey() @command('wp') async def getseed(self, password=None, wallet: Abstract_Wallet = None): """Get seed phrase. Print the generation seed of your wallet.""" s = wallet.get_seed(password) return s @command('wp') async def importprivkey(self, privkey, password=None, wallet: Abstract_Wallet = None): """Import a private key or a list of private keys. arg:str:privkey:Private key. Type \'?\' to get a prompt. """ if not wallet.can_import_privkey(): return "Error: This type of wallet cannot import private keys. Try to create a new wallet with that key." assert isinstance(wallet, Imported_Wallet) keys = privkey.split() if not keys: return "Error: no keys given" elif len(keys) == 1: try: addr = wallet.import_private_key(keys[0], password) out = "Keypair imported: " + addr except Exception as e: out = "Error: " + repr(e) return out else: good_inputs, bad_inputs = wallet.import_private_keys(keys, password) return { "good_keys": len(good_inputs), "bad_keys": len(bad_inputs), } async def _resolver(self, x, wallet: Abstract_Wallet): if x is None: return None out = await wallet.contacts.resolve(x) return out['address'] @command('n') async def sweep(self, privkey, destination, fee=None, feerate=None, imax=100): """ Sweep private keys. Returns a transaction that spends UTXOs from privkey to a destination address. The transaction will not be broadcast. arg:str:privkey:Private key. Type \'?\' to get a prompt. arg:str:destination:Bitcoin address, contact or alias arg:decimal:fee:Transaction fee (absolute, in BTC) arg:decimal:feerate:Transaction fee rate (in sat/vbyte) arg:int:imax:Maximum number of inputs """ from .wallet import sweep fee_policy = self._get_fee_policy(fee, feerate) privkeys = privkey.split() #dest = self._resolver(destination) tx = await sweep( privkeys, network=self.network, to_address=destination, fee_policy=fee_policy, imax=imax, ) return tx.serialize() if tx else None @command('wp') async def signmessage(self, address, message, password=None, wallet: Abstract_Wallet = None): """Sign a message with a key. Use quotes if your message contains whitespaces arg:str:address:Bitcoin address arg:str:message:Clear text message. Use quotes if it contains spaces. """ sig = wallet.sign_message(address, message, password) return base64.b64encode(sig).decode('ascii') @command('') async def verifymessage(self, address, signature, message): """Verify a signature. arg:str:address:Bitcoin address arg:str:message:Clear text message. Use quotes if it contains spaces. arg:str:signature:The signature, base64-encoded. """ try: sig = base64.b64decode(signature, validate=True) except binascii.Error: return False message = util.to_bytes(message) return bitcoin.verify_usermessage_with_address(address, sig, message) def _get_fee_policy(self, fee: str, feerate: str): if fee is not None and feerate is not None: raise Exception('Cannot set both fee and feerate') if fee is not None: fee_sats = satoshis(fee) fee_policy = FeePolicy(f'fixed:{fee_sats}') elif feerate is not None: sat_per_kvbyte = int(1000 * to_decimal(feerate)) fee_policy = FeePolicy(f'feerate:{sat_per_kvbyte}') else: fee_policy = FeePolicy(self.config.FEE_POLICY) return fee_policy @command('wp') async def payto(self, destination, amount, fee=None, feerate=None, from_addr=None, from_coins=None, change_addr=None, unsigned=False, rbf=True, password=None, locktime=None, addtransaction=False, wallet: Abstract_Wallet = None): """Create an on-chain transaction. arg:str:destination:Bitcoin address, contact or alias arg:decimal_or_max:amount:Amount to be sent (in BTC). Type '!' to send the maximum available. arg:decimal:fee:Transaction fee (absolute, in BTC) arg:decimal:feerate:Transaction fee rate (in sat/vbyte) arg:str:from_addr:Source address (must be a wallet address; use sweep to spend from non-wallet address) arg:str:change_addr:Change address. Default is a spare address, or the source address if it's not in the wallet arg:bool:rbf:Whether to signal opt-in Replace-By-Fee in the transaction (true/false) arg:bool:addtransaction:Whether transaction is to be used for broadcasting afterwards. Adds transaction to the wallet arg:int:locktime:Set locktime block number arg:bool:unsigned:Do not sign transaction arg:json:from_coins:Source coins (must be in wallet; use sweep to spend from non-wallet address) """ return await self.paytomany( outputs=[(destination, amount),], fee=fee, feerate=feerate, from_addr=from_addr, from_coins=from_coins, change_addr=change_addr, unsigned=unsigned, rbf=rbf, password=password, locktime=locktime, addtransaction=addtransaction, wallet=wallet, ) @command('wp') async def paytomany(self, outputs, fee=None, feerate=None, from_addr=None, from_coins=None, change_addr=None, unsigned=False, rbf=True, password=None, locktime=None, addtransaction=False, wallet: Abstract_Wallet = None): """Create a multi-output transaction. arg:json:outputs:json list of ["address", "amount in BTC"] arg:bool:rbf:Whether to signal opt-in Replace-By-Fee in the transaction (true/false) arg:decimal:fee:Transaction fee (absolute, in BTC) arg:decimal:feerate:Transaction fee rate (in sat/vbyte) arg:str:from_addr:Source address (must be a wallet address; use sweep to spend from non-wallet address) arg:str:change_addr:Change address. Default is a spare address, or the source address if it's not in the wallet arg:bool:addtransaction:Whether transaction is to be used for broadcasting afterwards. Adds transaction to the wallet arg:int:locktime:Set locktime block number arg:bool:unsigned:Do not sign transaction arg:json:from_coins:Source coins (must be in wallet; use sweep to spend from non-wallet address) """ fee_policy = self._get_fee_policy(fee, feerate) domain_addr = from_addr.split(',') if from_addr else None domain_coins = from_coins.split(',') if from_coins else None change_addr = await self._resolver(change_addr, wallet) if domain_addr is not None: resolvers = [self._resolver(addr, wallet) for addr in domain_addr] domain_addr = await asyncio.gather(*resolvers) final_outputs = [] for address, amount in outputs: address = await self._resolver(address, wallet) amount_sat = satoshis_or_max(amount) final_outputs.append(PartialTxOutput.from_address_and_value(address, amount_sat)) coins = wallet.get_spendable_coins(domain_addr) if domain_coins is not None: coins = [coin for coin in coins if (coin.prevout.to_str() in domain_coins)] tx = wallet.make_unsigned_transaction( outputs=final_outputs, fee_policy=fee_policy, change_addr=change_addr, coins=coins, rbf=rbf, locktime=locktime, ) if not unsigned: wallet.sign_transaction(tx, password) result = tx.serialize() if addtransaction: await self.addtransaction(result, wallet=wallet) return result def get_year_timestamps(self, year: int) -> dict[str, Any]: kwargs = {} if year: start_date = datetime.datetime(year, 1, 1) end_date = datetime.datetime(year+1, 1, 1) kwargs['from_timestamp'] = time.mktime(start_date.timetuple()) kwargs['to_timestamp'] = time.mktime(end_date.timetuple()) return kwargs @command('w') async def onchain_capital_gains(self, year=None, wallet: Abstract_Wallet = None): """ Capital gains, using utxo pricing. This cannot be used with lightning. arg:int:year:Show cap gains for a given year """ kwargs = self.get_year_timestamps(year) from .exchange_rate import FxThread fx = self.daemon.fx if self.daemon else FxThread(config=self.config) return json_normalize(wallet.get_onchain_capital_gains(fx, **kwargs)) @command('wp') async def bumpfee(self, tx, new_fee_rate, from_coins=None, decrease_payment=False, password=None, unsigned=False, wallet: Abstract_Wallet = None): """ Bump the fee for an unconfirmed transaction. 'tx' can be either a raw hex tx or a txid. If txid, the corresponding tx must already be part of the wallet history. arg:str:tx:Serialized transaction (hexadecimal) arg:str:new_fee_rate: The Updated/Increased Transaction fee rate (in sats/vbyte) arg:bool:decrease_payment:Whether payment amount will be decreased (true/false) arg:bool:unsigned:Do not sign transaction arg:json:from_coins:Coins that may be used to inncrease the fee (must be in wallet) """ if is_hash256_str(tx): # txid tx = wallet.db.get_transaction(tx) if tx is None: raise UserFacingException("Transaction not in wallet.") else: # raw tx try: tx = Transaction(tx) tx.deserialize() except transaction.SerializationError as e: raise UserFacingException(f"Failed to deserialize transaction: {e}") from e domain_coins = from_coins.split(',') if from_coins else None coins = wallet.get_spendable_coins(None) if domain_coins is not None: coins = [coin for coin in coins if (coin.prevout.to_str() in domain_coins)] tx.add_info_from_wallet(wallet) await tx.add_info_from_network(self.network) new_tx = wallet.bump_fee( tx=tx, coins=coins, strategy=BumpFeeStrategy.DECREASE_PAYMENT if decrease_payment else BumpFeeStrategy.PRESERVE_PAYMENT, new_fee_rate=new_fee_rate) if not unsigned: wallet.sign_transaction(new_tx, password) return new_tx.serialize() @command('w') async def onchain_history( self, show_fiat=False, year=None, show_addresses=False, from_height=None, to_height=None, wallet: Abstract_Wallet = None, ): """Wallet onchain history. Returns the transaction history of your wallet. arg:bool:show_addresses:Show input and output addresses arg:bool:show_fiat:Show fiat value of transactions arg:int:year:Show history for a given year arg:int:from_height:Only show transactions that confirmed after(inclusive) given block height arg:int:to_height:Only show transactions that confirmed before(exclusive) given block height """ # trigger lnwatcher callbacks for their side effects: setting labels and accounting_addresses if not self.network and wallet.lnworker: await wallet.lnworker.lnwatcher.trigger_callbacks(requires_synchronizer=False) kwargs = self.get_year_timestamps(year) kwargs['from_height'] = from_height kwargs['to_height'] = to_height onchain_history = wallet.get_onchain_history(**kwargs) out = [x.to_dict() for x in onchain_history.values()] if show_fiat: from .exchange_rate import FxThread fx = self.daemon.fx if self.daemon else FxThread(config=self.config) else: fx = None for item in out: if show_addresses: tx = wallet.db.get_transaction(item['txid']) item['inputs'] = list(map(lambda x: x.to_json(), tx.inputs())) item['outputs'] = list(map(lambda x: {'address': x.get_ui_address_str(), 'value_sat': x.value}, tx.outputs())) if fx: fiat_fields = wallet.get_tx_item_fiat(tx_hash=item['txid'], amount_sat=item['amount_sat'], fx=fx, tx_fee=item['fee_sat']) item.update(fiat_fields) return json_normalize(out) @command('wl') async def lightning_history(self, wallet: Abstract_Wallet = None): """ lightning history. """ lightning_history = wallet.lnworker.get_lightning_history() if wallet.lnworker else {} sorted_hist= sorted(lightning_history.values(), key=lambda x: x.timestamp) return json_normalize([x.to_dict() for x in sorted_hist]) @command('w') async def setlabel(self, key, label, wallet: Abstract_Wallet = None): """ Assign a label to an item. Item may be a bitcoin address or a transaction ID arg:str:key:Key arg:str:label:Label """ wallet.set_label(key, label) @command('w') async def listcontacts(self, wallet: Abstract_Wallet = None): """Show your list of contacts""" return wallet.contacts @command('w') async def getopenalias(self, key, wallet: Abstract_Wallet = None): """ Retrieve alias. Lookup in your list of contacts, and for an OpenAlias DNS record. arg:str:key:the alias to be retrieved """ d = await wallet.contacts.resolve(key) if d.get("type") == "openalias": # we always validate DNSSEC now d["validated"] = True return d @command('w') async def searchcontacts(self, query, wallet: Abstract_Wallet = None): """ Search through your wallet contacts, return matching entries. arg:str:query:Search query """ results = {} for key, value in wallet.contacts.items(): if query.lower() in key.lower(): results[key] = value return results @command('w') async def listaddresses(self, receiving=False, change=False, labels=False, frozen=False, unused=False, funded=False, balance=False, wallet: Abstract_Wallet = None): """List wallet addresses. Returns the list of all addresses in your wallet. Use optional arguments to filter the results. arg:bool:receiving:Show only receiving addresses arg:bool:change:Show only change addresses arg:bool:frozen:Show only frozen addresses arg:bool:unused:Show only unused addresses arg:bool:funded:Show only funded addresses arg:bool:balance:Show the balances of listed addresses arg:bool:labels:Show the labels of listed addresses """ out = [] for addr in wallet.get_addresses(): if frozen and not wallet.is_frozen_address(addr): continue if receiving and wallet.is_change(addr): continue if change and not wallet.is_change(addr): continue if unused and wallet.adb.is_used(addr): continue if funded and wallet.adb.is_empty(addr): continue item = addr if labels or balance: item = (item,) if balance: item += (format_satoshis(sum(wallet.get_addr_balance(addr))),) if labels: item += (repr(wallet.get_label_for_address(addr)),) out.append(item) return out @command('n') async def gettransaction(self, txid, wallet: Abstract_Wallet = None): """Retrieve a transaction. arg:txid:txid:Transaction ID """ tx = None if wallet: tx = wallet.db.get_transaction(txid) if tx is None: raw = await self.network.get_transaction(txid) if raw: tx = Transaction(raw) else: raise UserFacingException("Unknown transaction") if tx.txid() != txid: raise UserFacingException("Mismatching txid") return tx.serialize() @command('') async def encrypt(self, pubkey, message) -> str: """ Encrypt a message with a public key. Use quotes if the message contains whitespaces. arg:str:pubkey:Public key arg:str:message:Clear text message. Use quotes if it contains spaces. """ if not is_hex_str(pubkey): raise UserFacingException(f"pubkey must be a hex string instead of {repr(pubkey)}") try: message = to_bytes(message) except TypeError: raise UserFacingException(f"message must be a string-like object instead of {repr(message)}") public_key = ecc.ECPubkey(bfh(pubkey)) encrypted = crypto.ecies_encrypt_message(public_key, message) return encrypted.decode('utf-8') @command('wp') async def decrypt(self, pubkey, encrypted, password=None, wallet: Abstract_Wallet = None) -> str: """Decrypt a message encrypted with a public key. arg:str:encrypted:Encrypted message arg:str:pubkey:Public key of one of your wallet addresses """ if not is_hex_str(pubkey): raise UserFacingException(f"pubkey must be a hex string instead of {repr(pubkey)}") if not isinstance(encrypted, (str, bytes, bytearray)): raise UserFacingException(f"encrypted must be a string-like object instead of {repr(encrypted)}") decrypted = wallet.decrypt_message(pubkey, encrypted, password) return decrypted.decode('utf-8') @command('w') async def get_request(self, request_id, wallet: Abstract_Wallet = None): """Returns a payment request arg:str:request_id:The request ID, as seen in list_requests or add_request """ r = wallet.get_request(request_id) if not r: raise UserFacingException("Request not found") return wallet.export_request(r) @command('w') async def get_invoice(self, invoice_id, wallet: Abstract_Wallet = None): """ Returns an invoice (request for outgoing payment) arg:str:invoice_id:The invoice ID, as seen in list_invoices """ r = wallet.get_invoice(invoice_id) if not r: raise UserFacingException("Request not found") return wallet.export_invoice(r) def _filter_invoices(self, _list, wallet, pending, expired, paid): if pending: f = PR_UNPAID elif expired: f = PR_EXPIRED elif paid: f = PR_PAID else: f = None if f is not None: _list = [x for x in _list if f == wallet.get_invoice_status(x)] return _list @command('w') async def list_requests(self, pending=False, expired=False, paid=False, wallet: Abstract_Wallet = None): """ Returns the list of incoming payment requests saved in the wallet. arg:bool:paid:Show only paid requests arg:bool:pending:Show only pending requests arg:bool:expired:Show only expired requests """ l = wallet.get_sorted_requests() l = self._filter_invoices(l, wallet, pending, expired, paid) return [wallet.export_request(x) for x in l] @command('w') async def list_invoices(self, pending=False, expired=False, paid=False, wallet: Abstract_Wallet = None): """ Returns the list of invoices (requests for outgoing payments) saved in the wallet. arg:bool:paid:Show only paid invoices arg:bool:pending:Show only pending invoices arg:bool:expired:Show only expired invoices """ l = wallet.get_invoices() l = self._filter_invoices(l, wallet, pending, expired, paid) return [wallet.export_invoice(x) for x in l] @command('w') async def createnewaddress(self, wallet: Abstract_Wallet = None): """Create a new receiving address, beyond the gap limit of the wallet""" return wallet.create_new_address(False) @command('w') async def changegaplimit(self, new_limit, iknowwhatimdoing=False, wallet: Abstract_Wallet = None): """ Change the gap limit of the wallet. arg:int:new_limit:new gap limit arg:bool:iknowwhatimdoing:Acknowledge that I understand the full implications of what I am about to do """ if not iknowwhatimdoing: raise UserFacingException( "WARNING: Are you SURE you want to change the gap limit?\n" "It makes recovering your wallet from seed difficult!\n" "Please do your research and make sure you understand the implications.\n" "Typically only merchants and power users might want to do this.\n" "To proceed, try again, with the --iknowwhatimdoing option.") if not isinstance(wallet, Deterministic_Wallet): raise UserFacingException("This wallet is not deterministic.") return wallet.change_gap_limit(new_limit) @command('wn') async def getminacceptablegap(self, wallet: Abstract_Wallet = None): """Returns the minimum value for gap limit that would be sufficient to discover all known addresses in the wallet. """ if not isinstance(wallet, Deterministic_Wallet): raise UserFacingException("This wallet is not deterministic.") if not wallet.is_up_to_date(): raise NotSynchronizedException("Wallet not fully synchronized.") return wallet.min_acceptable_gap() @command('w') async def getunusedaddress(self, wallet: Abstract_Wallet = None): """Returns the first unused address of the wallet, or None if all addresses are used. An address is considered as used if it has received a transaction, or if it is used in a payment request.""" return wallet.get_unused_address() @command('w') async def add_request(self, amount, memo='', expiry=3600, lightning=False, force=False, wallet: Abstract_Wallet = None): """Create a payment request, using the first unused address of the wallet. The address will be considered as used after this operation. If no payment is received, the address will be considered as unused if the payment request is deleted from the wallet. arg:decimal:amount:Requested amount (in btc) arg:str:memo:Description of the request arg:bool:force:Create new address beyond gap limit, if no more addresses are available. arg:bool:lightning:Create lightning request. arg:int:expiry:Time in seconds. """ amount = satoshis(amount) if not lightning: addr = wallet.get_unused_address() if addr is None: if force: addr = wallet.create_new_address(False) else: return False else: addr = None expiry = int(expiry) if expiry else None key = wallet.create_request(amount, memo, expiry, addr) req = wallet.get_request(key) return wallet.export_request(req) @command('wnl') async def add_hold_invoice( self, payment_hash: str, amount: Optional[Decimal] = None, memo: str = "", expiry: int = 3600, min_final_cltv_expiry_delta: int = MIN_FINAL_CLTV_DELTA_ACCEPTED * 2, wallet: Abstract_Wallet = None ) -> dict: """ Create a lightning hold invoice for the given payment hash. Hold invoices have to get settled manually later. HTLCs will get failed automatically if block_height + 144 > htlc.cltv_abs, if the intention is to settle them as late as possible a safety margin of some blocks should be used to prevent them from getting failed accidentally. arg:str:payment_hash:Hex encoded payment hash to be used for the invoice arg:decimal:amount:Optional requested amount (in btc) arg:str:memo:Optional description of the invoice arg:int:expiry:Optional expiry in seconds (default: 3600s) arg:int:min_final_cltv_expiry_delta:Optional min final cltv expiry delta (default: 294 blocks) """ assert len(payment_hash) == 64, f"Invalid payment hash length: {len(payment_hash)} != 64" assert not wallet.lnworker.get_payment_info(bfh(payment_hash), direction=RECEIVED), "Payment hash already used!" assert payment_hash not in wallet.lnworker.dont_expire_htlcs, "Payment hash already used!" assert wallet.lnworker.get_preimage(bfh(payment_hash)) is None, "Already got a preimage for this payment hash!" assert MIN_FINAL_CLTV_DELTA_ACCEPTED < min_final_cltv_expiry_delta < 576, "Use a sane min_final_cltv_expiry_delta value" amount = amount if amount and satoshis(amount) > 0 else None # make amount either >0 or None inbound_capacity = wallet.lnworker.num_sats_can_receive() assert inbound_capacity > satoshis(amount or 0), \ f"Not enough inbound capacity [{inbound_capacity} sat] to receive this payment" wallet.lnworker.add_payment_info_for_hold_invoice( bfh(payment_hash), lightning_amount_sat=satoshis(amount) if amount else None, min_final_cltv_delta=min_final_cltv_expiry_delta, exp_delay=expiry, ) info = wallet.lnworker.get_payment_info(bfh(payment_hash), direction=RECEIVED) lnaddr, invoice = wallet.lnworker.get_bolt11_invoice( payment_info=info, message=memo, fallback_address=None ) # this prevents incoming htlcs from getting expired while the preimage isn't set. # If their blocks to expiry fall below MIN_FINAL_CLTV_DELTA_ACCEPTED they will get failed. wallet.lnworker.dont_expire_htlcs[payment_hash] = MIN_FINAL_CLTV_DELTA_ACCEPTED wallet.set_label(payment_hash, memo) result = { "invoice": invoice } return result @command('wnl') async def settle_hold_invoice(self, preimage: str, wallet: Abstract_Wallet = None) -> dict: """ Settles lightning hold invoice with the given preimage. Doesn't block until actual settlement of the HTLCs. arg:str:preimage:Hex encoded preimage of the invoice to be settled """ assert len(preimage) == 64, f"Invalid payment_hash length: {len(preimage)} != 64" payment_hash: str = crypto.sha256(bfh(preimage)).hex() assert payment_hash not in wallet.lnworker._preimages, f"Invoice {payment_hash=} already settled" info = wallet.lnworker.get_payment_info(bfh(payment_hash), direction=RECEIVED) assert info, f"Couldn't find lightning invoice for {payment_hash=}" assert payment_hash in wallet.lnworker.dont_expire_htlcs, f"Invoice {payment_hash=} not a hold invoice?" assert wallet.lnworker.is_complete_mpp(bfh(payment_hash)), \ f"MPP incomplete, cannot settle hold invoice {payment_hash} yet" assert (wallet.lnworker.get_payment_mpp_amount_msat(bfh(payment_hash)) or 0) >= (info.amount_msat or 0) wallet.lnworker.save_preimage(bfh(payment_hash), bfh(preimage)) util.trigger_callback('wallet_updated', wallet) result = { "settled": payment_hash } return result @command('wnl') async def cancel_hold_invoice(self, payment_hash: str, wallet: Abstract_Wallet = None) -> dict: """ Cancels lightning hold invoice 'payment_hash'. arg:str:payment_hash:Payment hash in hex of the hold invoice """ assert wallet.lnworker.get_payment_info(bfh(payment_hash), direction=RECEIVED), \ f"Couldn't find lightning invoice for payment hash {payment_hash}" assert payment_hash not in wallet.lnworker._preimages, "Cannot cancel anymore, preimage already given." assert payment_hash in wallet.lnworker.dont_expire_htlcs, f"{payment_hash=} not a hold invoice?" # set to PR_UNPAID so it can get deleted wallet.lnworker.set_payment_status(bfh(payment_hash), PR_UNPAID, direction=RECEIVED) wallet.lnworker.delete_payment_info(payment_hash, direction=RECEIVED) wallet.set_label(payment_hash, None) del wallet.lnworker.dont_expire_htlcs[payment_hash] while wallet.lnworker.is_complete_mpp(bfh(payment_hash)): # block until the htlcs got failed await asyncio.sleep(0.1) result = { "cancelled": payment_hash } return result @command('wnl') async def check_hold_invoice(self, payment_hash: str, wallet: Abstract_Wallet = None) -> dict: """ Checks the status of a lightning hold invoice 'payment_hash'. Returns: { "status": unpaid | paid | settled | unknown (cancelled or not found), "received_amount_sat": currently received amount (pending htlcs or final after settling), "invoice_amount_sat": Invoice amount, Optional (only if invoice is found), "closest_htlc_expiry_height": Closest absolute expiry height of all received htlcs (Note: HTLCs will get failed automatically if block_height + 144 > htlc_expiry_height) } arg:str:payment_hash:Payment hash in hex of the hold invoice """ assert len(payment_hash) == 64, f"Invalid payment_hash length: {len(payment_hash)} != 64" info: Optional['PaymentInfo'] = wallet.lnworker.get_payment_info(bfh(payment_hash), direction=RECEIVED) is_complete_mpp: bool = wallet.lnworker.is_complete_mpp(bfh(payment_hash)) amount_sat = (wallet.lnworker.get_payment_mpp_amount_msat(bfh(payment_hash)) or 0) // 1000 result = { "status": "unknown", "received_amount_sat": amount_sat, } if info is None: pass elif not is_complete_mpp and not wallet.lnworker.get_preimage_hex(payment_hash): # is_complete_mpp is False for settled payments result["status"] = "unpaid" elif is_complete_mpp and payment_hash in wallet.lnworker.dont_expire_htlcs: result["status"] = "paid" payment_key: str = wallet.lnworker._get_payment_key(bfh(payment_hash)).hex() htlc_status = wallet.lnworker.received_mpp_htlcs[payment_key] result["closest_htlc_expiry_height"] = min( mpp_htlc.htlc.cltv_abs for mpp_htlc in htlc_status.htlcs ) elif wallet.lnworker.get_preimage_hex(payment_hash) is not None: result["status"] = "settled" plist = wallet.lnworker.get_payments(status='settled')[bfh(payment_hash)] _dir, amount_msat, _fee, _ts = wallet.lnworker.get_payment_value(None, plist) result["received_amount_sat"] = amount_msat // 1000 result['preimage'] = wallet.lnworker.get_preimage_hex(payment_hash) if info is not None: result["invoice_amount_sat"] = (info.amount_msat or 0) // 1000 return result @command('wl') async def export_lightning_preimage(self, payment_hash: str, wallet: 'Abstract_Wallet' = None) -> Optional[str]: """ Returns the stored preimage of the given payment_hash if it is known. arg:str:payment_hash: Hash of the preimage """ preimage = wallet.lnworker.get_preimage_hex(payment_hash) assert preimage is None or crypto.sha256(bytes.fromhex(preimage)).hex() == payment_hash return preimage @command('w') async def addtransaction(self, tx, wallet: Abstract_Wallet = None): """ Add a transaction to the wallet history, without broadcasting it. arg:tx:tx:Transaction, in hexadecimal format. """ tx = Transaction(tx) if not wallet.adb.add_transaction(tx): return False wallet.save_db() return tx.txid() @command('w') async def delete_request(self, request_id, wallet: Abstract_Wallet = None): """Remove an incoming payment request arg:str:request_id:The request ID, as returned in list_invoices """ return wallet.delete_request(request_id) @command('w') async def delete_invoice(self, invoice_id, wallet: Abstract_Wallet = None): """Remove an outgoing payment invoice arg:str:invoice_id:The invoice ID, as returned in list_invoices """ return wallet.delete_invoice(invoice_id) @command('w') async def clear_requests(self, wallet: Abstract_Wallet = None): """Remove all payment requests""" wallet.clear_requests() return True @command('w') async def clear_invoices(self, wallet: Abstract_Wallet = None): """Remove all invoices""" wallet.clear_invoices() return True @command('n') async def notify(self, address: str, URL: Optional[str]): """ Watch an address. Every time the address changes, a http POST is sent to the URL. Call with an empty URL to stop watching an address. arg:str:address:Bitcoin address arg:str:URL:The callback URL """ if not hasattr(self, "_notifier"): self._notifier = Notifier(self.network) if URL: await self._notifier.start_watching_addr(address, URL) else: await self._notifier.stop_watching_addr(address) return True @command('wn') async def is_synchronized(self, wallet: Abstract_Wallet = None): """ return wallet synchronization status """ return wallet.is_up_to_date() @command('wn') async def wait_for_sync(self, wallet: Abstract_Wallet = None): """Block until the wallet synchronization finishes.""" while True: if wallet.is_up_to_date(): return True await wallet.up_to_date_changed_event.wait() @command('n') async def getfeerate(self): """ Return current fee estimate given network conditions (in sat/kvByte). To change the fee policy, use 'getconfig/setconfig fee_policy' """ fee_policy = FeePolicy(self.config.FEE_POLICY) description = fee_policy.get_target_text() feerate = fee_policy.fee_per_kb(self.network) tooltip = fee_policy.get_estimate_text(self.network) return { 'policy': fee_policy.get_descriptor(), 'description': description, 'sat/kvB': feerate, 'tooltip': tooltip, } @command('n') async def test_inject_fee_etas(self, fee_est): """ Inject fee estimates into the network object, as if they were coming from connected servers. `setconfig 'test_disable_automatic_fee_eta_update' true` to prevent Network from overriding the configured fees. Useful on regtest. arg:str:fee_est:dict of ETA-based fee estimates, encoded as str """ if not isinstance(fee_est, dict): fee_est = ast.literal_eval(fee_est) assert isinstance(fee_est, dict), f"unexpected type for fee_est. got {repr(fee_est)}" # populate missing high-block-number estimates using default relay fee. # e.g. {"25": 2222} -> {"25": 2222, "144": 1000, "1008": 1000} furthest_estimate = max(fee_est.keys()) if fee_est else 0 further_fee_est = { eta_target: FEERATE_DEFAULT_RELAY for eta_target in FEE_ETA_TARGETS if eta_target > furthest_estimate } fee_est.update(further_fee_est) self.network.update_fee_estimates(fee_est=fee_est) @command('w') async def removelocaltx(self, txid, wallet: Abstract_Wallet = None): """Remove a 'local' transaction from the wallet, and its dependent transactions. arg:txid:txid:Transaction ID """ height = wallet.adb.get_tx_height(txid).height() if height != TX_HEIGHT_LOCAL: raise UserFacingException( f'Only local transactions can be removed. ' f'This tx has height: {height} != {TX_HEIGHT_LOCAL}') wallet.adb.remove_transaction(txid) wallet.save_db() @command('wn') async def get_tx_status(self, txid, wallet: Abstract_Wallet = None): """Returns some information regarding the tx. For now, only confirmations. The transaction must be related to the wallet. arg:txid:txid:Transaction ID """ if not wallet.db.get_transaction(txid): raise UserFacingException("Transaction not in wallet.") return { "confirmations": wallet.adb.get_tx_height(txid).conf, } @command('') async def help(self): """Show help about a command""" # for the python console return sorted(known_commands.keys()) # lightning network commands @command('wnl') async def add_peer(self, connection_string, timeout=20, gossip=False, wallet: Abstract_Wallet = None): """ Connect to a lightning node arg:str:connection_string:Lightning network node ID or network address arg:bool:gossip:Apply command to your gossip node instead of wallet node arg:int:timeout:Timeout in seconds (default=20) """ lnworker = self.network.lngossip if gossip else wallet.lnworker peer = await lnworker.lnpeermgr.add_peer(connection_string) try: await util.wait_for2(peer.initialized, timeout=LN_P2P_NETWORK_TIMEOUT) except (CancelledError, Exception) as e: # FIXME often simply CancelledError and real cause (e.g. timeout) remains hidden raise UserFacingException(f"Connection failed: {repr(e)}") return True @command('wnl') async def gossip_info(self, wallet: Abstract_Wallet = None): """Display statistics about lightninig gossip""" lngossip = self.network.lngossip channel_db = lngossip.channel_db forwarded = dict([(key.hex(), p._num_gossip_messages_forwarded) for key, p in wallet.lnworker.lnpeermgr.peers.items()]), out = { 'received': { 'channel_announcements': lngossip._num_chan_ann, 'channel_updates': lngossip._num_chan_upd, 'channel_updates_good': lngossip._num_chan_upd_good, 'node_announcements': lngossip._num_node_ann, }, 'database': { 'nodes': channel_db.num_nodes, 'channels': channel_db.num_channels, 'channel_policies': channel_db.num_policies, }, 'forwarded': forwarded, } return out @command('wnl') async def list_peers(self, gossip=False, wallet: Abstract_Wallet = None): """ List lightning peers of your node arg:bool:gossip:Apply command to your gossip node instead of wallet node """ lnworker = self.network.lngossip if gossip else wallet.lnworker return [{ 'node_id': p.pubkey.hex(), 'address': p.transport.name(), 'initialized': p.is_initialized(), 'features': str(LnFeatures(p.features)), 'channels': [c.funding_outpoint.to_str() for c in p.channels.values()], } for p in lnworker.lnpeermgr.peers.values()] @command('wpnl') async def open_channel(self, connection_string, amount, push_amount=0, public=False, zeroconf=False, password=None, wallet: Abstract_Wallet = None): """ Open a lightning channel with a peer arg:str:connection_string:Lightning network node ID or network address arg:decimal_or_max:amount:funding amount (in BTC) arg:decimal:push_amount:Push initial amount (in BTC) arg:bool:public:The channel will be announced arg:bool:zeroconf:request zeroconf channel """ if not wallet.can_have_lightning(): raise UserFacingException("This wallet cannot create new channels") funding_sat = satoshis(amount) push_sat = satoshis(push_amount) peer = await wallet.lnworker.lnpeermgr.add_peer(connection_string) chan, funding_tx = await wallet.lnworker.open_channel_with_peer( peer, funding_sat, push_sat=push_sat, public=public, zeroconf=zeroconf, password=password) return chan.funding_outpoint.to_str() @command('') async def decode_invoice(self, invoice: str): """ Decode a lightning invoice arg:str:invoice:Lightning invoice (bolt 11) """ invoice = Invoice.from_bech32(invoice) return invoice.to_debug_json() @command('wnpl') async def lnpay( self, invoice: str, timeout: int = 120, max_cltv: Optional[int] = None, max_fee_msat: Optional[int] = None, password=None, wallet: Abstract_Wallet = None ): """ Pay a lightning invoice Note: it is *not* safe to try paying the same invoice multiple times with a timeout. It is only safe to retry paying the same invoice if there are no more pending HTLCs with the same payment_hash. # FIXME should there even be a default timeout? just block forever. arg:str:invoice:Lightning invoice (bolt 11) arg:int:timeout:Timeout in seconds (default=120) arg:int:max_cltv:Maximum total time lock for the route (default=4032+invoice_final_cltv_delta) arg:int:max_fee_msat:Maximum absolute fee budget for the payment (if unset, the default is a percentage fee based on config.LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS) """ # note: The "timeout" param works via black magic. # The CLI-parser stores it in the config, and the argname matches config.cv.CLI_TIMEOUT.key(). # - it works when calling the CLI and there is also a daemon (online command) # - FIXME it does NOT work when calling an offline command (-o) # - FIXME it does NOT work when calling RPC directly (e.g. curl) lnworker = wallet.lnworker lnaddr = lnworker._check_bolt11_invoice(invoice) # also checks if amount is given payment_hash = lnaddr.paymenthash invoice_obj = Invoice.from_bech32(invoice) assert not max_fee_msat or max_fee_msat < max(invoice_obj.amount_msat // 2, 1_000_000), \ f"{max_fee_msat=} > max(invoice amount msat / 2, 1_000_000)" wallet.save_invoice(invoice_obj) if max_cltv is not None: # The cltv budget excludes the final cltv delta which is why it is deducted here # so the whole used cltv is <= max_cltv assert max_cltv <= NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE, \ f"{max_cltv=} > {NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE=}" max_cltv_remaining = max_cltv - lnaddr.get_min_final_cltv_delta() assert max_cltv_remaining > 0, f"{max_cltv=} - {lnaddr.get_min_final_cltv_delta()=} < 1" max_cltv = max_cltv_remaining budget = PaymentFeeBudget.from_invoice_amount( config=wallet.config, invoice_amount_msat=invoice_obj.amount_msat, max_cltv_delta=max_cltv, max_fee_msat=max_fee_msat, ) success, log = await lnworker.pay_invoice(invoice_obj, budget=budget) return { 'payment_hash': payment_hash.hex(), 'success': success, 'preimage': lnworker.get_preimage(payment_hash).hex() if success else None, 'log': [x.formatted_tuple() for x in log] } @command('wl') async def nodeid(self, wallet: Abstract_Wallet = None): """Return the Lightning Node ID of a wallet""" listen_addr = self.config.LIGHTNING_LISTEN return wallet.lnworker.node_keypair.pubkey.hex() + (('@' + listen_addr) if listen_addr else '') @command('wl') async def list_channels(self, public: bool = False, private: bool = False, active: bool = False, open: bool = False, wallet: Abstract_Wallet = None): """Return the list of channels in the wallet arg:bool:public:list only public channels arg:bool:private:list only private channels arg:bool:open:list only open channels arg:bool:active:list only active channels """ from .lnutil import LOCAL, REMOTE, format_short_channel_id if public and private: raise Exception("incompatible options") def _filter(chan): if public and not chan.is_public(): return False if private and chan.is_public(): return False if active and not chan.is_redeemed(): return False if open and not chan.is_open(): return False return True return [ { 'short_channel_id': format_short_channel_id(chan.short_channel_id) if chan.short_channel_id else None, 'channel_id': chan.channel_id.hex(), 'channel_point': chan.funding_outpoint.to_str(), 'closing_txid': chan.get_closing_height()[0] if chan.get_closing_height() else None, 'state': chan.get_state().name, 'peer_state': chan.peer_state.name, 'remote_pubkey': chan.node_id.hex(), 'local_balance': chan.balance(LOCAL)//1000, 'remote_balance': chan.balance(REMOTE)//1000, 'local_ctn': chan.get_latest_ctn(LOCAL), 'remote_ctn': chan.get_latest_ctn(REMOTE), 'local_reserve': chan.config[REMOTE].reserve_sat, # their config has our reserve 'remote_reserve': chan.config[LOCAL].reserve_sat, 'local_unsettled_sent': chan.balance_tied_up_in_htlcs_by_direction(LOCAL, direction=SENT) // 1000, 'remote_unsettled_sent': chan.balance_tied_up_in_htlcs_by_direction(REMOTE, direction=SENT) // 1000, } for chan in wallet.lnworker.channels.values() if _filter(chan) ] @command('wl') async def list_channel_backups(self, wallet: Abstract_Wallet = None): """Return the list of channel backups in the wallet""" # FIXME: we need to be online to display capacity of backups from .lnutil import LOCAL, REMOTE, format_short_channel_id return [ { 'short_channel_id': format_short_channel_id(chan.short_channel_id) if chan.short_channel_id else None, 'channel_id': chan.channel_id.hex(), 'channel_point': chan.funding_outpoint.to_str(), 'closing_txid': chan.get_closing_height()[0] if chan.get_closing_height() else None, 'state': chan.get_state().name, } for chan in wallet.lnworker.channel_backups.values() ] @command('wnl') async def enable_htlc_settle(self, b: bool, wallet: Abstract_Wallet = None): """ command used in regtests arg:bool:b:boolean """ wallet.lnworker.enable_htlc_settle = b @command('n') async def clear_ln_blacklist(self): if self.network.path_finder: self.network.path_finder.clear_blacklist() @command('n') async def reset_liquidity_hints(self): if self.network.path_finder: self.network.path_finder.liquidity_hints.reset_liquidity_hints() self.network.path_finder.clear_blacklist() @command('wnpl') async def close_channel(self, channel_point, force=False, password=None, wallet: Abstract_Wallet = None): """ Close a lightning channel. Returns txid of closing tx. arg:str:channel_point:channel point arg:bool:force:Force closes (broadcast local commitment transaction) """ txid, index = channel_point.split(':') chan_id, _ = channel_id_from_funding_tx(txid, int(index)) if chan_id not in wallet.lnworker.channels: raise UserFacingException(f'Unknown channel {channel_point}') coro = wallet.lnworker.force_close_channel(chan_id) if force else wallet.lnworker.close_channel(chan_id) return await coro @command('wnpl') async def request_force_close(self, channel_point, connection_string=None, password=None, wallet: Abstract_Wallet = None): """ Requests the remote to force close a channel. If a connection string is passed, can be used without having state or any backup for the channel. Assumes that channel was originally opened with the same local peer (node_keypair). arg:str:connection_string:Lightning network node ID or network address arg:str:channel_point:channel point """ txid, index = channel_point.split(':') chan_id, _ = channel_id_from_funding_tx(txid, int(index)) if chan_id not in wallet.lnworker.channels and chan_id not in wallet.lnworker.channel_backups: raise UserFacingException(f'Unknown channel {channel_point}') await wallet.lnworker.request_force_close(chan_id, connect_str=connection_string) @command('wpl') async def export_channel_backup(self, channel_point, password=None, wallet: Abstract_Wallet = None): """ Returns an encrypted channel backup arg:str:channel_point:Channel outpoint """ txid, index = channel_point.split(':') chan_id, _ = channel_id_from_funding_tx(txid, int(index)) if chan_id not in wallet.lnworker.channels: raise UserFacingException(f'Unknown channel {channel_point}') return wallet.lnworker.export_channel_backup(chan_id) @command('wl') async def import_channel_backup(self, encrypted, wallet: Abstract_Wallet = None): """ arg:str:encrypted:Encrypted channel backup """ return wallet.lnworker.import_channel_backup(encrypted) @command('wnpl') async def get_channel_ctx(self, channel_point, password=None, iknowwhatimdoing=False, wallet: Abstract_Wallet = None): """ return the current commitment transaction of a channel arg:str:channel_point:Channel outpoint arg:bool:iknowwhatimdoing:Acknowledge that I understand the full implications of what I am about to do """ if not iknowwhatimdoing: raise UserFacingException( "WARNING: this command is potentially unsafe.\n" "To proceed, try again, with the --iknowwhatimdoing option.") txid, index = channel_point.split(':') chan_id, _ = channel_id_from_funding_tx(txid, int(index)) if chan_id not in wallet.lnworker.channels: raise UserFacingException(f'Unknown channel {channel_point}') chan = wallet.lnworker.channels[chan_id] tx = chan.force_close_tx() return tx.serialize() @command('wnl') async def get_watchtower_ctn(self, channel_point, wallet: Abstract_Wallet = None): """ Return the local watchtower's ctn of channel. used in regtests arg:str:channel_point:Channel outpoint (txid:index) """ return wallet.lnworker.get_watchtower_ctn(channel_point) @command('wnpl') async def rebalance_channels(self, from_scid, dest_scid, amount, password=None, wallet: Abstract_Wallet = None): """ Rebalance channels. If trampoline is used, channels must be with different trampolines. arg:str:from_scid:Short channel ID arg:str:dest_scid:Short channel ID arg:decimal:amount:Amount (in BTC) """ from .lnutil import ShortChannelID from_scid = ShortChannelID.from_str(from_scid) dest_scid = ShortChannelID.from_str(dest_scid) from_channel = wallet.lnworker.get_channel_by_short_id(from_scid) dest_channel = wallet.lnworker.get_channel_by_short_id(dest_scid) amount_sat = satoshis(amount) success, log = await wallet.lnworker.rebalance_channels( from_channel, dest_channel, amount_msat=amount_sat * 1000, ) return { 'success': success, 'log': [x.formatted_tuple() for x in log] } @command('wnl') async def get_submarine_swap_providers(self, query_time=15, wallet: Abstract_Wallet = None): """ Queries nostr relays for available submarine swap providers. To configure one of the providers use: setconfig swapserver_npub 'npub...' arg:int:query_time:Optional timeout how long the relays should be queried for provider announcements. Default: 15 sec """ sm = wallet.lnworker.swap_manager async with sm.create_transport() as transport: assert isinstance(transport, NostrTransport) await asyncio.sleep(query_time) offers = transport.get_recent_offers() result = {} for offer in offers: result[offer.server_npub] = { "percentage_fee": float(offer.pairs.percentage), "max_forward_sat": offer.pairs.max_forward, "max_reverse_sat": offer.pairs.max_reverse, "min_amount_sat": offer.pairs.min_amount, "prepayment": 2 * offer.pairs.mining_fee, } return result @command('wnpl') async def normal_swap(self, onchain_amount, lightning_amount, password=None, wallet: Abstract_Wallet = None): """ Normal submarine swap: send on-chain BTC, receive on Lightning arg:decimal_or_dryrun:lightning_amount:Amount to be received, in BTC. Set it to 'dryrun' to receive a value arg:decimal_or_dryrun:onchain_amount:Amount to be sent, in BTC. Set it to 'dryrun' to receive a value """ sm = wallet.lnworker.swap_manager assert self.config.SWAPSERVER_NPUB or self.config.SWAPSERVER_URL, \ "Configure swap provider first. See 'get_submarine_swap_providers'." async with sm.create_transport() as transport: try: await asyncio.wait_for(sm.is_initialized.wait(), timeout=15) except asyncio.TimeoutError: raise TimeoutError("Could not find configured swap provider. Setup another one. See 'get_submarine_swap_providers'") if lightning_amount == 'dryrun': onchain_amount_sat = satoshis(onchain_amount) lightning_amount_sat = sm.get_recv_amount(onchain_amount_sat, is_reverse=False) txid = None elif onchain_amount == 'dryrun': lightning_amount_sat = satoshis(lightning_amount) onchain_amount_sat = sm.get_send_amount(lightning_amount_sat, is_reverse=False) txid = None else: lightning_amount_sat = satoshis(lightning_amount) onchain_amount_sat = satoshis(onchain_amount) txid = await wallet.lnworker.swap_manager.normal_swap( transport=transport, lightning_amount_sat=lightning_amount_sat, expected_onchain_amount_sat=onchain_amount_sat, password=password, ) return { 'txid': txid, 'lightning_amount': format_satoshis(lightning_amount_sat), 'onchain_amount': format_satoshis(onchain_amount_sat), } @command('wnpl') async def reverse_swap( self, lightning_amount, onchain_amount, prepayment='dryrun', password=None, wallet: Abstract_Wallet = None, ): """ Reverse submarine swap: send on Lightning, receive on-chain arg:decimal_or_dryrun:lightning_amount:Amount to be sent, in BTC. Set it to 'dryrun' to receive a value arg:decimal_or_dryrun:onchain_amount:Amount to be received, in BTC. Set it to 'dryrun' to receive a value arg:decimal_or_dryrun:prepayment:Lightning payment required by the swap provider in order to cover their mining fees. This is included in lightning_amount. However, this part of the operation is not trustless; the provider is trusted to fail this payment if the swap fails. """ sm = wallet.lnworker.swap_manager assert self.config.SWAPSERVER_NPUB or self.config.SWAPSERVER_URL, \ "Configure swap provider first. See 'get_submarine_swap_providers'." async with sm.create_transport() as transport: try: await asyncio.wait_for(sm.is_initialized.wait(), timeout=15) except asyncio.TimeoutError: raise TimeoutError("Could not find configured swap provider. Setup another one. See 'get_submarine_swap_providers'") if onchain_amount == 'dryrun': lightning_amount_sat = satoshis(lightning_amount) onchain_amount_sat = sm.get_recv_amount(lightning_amount_sat, is_reverse=True) assert prepayment == "dryrun", f"Cannot use {prepayment=} in dryrun. Set it to 'dryrun'." prepayment_sat = 2 * sm.mining_fee funding_txid = None elif lightning_amount == 'dryrun': onchain_amount_sat = satoshis(onchain_amount) lightning_amount_sat = sm.get_send_amount(onchain_amount_sat, is_reverse=True) assert prepayment == "dryrun", f"Cannot use {prepayment=} in dryrun. Set it to 'dryrun'." prepayment_sat = 2 * sm.mining_fee funding_txid = None else: lightning_amount_sat = satoshis(lightning_amount) claim_fee = sm.get_fee_for_txbatcher() onchain_amount_sat = satoshis(onchain_amount) + claim_fee assert prepayment != "dryrun", "Provide the 'prepayment' obtained from the dryrun." prepayment_sat = satoshis(prepayment) funding_txid = await wallet.lnworker.swap_manager.reverse_swap( transport=transport, lightning_amount_sat=lightning_amount_sat, expected_onchain_amount_sat=onchain_amount_sat, prepayment_sat=prepayment_sat, ) return { 'funding_txid': funding_txid, 'lightning_amount': format_satoshis(lightning_amount_sat), 'onchain_amount': format_satoshis(onchain_amount_sat), 'prepayment': format_satoshis(prepayment_sat) } @command('n') async def convert_currency(self, from_amount=1, from_ccy='', to_ccy=''): """ Converts the given amount of currency to another using the configured exchange rate source. arg:decimal:from_amount:Amount to convert (default=1) arg:str:from_ccy:Currency to convert from arg:str:to_ccy:Currency to convert to """ if not self.daemon.fx.is_enabled(): raise UserFacingException("FX is disabled. To enable, run: 'electrum setconfig use_exchange_rate true'") # Currency codes are uppercase from_ccy = from_ccy.upper() to_ccy = to_ccy.upper() # Default currencies if from_ccy == '': from_ccy = "BTC" if to_ccy != "BTC" else self.daemon.fx.ccy if to_ccy == '': to_ccy = "BTC" if from_ccy != "BTC" else self.daemon.fx.ccy # Get current rates rate_from = self.daemon.fx.exchange.get_cached_spot_quote(from_ccy) rate_to = self.daemon.fx.exchange.get_cached_spot_quote(to_ccy) # Test if currencies exist if rate_from.is_nan(): raise UserFacingException(f'Currency to convert from ({from_ccy}) is unknown or rate is unavailable') if rate_to.is_nan(): raise UserFacingException(f'Currency to convert to ({to_ccy}) is unknown or rate is unavailable') # Conversion try: from_amount = to_decimal(from_amount) to_amount = from_amount / rate_from * rate_to except InvalidOperation: raise Exception("from_amount is not a number") return { "from_amount": self.daemon.fx.ccy_amount_str(from_amount, add_thousands_sep=False, ccy=from_ccy), "to_amount": self.daemon.fx.ccy_amount_str(to_amount, add_thousands_sep=False, ccy=to_ccy), "from_ccy": from_ccy, "to_ccy": to_ccy, "source": self.daemon.fx.exchange.name(), } @command('wnl') async def send_onion_message(self, node_id_or_blinded_path_hex: str, message: str, wallet: Abstract_Wallet = None): """ Send an onion message with onionmsg_tlv.message payload to node_id. arg:str:node_id_or_blinded_path_hex:node id or blinded path arg:str:message:Message to send """ assert wallet assert wallet.lnworker assert node_id_or_blinded_path_hex assert message node_id_or_blinded_path = bfh(node_id_or_blinded_path_hex) assert len(node_id_or_blinded_path) >= 33 destination_payload = { 'message': {'text': message.encode('utf-8')} } try: send_onion_message_to(wallet.lnworker, node_id_or_blinded_path, destination_payload) return {'success': True} except Exception as e: msg = str(e) return { 'success': False, 'msg': msg } @command('wnl') async def get_blinded_path_via(self, node_id: str, dummy_hops: int = 0, wallet: Abstract_Wallet = None): """ Create a blinded path with node_id as introduction point. Introduction point must be direct peer of me. arg:str:node_id:Node pubkey in hex format arg:int:dummy_hops:Number of dummy hops to add """ # TODO: allow introduction_point to not be a direct peer and construct a route assert wallet assert node_id pubkey = bfh(node_id) assert len(pubkey) == 33, 'invalid node_id' peer = wallet.lnworker.lnpeermgr.peers[pubkey] assert peer, 'node_id not a peer' path = [pubkey, wallet.lnworker.node_keypair.pubkey] session_key = os.urandom(32) blinded_path = create_blinded_path(session_key, path=path, final_recipient_data={}, dummy_hops=dummy_hops) with io.BytesIO() as blinded_path_fd: OnionWireSerializer.write_field( fd=blinded_path_fd, field_type='blinded_path', count=1, value=blinded_path) encoded_blinded_path = blinded_path_fd.getvalue() return encoded_blinded_path.hex() def plugin_command(s, plugin_name): """Decorator to register a cli command inside a plugin. To be used within a commands.py file in the plugins root.""" # atm all plugin commands require a daemon, cannot be run in 'offline' mode: if 'n' not in s: s += 'n' def decorator(func): assert len(plugin_name) > 0, "Plugin name must not be empty" func.plugin_name = plugin_name name = plugin_name + '_' + func.__name__ if name in known_commands or hasattr(Commands, name): raise Exception(f"Command name {name} already exists. Plugin commands should not overwrite other commands.") assert inspect.iscoroutinefunction(func), f"Plugin commands must be a coroutine: {name}" @command(s) @wraps(func) async def func_wrapper(*args, **kwargs): cmd_runner = args[0] # type: Commands daemon = cmd_runner.daemon assert daemon is not None kwargs['plugin'] = daemon._plugins.get_plugin(plugin_name) return await func(*args, **kwargs) setattr(Commands, name, func_wrapper) return func_wrapper return decorator def eval_bool(x: str) -> bool: if x == 'false': return False if x == 'true': return True # assume python, raise if malformed return bool(ast.literal_eval(x)) # don't use floats because of rounding errors json_loads = lambda x: json.loads(x, parse_float=lambda x: str(to_decimal(x))) def check_txid(txid): if not is_hash256_str(txid): raise UserFacingException(f"{repr(txid)} is not a txid") return txid arg_types = { 'int': int, 'bool': eval_bool, 'str': str, 'txid': check_txid, 'tx': convert_raw_tx_to_hex, 'json': json_loads, 'decimal': lambda x: str(to_decimal(x)), 'decimal_or_dryrun': lambda x: str(to_decimal(x)) if x != 'dryrun' else x, 'decimal_or_max': lambda x: str(to_decimal(x)) if not parse_max_spend(x) else x, } config_variables = { 'addrequest': { 'ssl_privkey': 'Path to your SSL private key, needed to sign the request.', 'ssl_chain': 'Chain of SSL certificates, needed for signed requests. Put your certificate at the top and the root CA at the end', 'url_rewrite': 'Parameters passed to str.replace(), in order to create the r= part of bitcoin: URIs. Example: \"(\'file:///var/www/\',\'https://electrum.org/\')\"', }, 'listrequests': { 'url_rewrite': 'Parameters passed to str.replace(), in order to create the r= part of bitcoin: URIs. Example: \"(\'file:///var/www/\',\'https://electrum.org/\')\"', } } def set_default_subparser(self, name, args=None): """see http://stackoverflow.com/questions/5176691/argparse-how-to-specify-a-default-subcommand""" subparser_found = False for arg in sys.argv[1:]: if arg in ['-h', '--help', '--version']: # global help/version if no subparser break else: for x in self._subparsers._actions: if not isinstance(x, argparse._SubParsersAction): continue for sp_name in x._name_parser_map.keys(): if sp_name in sys.argv[1:]: subparser_found = True if not subparser_found: # insert default in first position, this implies no # global options without a sub_parsers specified if args is None: sys.argv.insert(1, name) else: args.insert(0, name) argparse.ArgumentParser.set_default_subparser = set_default_subparser # workaround https://bugs.python.org/issue23058 # see https://github.com/nickstenning/honcho/pull/121 def subparser_call(self, parser, namespace, values, option_string=None): from argparse import ArgumentError, SUPPRESS, _UNRECOGNIZED_ARGS_ATTR parser_name = values[0] arg_strings = values[1:] # set the parser name if requested if self.dest is not SUPPRESS: setattr(namespace, self.dest, parser_name) # select the parser try: parser = self._name_parser_map[parser_name] except KeyError: tup = parser_name, ', '.join(self._name_parser_map) msg = _('unknown parser {!r} (choices: {})').format(*tup) raise ArgumentError(self, msg) # parse all the remaining options into the namespace # store any unrecognized options on the object, so that the top # level parser can decide what to do with them namespace, arg_strings = parser.parse_known_args(arg_strings, namespace) if arg_strings: vars(namespace).setdefault(_UNRECOGNIZED_ARGS_ATTR, []) getattr(namespace, _UNRECOGNIZED_ARGS_ATTR).extend(arg_strings) argparse._SubParsersAction.__call__ = subparser_call def add_network_options(parser): group = parser.add_argument_group('network options') group.add_argument( "-f", "--serverfingerprint", dest=SimpleConfig.NETWORK_SERVERFINGERPRINT.key(), default=None, help="only allow connecting to servers with a matching SSL certificate SHA256 fingerprint. " + "To calculate this yourself: '$ openssl x509 -noout -fingerprint -sha256 -inform pem -in mycertfile.crt'. Enter as 64 hex chars.") group.add_argument( "-1", "--oneserver", action="store_true", dest=SimpleConfig.NETWORK_ONESERVER.key(), default=None, help="connect to one server only") group.add_argument( "-s", "--server", dest=SimpleConfig.NETWORK_SERVER.key(), default=None, help="set server host:port:protocol, where protocol is either t (tcp) or s (ssl)") group.add_argument( "-p", "--proxy", dest=SimpleConfig.NETWORK_PROXY.key(), default=None, help="set proxy [type:]host:port (or 'none' to disable proxy), where type is socks4 or socks5") group.add_argument( "--proxyuser", dest=SimpleConfig.NETWORK_PROXY_USER.key(), default=None, help="set proxy username") group.add_argument( "--proxypassword", dest=SimpleConfig.NETWORK_PROXY_PASSWORD.key(), default=None, help="set proxy password") group.add_argument( "--noonion", action="store_true", dest=SimpleConfig.NETWORK_NOONION.key(), default=None, help="do not try to connect to onion servers") group.add_argument( "--skipmerklecheck", action="store_true", dest=SimpleConfig.NETWORK_SKIPMERKLECHECK.key(), default=None, help="Tolerate invalid merkle proofs from Electrum server") def add_global_options(parser, suppress=False): group = parser.add_argument_group('global options') group.add_argument( "-v", dest="verbosity", default='', help=argparse.SUPPRESS if suppress else "Set verbosity (log levels)") group.add_argument( "-D", "--dir", dest="electrum_path", help=argparse.SUPPRESS if suppress else "electrum directory") group.add_argument( "-w", "--wallet", dest="wallet_path", help=argparse.SUPPRESS if suppress else "wallet path") group.add_argument( "-P", "--portable", action="store_true", dest="portable", default=False, help=argparse.SUPPRESS if suppress else "Use local 'electrum_data' directory") for chain in constants.NETS_LIST: group.add_argument( f"--{chain.cli_flag()}", action="store_true", dest=chain.config_key(), default=False, help=argparse.SUPPRESS if suppress else f"Use {chain.NET_NAME} chain") group.add_argument( "-o", "--offline", action="store_true", dest=SimpleConfig.NETWORK_OFFLINE.key(), default=None, help=argparse.SUPPRESS if suppress else "Run offline") group.add_argument( "--rpcuser", dest=SimpleConfig.RPC_USERNAME.key(), default=argparse.SUPPRESS, help=argparse.SUPPRESS if suppress else "RPC user") group.add_argument( "--rpcpassword", dest=SimpleConfig.RPC_PASSWORD.key(), default=argparse.SUPPRESS, help=argparse.SUPPRESS if suppress else "RPC password") group.add_argument( "--forgetconfig", action="store_true", dest=SimpleConfig.CONFIG_FORGET_CHANGES.key(), default=None, help=argparse.SUPPRESS if suppress else "Forget config on exit") def get_simple_parser(): """ simple parser that figures out the path of the config file and ignore unknown args """ from optparse import OptionParser, BadOptionError, AmbiguousOptionError class PassThroughOptionParser(OptionParser): # see https://stackoverflow.com/questions/1885161/how-can-i-get-optparses-optionparser-to-ignore-invalid-options def _process_args(self, largs, rargs, values): while rargs: try: OptionParser._process_args(self, largs, rargs, values) except (BadOptionError, AmbiguousOptionError) as e: largs.append(e.opt_str) parser = PassThroughOptionParser() parser.add_option("-D", "--dir", dest="electrum_path", help="electrum directory") parser.add_option("-P", "--portable", action="store_true", dest="portable", default=False, help="Use local 'electrum_data' directory") for chain in constants.NETS_LIST: parser.add_option(f"--{chain.cli_flag()}", action="store_true", dest=chain.config_key(), default=False, help=f"Use {chain.NET_NAME} chain") return parser def get_parser(): # create main parser parser = argparse.ArgumentParser( epilog="Run 'electrum help ' to see the help for a command") parser.add_argument("--version", dest="cmd", action='store_const', const='version', help="Return the version of Electrum.") add_global_options(parser) subparsers = parser.add_subparsers(dest='cmd', metavar='') # gui parser_gui = subparsers.add_parser('gui', description="Run Electrum's Graphical User Interface.", help="Run GUI (default)") parser_gui.add_argument("url", nargs='?', default=None, help="bitcoin URI (or bip70 file)") parser_gui.add_argument("-g", "--gui", dest=SimpleConfig.GUI_NAME.key(), help="select graphical user interface", choices=['qt', 'text', 'stdio', 'qml']) parser_gui.add_argument("-m", action="store_true", dest=SimpleConfig.GUI_QT_HIDE_ON_STARTUP.key(), default=False, help="hide GUI on startup") parser_gui.add_argument("-L", "--lang", dest=SimpleConfig.LOCALIZATION_LANGUAGE.key(), default=None, help="default language used in GUI") parser_gui.add_argument("--daemon", action="store_true", dest="daemon", default=False, help="keep daemon running after GUI is closed") parser_gui.add_argument("--nosegwit", action="store_true", dest=SimpleConfig.WIZARD_DONT_CREATE_SEGWIT.key(), default=False, help="Do not create segwit wallets") add_network_options(parser_gui) add_global_options(parser_gui) # daemon parser_daemon = subparsers.add_parser('daemon', help="Run Daemon") parser_daemon.add_argument("-d", "--detached", action="store_true", dest="detach", default=False, help="run daemon in detached mode") # FIXME: all these options are rpc-server-side. The CLI client-side cannot use e.g. --rpcport, # instead it reads it from the daemon lockfile. parser_daemon.add_argument("--rpchost", dest=SimpleConfig.RPC_HOST.key(), default=argparse.SUPPRESS, help="RPC host") parser_daemon.add_argument("--rpcport", dest=SimpleConfig.RPC_PORT.key(), type=int, default=argparse.SUPPRESS, help="RPC port") parser_daemon.add_argument("--rpcsock", dest=SimpleConfig.RPC_SOCKET_TYPE.key(), default=None, help="what socket type to which to bind RPC daemon", choices=['unix', 'tcp', 'auto']) parser_daemon.add_argument("--rpcsockpath", dest=SimpleConfig.RPC_SOCKET_FILEPATH.key(), help="where to place RPC file socket") add_network_options(parser_daemon) add_global_options(parser_daemon) # commands for cmdname in sorted(known_commands.keys()): cmd = known_commands[cmdname] p = subparsers.add_parser( cmdname, description=cmd.description, help=cmd.short_description, formatter_class=argparse.RawDescriptionHelpFormatter, epilog="Run 'electrum -h' to see the list of global options", ) for optname, default in zip(cmd.options, cmd.defaults): if optname in ['wallet_path', 'wallet', 'plugin']: continue if optname == 'password': p.add_argument("--password", dest='password', help="Wallet password. Use '--password :' if you want a prompt.") continue help = cmd.arg_descriptions.get(optname) if not help: print(f'undocumented argument {cmdname}::{optname}', file=sys.stderr) action = "store_true" if default is False else 'store' if action == 'store': type_descriptor = cmd.arg_types.get(optname) _type = arg_types.get(type_descriptor, str) p.add_argument('--' + optname, dest=optname, action=action, default=default, help=help, type=_type) else: p.add_argument('--' + optname, dest=optname, action=action, default=default, help=help) add_global_options(p, suppress=True) for param in cmd.params: if param in ['wallet_path', 'wallet']: continue help = cmd.arg_descriptions.get(param) if not help: print(f'undocumented argument {cmdname}::{param}', file=sys.stderr) type_descriptor = cmd.arg_types.get(param) _type = arg_types.get(type_descriptor) if help is not None and _type is None: print(f'unknown type \'{_type}\' for {cmdname}::{param}', file=sys.stderr) p.add_argument(param, help=help, type=_type) cvh = config_variables.get(cmdname) if cvh: group = p.add_argument_group('configuration variables', '(set with setconfig/getconfig)') for k, v in cvh.items(): group.add_argument(k, nargs='?', help=v) # 'gui' is the default command # note: set_default_subparser modifies sys.argv parser.set_default_subparser('gui') return parser ================================================ FILE: electrum/constants.py ================================================ # -*- coding: utf-8 -*- # # Electrum - lightweight Bitcoin client # Copyright (C) 2018 The Electrum developers # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files # (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, # publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import os import json from typing import Sequence, Tuple, Mapping, Type, List, Optional from .lntransport import LNPeerAddr from .util import inv_dict, all_subclasses, classproperty from . import bitcoin def read_json(filename, default=None): path = os.path.join(os.path.dirname(__file__), filename) try: with open(path, 'r') as f: r = json.loads(f.read()) except Exception: if default is None: # Sometimes it's better to hard-fail: the file might be missing # due to a packaging issue, which might otherwise go unnoticed. raise r = default return r def create_fallback_node_list(fallback_nodes_dict: dict[str, dict]) -> List[LNPeerAddr]: """Take a json dict of fallback nodes like: k:node_id, v:{k:'host', k:'port'} and return LNPeerAddr list""" fallback_nodes = [] for node_id, address in fallback_nodes_dict.items(): fallback_nodes.append( LNPeerAddr(host=address['host'], port=int(address['port']), pubkey=bytes.fromhex(node_id))) return fallback_nodes GIT_REPO_URL = "https://github.com/spesmilo/electrum" GIT_REPO_ISSUES_URL = "https://github.com/spesmilo/electrum/issues" RELEASE_NOTES_URL = "https://raw.githubusercontent.com/spesmilo/electrum/refs/heads/master/RELEASE-NOTES" BIP39_WALLET_FORMATS = read_json('bip39_wallet_formats.json') class AbstractNet: NET_NAME: str TESTNET: bool WIF_PREFIX: int ADDRTYPE_P2PKH: int ADDRTYPE_P2SH: int SEGWIT_HRP: str BOLT11_HRP: str GENESIS: str BLOCK_HEIGHT_FIRST_LIGHTNING_CHANNELS: int = 0 BIP44_COIN_TYPE: int LN_REALM_BYTE: int DEFAULT_PORTS: Mapping[str, str] LN_DNS_SEEDS: Sequence[str] XPRV_HEADERS: Mapping[str, int] XPRV_HEADERS_INV: Mapping[int, str] XPUB_HEADERS: Mapping[str, int] XPUB_HEADERS_INV: Mapping[int, str] @classmethod def max_checkpoint(cls) -> int: return max(0, len(cls.CHECKPOINTS) * 2016 - 1) @classmethod def rev_genesis_bytes(cls) -> bytes: return bytes.fromhex(cls.GENESIS)[::-1] @classmethod def set_as_network(cls) -> None: global net net = cls _cached_default_servers = None @classproperty def DEFAULT_SERVERS(cls) -> Mapping[str, Mapping[str, str]]: if cls._cached_default_servers is None: default_file = {} if cls.TESTNET else None # for mainnet we hard-fail if the file is missing. cls._cached_default_servers = read_json(os.path.join('chains', cls.NET_NAME, 'servers.json'), default_file) return cls._cached_default_servers _cached_fallback_lnnodes = None @classproperty def FALLBACK_LN_NODES(cls) -> Sequence[LNPeerAddr]: if cls._cached_fallback_lnnodes is None: default_file = {} if cls.TESTNET else None # for mainnet we hard-fail if the file is missing. d = read_json(os.path.join('chains', cls.NET_NAME, 'fallback_lnnodes.json'), default_file) cls._cached_fallback_lnnodes = create_fallback_node_list(d) return cls._cached_fallback_lnnodes _cached_checkpoints = None @classproperty def CHECKPOINTS(cls) -> Sequence[Tuple[str, int]]: if cls._cached_checkpoints is None: default_file = [] if cls.TESTNET else None # for mainnet we hard-fail if the file is missing. cls._cached_checkpoints = read_json(os.path.join('chains', cls.NET_NAME, 'checkpoints.json'), default_file) return cls._cached_checkpoints @classmethod def datadir_subdir(cls) -> Optional[str]: """The name of the folder in the filesystem. None means top-level, used by mainnet. """ return cls.NET_NAME @classmethod def cli_flag(cls) -> str: """as used in e.g. `$ run_electrum --testnet4`""" return cls.NET_NAME @classmethod def config_key(cls) -> str: """as used for SimpleConfig.get()""" return cls.NET_NAME class BitcoinMainnet(AbstractNet): NET_NAME = "mainnet" TESTNET = False WIF_PREFIX = 0x80 ADDRTYPE_P2PKH = 0 ADDRTYPE_P2SH = 5 SEGWIT_HRP = "bc" BOLT11_HRP = SEGWIT_HRP GENESIS = "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f" DEFAULT_PORTS = {'t': '50001', 's': '50002'} BLOCK_HEIGHT_FIRST_LIGHTNING_CHANNELS = 497000 XPRV_HEADERS = { 'standard': 0x0488ade4, # xprv 'p2wpkh-p2sh': 0x049d7878, # yprv 'p2wsh-p2sh': 0x0295b005, # Yprv 'p2wpkh': 0x04b2430c, # zprv 'p2wsh': 0x02aa7a99, # Zprv } XPRV_HEADERS_INV = inv_dict(XPRV_HEADERS) XPUB_HEADERS = { 'standard': 0x0488b21e, # xpub 'p2wpkh-p2sh': 0x049d7cb2, # ypub 'p2wsh-p2sh': 0x0295b43f, # Ypub 'p2wpkh': 0x04b24746, # zpub 'p2wsh': 0x02aa7ed3, # Zpub } XPUB_HEADERS_INV = inv_dict(XPUB_HEADERS) BIP44_COIN_TYPE = 0 LN_REALM_BYTE = 0 LN_DNS_SEEDS = [ 'nodes.lightning.directory.', 'lseed.bitcoinstats.com.', 'lseed.darosior.ninja', ] @classmethod def datadir_subdir(cls): return None class BitcoinTestnet(AbstractNet): NET_NAME = "testnet" TESTNET = True WIF_PREFIX = 0xef ADDRTYPE_P2PKH = 111 ADDRTYPE_P2SH = 196 SEGWIT_HRP = "tb" BOLT11_HRP = SEGWIT_HRP GENESIS = "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943" DEFAULT_PORTS = {'t': '51001', 's': '51002'} XPRV_HEADERS = { 'standard': 0x04358394, # tprv 'p2wpkh-p2sh': 0x044a4e28, # uprv 'p2wsh-p2sh': 0x024285b5, # Uprv 'p2wpkh': 0x045f18bc, # vprv 'p2wsh': 0x02575048, # Vprv } XPRV_HEADERS_INV = inv_dict(XPRV_HEADERS) XPUB_HEADERS = { 'standard': 0x043587cf, # tpub 'p2wpkh-p2sh': 0x044a5262, # upub 'p2wsh-p2sh': 0x024289ef, # Upub 'p2wpkh': 0x045f1cf6, # vpub 'p2wsh': 0x02575483, # Vpub } XPUB_HEADERS_INV = inv_dict(XPUB_HEADERS) BIP44_COIN_TYPE = 1 LN_REALM_BYTE = 1 LN_DNS_SEEDS = [ # TODO investigate this again #'test.nodes.lightning.directory.', # times out. #'lseed.bitcoinstats.com.', # ignores REALM byte and returns mainnet peers... ] class BitcoinTestnet4(BitcoinTestnet): NET_NAME = "testnet4" GENESIS = "00000000da84f2bafbbc53dee25a72ae507ff4914b867c565be350b0da8bf043" LN_DNS_SEEDS = [] class BitcoinRegtest(BitcoinTestnet): NET_NAME = "regtest" SEGWIT_HRP = "bcrt" BOLT11_HRP = SEGWIT_HRP GENESIS = "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206" LN_DNS_SEEDS = [] class BitcoinSimnet(BitcoinTestnet): NET_NAME = "simnet" WIF_PREFIX = 0x64 ADDRTYPE_P2PKH = 0x3f ADDRTYPE_P2SH = 0x7b SEGWIT_HRP = "sb" BOLT11_HRP = SEGWIT_HRP GENESIS = "683e86bd5c6d110d91b94b97137ba6bfe02dbbdb8e3dff722a669b5d69d77af6" LN_DNS_SEEDS = [] class BitcoinSignet(BitcoinTestnet): NET_NAME = "signet" BOLT11_HRP = "tbs" GENESIS = "00000008819873e925422c1ff0f99f7cc9bbb232af63a077a480a3633bee1ef6" LN_DNS_SEEDS = [] class BitcoinMutinynet(BitcoinTestnet): NET_NAME = "mutinynet" BOLT11_HRP = "tbs" GENESIS = "00000008819873e925422c1ff0f99f7cc9bbb232af63a077a480a3633bee1ef6" LN_DNS_SEEDS = [] NETS_LIST = tuple(all_subclasses(AbstractNet)) # type: Sequence[Type[AbstractNet]] NETS_LIST = tuple(sorted(NETS_LIST, key=lambda x: x.NET_NAME)) assert len(NETS_LIST) == len(set([chain.NET_NAME for chain in NETS_LIST])), "NET_NAME must be unique for each concrete AbstractNet" assert len(NETS_LIST) == len(set([chain.datadir_subdir() for chain in NETS_LIST])), "datadir must be unique for each concrete AbstractNet" assert len(NETS_LIST) == len(set([chain.cli_flag() for chain in NETS_LIST])), "cli_flag must be unique for each concrete AbstractNet" assert len(NETS_LIST) == len(set([chain.config_key() for chain in NETS_LIST])), "config_key must be unique for each concrete AbstractNet" # don't import net directly, import the module instead (so that net is singleton) net = BitcoinMainnet # type: Type[AbstractNet] ================================================ FILE: electrum/contacts.py ================================================ # Electrum - Lightweight Bitcoin Client # Copyright (c) 2015 Thomas Voegtlin # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files # (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, # publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import re from typing import Optional, Tuple, Dict, Any, TYPE_CHECKING import asyncio import dns from dns.exception import DNSException from . import bitcoin from . import dnssec from .util import read_json_file, write_json_file, to_string, is_valid_email from .logging import Logger, get_logger from .util import trigger_callback, get_asyncio_loop if TYPE_CHECKING: from .wallet_db import WalletDB from .simple_config import SimpleConfig _logger = get_logger(__name__) class AliasNotFoundException(Exception): pass class Contacts(dict, Logger): def __init__(self, db: 'WalletDB'): Logger.__init__(self) self.db = db d = self.db.get('contacts', {}) try: self.update(d) except Exception: return # backward compatibility for k, v in self.items(): _type, n = v if _type == 'address' and bitcoin.is_address(n): self.pop(k) self[n] = ('address', k) def save(self): self.db.put('contacts', dict(self)) trigger_callback('contacts_updated') def import_file(self, path): data = read_json_file(path) data = self._validate(data) self.update(data) self.save() def export_file(self, path): write_json_file(path, self) def __setitem__(self, key, value): dict.__setitem__(self, key, value) self.save() def pop(self, key): if key in self.keys(): res = dict.pop(self, key) self.save() return res return None async def resolve(self, k) -> dict: if bitcoin.is_address(k): return { 'address': k, 'type': 'address' } for address, (_type, label) in self.items(): if k.casefold() != label.casefold(): continue if _type in ('address', 'lnaddress'): return { 'address': address, 'type': 'contact' } if openalias := await self.resolve_openalias(k): return openalias raise AliasNotFoundException("Invalid Bitcoin address or alias", k) @classmethod async def resolve_openalias(cls, url: str) -> Dict[str, Any]: out = await cls._resolve_openalias(url) if out: address, name = out return { 'address': address, 'name': name, 'type': 'openalias', } return {} def by_name(self, name): for k in self.keys(): _type, addr = self[k] if addr.casefold() == name.casefold(): return { 'name': addr, 'type': _type, 'address': k } return None def fetch_openalias(self, config: 'SimpleConfig'): self.alias_info = None alias = config.OPENALIAS_ID if alias: alias = str(alias) async def f(): self.alias_info = await self._resolve_openalias(alias) trigger_callback('alias_received') asyncio.run_coroutine_threadsafe(f(), get_asyncio_loop()) @classmethod async def _resolve_openalias(cls, url: str) -> Optional[Tuple[str, str]]: # support email-style addresses, per the OA standard url = url.replace('@', '.') try: records, validated = await dnssec.query(url, dns.rdatatype.TXT) except DNSException as e: _logger.info(f'Error resolving openalias: {repr(e)}') return None if not validated: # enforce DNSSEC validation. without it, DNS is completely insecure _logger.info(f"DNSSEC validation failed for {url=!r}, or maybe dependencies are missing and could not even try.") return None prefix = 'btc' for record in records: if record.rdtype != dns.rdatatype.TXT: continue string = to_string(record.strings[0], 'utf8') if string.startswith('oa1:' + prefix): address = cls.find_regex(string, r'recipient_address=([A-Za-z0-9]+)') name = cls.find_regex(string, r'recipient_name=([^;]+)') if not name: name = address if not address: continue return address, name return None @staticmethod def find_regex(haystack, needle): regex = re.compile(needle) try: return regex.search(haystack).groups()[0] except AttributeError: return None def _validate(self, data): for k, v in list(data.items()): if k == 'contacts': return self._validate(v) if not (bitcoin.is_address(k) or is_valid_email(k)): data.pop(k) else: _type, _ = v if _type not in ('address', 'lnaddress'): data.pop(k) return data ================================================ FILE: electrum/crypto.py ================================================ # -*- coding: utf-8 -*- # # Electrum - lightweight Bitcoin client # Copyright (C) 2018 The Electrum developers # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files # (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, # publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import base64 import binascii import os import sys import hashlib import hmac from typing import Union, Mapping, Optional import electrum_ecc as ecc from .util import assert_bytes, InvalidPassword, to_bytes, to_string, WalletFileException, versiontuple from .i18n import _ from .logging import get_logger _logger = get_logger(__name__) HAS_PYAES = False try: import pyaes except Exception: pass else: HAS_PYAES = True HAS_CRYPTODOME = False MIN_CRYPTODOME_VERSION = "3.7" try: import Cryptodome if versiontuple(Cryptodome.__version__) < versiontuple(MIN_CRYPTODOME_VERSION): _logger.warning(f"found module 'Cryptodome' but it is too old: {Cryptodome.__version__}<{MIN_CRYPTODOME_VERSION}") raise Exception() from Cryptodome.Cipher import ChaCha20_Poly1305 as CD_ChaCha20_Poly1305 from Cryptodome.Cipher import ChaCha20 as CD_ChaCha20 from Cryptodome.Cipher import AES as CD_AES except Exception: pass else: HAS_CRYPTODOME = True HAS_CRYPTOGRAPHY = False MIN_CRYPTOGRAPHY_VERSION = "2.1" try: import cryptography if versiontuple(cryptography.__version__) < versiontuple(MIN_CRYPTOGRAPHY_VERSION): _logger.warning(f"found module 'cryptography' but it is too old: {cryptography.__version__}<{MIN_CRYPTOGRAPHY_VERSION}") raise Exception() from cryptography import exceptions from cryptography.hazmat.primitives.ciphers import Cipher as CG_Cipher from cryptography.hazmat.primitives.ciphers import algorithms as CG_algorithms from cryptography.hazmat.primitives.ciphers import modes as CG_modes from cryptography.hazmat.backends import default_backend as CG_default_backend import cryptography.hazmat.primitives.ciphers.aead as CG_aead except Exception: pass else: HAS_CRYPTOGRAPHY = True if not (HAS_CRYPTODOME or HAS_CRYPTOGRAPHY): sys.exit(f"Error: at least one of ('pycryptodomex', 'cryptography') needs to be installed.") def version_info() -> Mapping[str, Optional[str]]: ret = {} if HAS_PYAES: ret["pyaes.version"] = ".".join(map(str, pyaes.VERSION[:3])) else: ret["pyaes.version"] = None if HAS_CRYPTODOME: ret["cryptodome.version"] = Cryptodome.__version__ if hasattr(Cryptodome, "__path__"): ret["cryptodome.path"] = ", ".join(Cryptodome.__path__ or []) else: ret["cryptodome.version"] = None if HAS_CRYPTOGRAPHY: ret["cryptography.version"] = cryptography.__version__ if hasattr(cryptography, "__path__"): ret["cryptography.path"] = ", ".join(cryptography.__path__ or []) else: ret["cryptography.version"] = None return ret class InvalidPadding(Exception): pass class CiphertextFormatError(Exception): pass def append_PKCS7_padding(data: bytes) -> bytes: assert_bytes(data) padlen = 16 - (len(data) % 16) return data + bytes([padlen]) * padlen def strip_PKCS7_padding(data: bytes) -> bytes: assert_bytes(data) if len(data) % 16 != 0 or len(data) == 0: raise InvalidPadding("invalid length") padlen = data[-1] if not (0 < padlen <= 16): raise InvalidPadding("invalid padding byte (out of range)") for i in data[-padlen:]: if i != padlen: raise InvalidPadding("invalid padding byte (inconsistent)") return data[0:-padlen] def aes_encrypt_with_iv(key: bytes, iv: bytes, data: bytes) -> bytes: assert_bytes(key, iv, data) data = append_PKCS7_padding(data) if HAS_CRYPTODOME: e = CD_AES.new(key, CD_AES.MODE_CBC, iv).encrypt(data) elif HAS_CRYPTOGRAPHY: cipher = CG_Cipher(CG_algorithms.AES(key), CG_modes.CBC(iv), backend=CG_default_backend()) encryptor = cipher.encryptor() e = encryptor.update(data) + encryptor.finalize() elif HAS_PYAES: aes_cbc = pyaes.AESModeOfOperationCBC(key, iv=iv) aes = pyaes.Encrypter(aes_cbc, padding=pyaes.PADDING_NONE) e = aes.feed(data) + aes.feed() # empty aes.feed() flushes buffer else: raise Exception("no AES backend found") return e def aes_decrypt_with_iv(key: bytes, iv: bytes, data: bytes) -> bytes: assert_bytes(key, iv, data) if HAS_CRYPTODOME: cipher = CD_AES.new(key, CD_AES.MODE_CBC, iv) data = cipher.decrypt(data) elif HAS_CRYPTOGRAPHY: cipher = CG_Cipher(CG_algorithms.AES(key), CG_modes.CBC(iv), backend=CG_default_backend()) decryptor = cipher.decryptor() data = decryptor.update(data) + decryptor.finalize() elif HAS_PYAES: aes_cbc = pyaes.AESModeOfOperationCBC(key, iv=iv) aes = pyaes.Decrypter(aes_cbc, padding=pyaes.PADDING_NONE) data = aes.feed(data) + aes.feed() # empty aes.feed() flushes buffer else: raise Exception("no AES backend found") try: return strip_PKCS7_padding(data) except InvalidPadding: raise InvalidPassword() def EncodeAES_bytes(secret: bytes, msg: bytes) -> bytes: assert_bytes(msg) iv = bytes(os.urandom(16)) ct = aes_encrypt_with_iv(secret, iv, msg) return iv + ct def DecodeAES_bytes(secret: bytes, ciphertext: bytes) -> bytes: assert_bytes(ciphertext) iv, e = ciphertext[:16], ciphertext[16:] s = aes_decrypt_with_iv(secret, iv, e) return s PW_HASH_VERSION_LATEST = 1 KNOWN_PW_HASH_VERSIONS = (1, 2,) SUPPORTED_PW_HASH_VERSIONS = (1,) assert PW_HASH_VERSION_LATEST in KNOWN_PW_HASH_VERSIONS assert PW_HASH_VERSION_LATEST in SUPPORTED_PW_HASH_VERSIONS class UnexpectedPasswordHashVersion(InvalidPassword, WalletFileException): def __init__(self, version): InvalidPassword.__init__(self) WalletFileException.__init__(self) self.version = version def __str__(self): return "{unexpected}: {version}\n{instruction}".format( unexpected=_("Unexpected password hash version"), version=self.version, instruction=_('You are most likely using an outdated version of Electrum. Please update.')) class UnsupportedPasswordHashVersion(InvalidPassword, WalletFileException): def __init__(self, version): InvalidPassword.__init__(self) WalletFileException.__init__(self) self.version = version def __str__(self): return "{unsupported}: {version}\n{instruction}".format( unsupported=_("Unsupported password hash version"), version=self.version, instruction=f"To open this wallet, try 'git checkout password_v{self.version}'.\n" "Alternatively, restore from seed.") def _hash_password(password: Union[bytes, str], *, version: int) -> bytes: pw = to_bytes(password, 'utf8') if version not in SUPPORTED_PW_HASH_VERSIONS: raise UnsupportedPasswordHashVersion(version) if version == 1: return sha256d(pw) else: assert version not in KNOWN_PW_HASH_VERSIONS raise UnexpectedPasswordHashVersion(version) def _pw_encode_raw(data: bytes, password: Union[bytes, str], *, version: int) -> bytes: if version not in KNOWN_PW_HASH_VERSIONS: raise UnexpectedPasswordHashVersion(version) # derive key from password secret = _hash_password(password, version=version) # encrypt given data ciphertext = EncodeAES_bytes(secret, data) return ciphertext def _pw_decode_raw(data_bytes: bytes, password: Union[bytes, str], *, version: int) -> bytes: if version not in KNOWN_PW_HASH_VERSIONS: raise UnexpectedPasswordHashVersion(version) # derive key from password secret = _hash_password(password, version=version) # decrypt given data try: d = DecodeAES_bytes(secret, data_bytes) except Exception as e: raise InvalidPassword() from e return d def pw_encode_bytes(data: bytes, password: Union[bytes, str], *, version: int) -> str: """plaintext bytes -> base64 ciphertext""" ciphertext = _pw_encode_raw(data, password, version=version) ciphertext_b64 = base64.b64encode(ciphertext) return ciphertext_b64.decode('utf8') def pw_decode_bytes(data: str, password: Union[bytes, str], *, version:int) -> bytes: """base64 ciphertext -> plaintext bytes""" if version not in KNOWN_PW_HASH_VERSIONS: raise UnexpectedPasswordHashVersion(version) try: data_bytes = bytes(base64.b64decode(data, validate=True)) except binascii.Error as e: raise CiphertextFormatError("ciphertext not valid base64") from e return _pw_decode_raw(data_bytes, password, version=version) def pw_encode_with_version_and_mac(data: bytes, password: Union[bytes, str]) -> str: """plaintext bytes -> base64 ciphertext""" # https://crypto.stackexchange.com/questions/202/should-we-mac-then-encrypt-or-encrypt-then-mac # Encrypt-and-MAC. The MAC will be used to detect invalid passwords version = PW_HASH_VERSION_LATEST mac = sha256(data)[0:4] ciphertext = _pw_encode_raw(data, password, version=version) ciphertext_b64 = base64.b64encode(bytes([version]) + ciphertext + mac) return ciphertext_b64.decode('utf8') def pw_decode_with_version_and_mac(data: str, password: Union[bytes, str]) -> bytes: """base64 ciphertext -> plaintext bytes""" try: data_bytes = bytes(base64.b64decode(data, validate=True)) except binascii.Error as e: raise CiphertextFormatError("ciphertext not valid base64") from e version = int(data_bytes[0]) encrypted = data_bytes[1:-4] mac = data_bytes[-4:] if version not in KNOWN_PW_HASH_VERSIONS: raise UnexpectedPasswordHashVersion(version) decrypted = _pw_decode_raw(encrypted, password, version=version) if sha256(decrypted)[0:4] != mac: raise InvalidPassword() return decrypted def pw_encode(data: str, password: Union[bytes, str, None], *, version: int) -> str: """plaintext str -> base64 ciphertext""" if not password: return data plaintext_bytes = to_bytes(data, "utf8") return pw_encode_bytes(plaintext_bytes, password, version=version) def pw_decode(data: str, password: Union[bytes, str, None], *, version: int) -> str: """base64 ciphertext -> plaintext str""" if password is None: return data plaintext_bytes = pw_decode_bytes(data, password, version=version) try: plaintext_str = to_string(plaintext_bytes, "utf8") except UnicodeDecodeError as e: raise InvalidPassword() from e return plaintext_str def sha256(x: Union[bytes, str]) -> bytes: x = to_bytes(x, 'utf8') return bytes(hashlib.sha256(x).digest()) def sha256d(x: Union[bytes, str]) -> bytes: x = to_bytes(x, 'utf8') out = bytes(sha256(sha256(x))) return out def hash_160(x: bytes) -> bytes: return ripemd(sha256(x)) def ripemd(x: bytes) -> bytes: try: md = hashlib.new('ripemd160') md.update(x) return md.digest() except BaseException: # ripemd160 is not guaranteed to be available in hashlib on all platforms. # Historically, our Android builds had hashlib/openssl which did not have it. # see https://github.com/spesmilo/electrum/issues/7093 # We bundle a pure python implementation as fallback that gets used now: from . import ripemd md = ripemd.new(x) return md.digest() def hmac_oneshot(key: bytes, msg: bytes, digest) -> bytes: return hmac.digest(key, msg, digest) def chacha20_poly1305_encrypt( *, key: bytes, nonce: bytes, associated_data: bytes = None, data: bytes ) -> bytes: assert isinstance(key, (bytes, bytearray)) assert isinstance(nonce, (bytes, bytearray)) assert isinstance(associated_data, (bytes, bytearray, type(None))) assert isinstance(data, (bytes, bytearray)) assert len(key) == 32, f"unexpected key size: {len(key)} (expected: 32)" assert len(nonce) == 12, f"unexpected nonce size: {len(nonce)} (expected: 12)" if HAS_CRYPTODOME: cipher = CD_ChaCha20_Poly1305.new(key=key, nonce=nonce) if associated_data is not None: cipher.update(associated_data) ciphertext, mac = cipher.encrypt_and_digest(plaintext=data) return ciphertext + mac if HAS_CRYPTOGRAPHY: a = CG_aead.ChaCha20Poly1305(key) return a.encrypt(nonce, data, associated_data) raise Exception("no chacha20 backend found") def chacha20_poly1305_decrypt( *, key: bytes, nonce: bytes, associated_data: bytes = None, data: bytes ) -> bytes: assert isinstance(key, (bytes, bytearray)) assert isinstance(nonce, (bytes, bytearray)) assert isinstance(associated_data, (bytes, bytearray, type(None))) assert isinstance(data, (bytes, bytearray)) assert len(key) == 32, f"unexpected key size: {len(key)} (expected: 32)" assert len(nonce) == 12, f"unexpected nonce size: {len(nonce)} (expected: 12)" if HAS_CRYPTODOME: cipher = CD_ChaCha20_Poly1305.new(key=key, nonce=nonce) if associated_data is not None: cipher.update(associated_data) # raises ValueError if not valid (e.g. incorrect MAC) return cipher.decrypt_and_verify(ciphertext=data[:-16], received_mac_tag=data[-16:]) if HAS_CRYPTOGRAPHY: a = CG_aead.ChaCha20Poly1305(key) try: return a.decrypt(nonce, data, associated_data) except cryptography.exceptions.InvalidTag as e: raise ValueError("invalid tag") from e raise Exception("no chacha20 backend found") def chacha20_encrypt(*, key: bytes, nonce: bytes, data: bytes) -> bytes: """note: for any new protocol you design, please consider using chacha20_poly1305_encrypt instead (for its Authenticated Encryption property). """ assert isinstance(key, (bytes, bytearray)) assert isinstance(nonce, (bytes, bytearray)) assert isinstance(data, (bytes, bytearray)) assert len(key) == 32, f"unexpected key size: {len(key)} (expected: 32)" assert len(nonce) in (8, 12), f"unexpected nonce size: {len(nonce)} (expected: 8 or 12)" if HAS_CRYPTODOME: cipher = CD_ChaCha20.new(key=key, nonce=nonce) return cipher.encrypt(data) if HAS_CRYPTOGRAPHY: nonce = bytes(16 - len(nonce)) + nonce # cryptography wants 16 byte nonces algo = CG_algorithms.ChaCha20(key=key, nonce=nonce) cipher = CG_Cipher(algo, mode=None, backend=CG_default_backend()) encryptor = cipher.encryptor() return encryptor.update(data) raise Exception("no chacha20 backend found") def chacha20_decrypt(*, key: bytes, nonce: bytes, data: bytes) -> bytes: assert isinstance(key, (bytes, bytearray)) assert isinstance(nonce, (bytes, bytearray)) assert isinstance(data, (bytes, bytearray)) assert len(key) == 32, f"unexpected key size: {len(key)} (expected: 32)" assert len(nonce) in (8, 12), f"unexpected nonce size: {len(nonce)} (expected: 8 or 12)" if HAS_CRYPTODOME: cipher = CD_ChaCha20.new(key=key, nonce=nonce) return cipher.decrypt(data) if HAS_CRYPTOGRAPHY: nonce = bytes(16 - len(nonce)) + nonce # cryptography wants 16 byte nonces algo = CG_algorithms.ChaCha20(key=key, nonce=nonce) cipher = CG_Cipher(algo, mode=None, backend=CG_default_backend()) decryptor = cipher.decryptor() return decryptor.update(data) raise Exception("no chacha20 backend found") def ecies_encrypt_message( ec_pubkey: 'ecc.ECPubkey', message: bytes, *, magic: bytes = b'BIE1', ) -> bytes: """ ECIES encryption/decryption methods; AES-128-CBC with PKCS7 is used as the cipher; hmac-sha256 is used as the mac """ assert_bytes(message) ephemeral = ecc.ECPrivkey.generate_random_key() ecdh_key = (ec_pubkey * ephemeral.secret_scalar).get_public_key_bytes(compressed=True) key = hashlib.sha512(ecdh_key).digest() iv, key_e, key_m = key[0:16], key[16:32], key[32:] ciphertext = aes_encrypt_with_iv(key_e, iv, message) ephemeral_pubkey = ephemeral.get_public_key_bytes(compressed=True) encrypted = magic + ephemeral_pubkey + ciphertext mac = hmac_oneshot(key_m, encrypted, hashlib.sha256) return base64.b64encode(encrypted + mac) def ecies_decrypt_message( ec_privkey: 'ecc.ECPrivkey', encrypted: Union[str, bytes], *, magic: bytes = b'BIE1', ) -> bytes: encrypted = base64.b64decode(encrypted, validate=True) # type: bytes if len(encrypted) < 85: raise Exception('invalid ciphertext: length') magic_found = encrypted[:4] ephemeral_pubkey_bytes = encrypted[4:37] ciphertext = encrypted[37:-32] mac = encrypted[-32:] if magic_found != magic: raise Exception('invalid ciphertext: invalid magic bytes') try: ephemeral_pubkey = ecc.ECPubkey(ephemeral_pubkey_bytes) except ecc.InvalidECPointException as e: raise Exception('invalid ciphertext: invalid ephemeral pubkey') from e ecdh_key = (ephemeral_pubkey * ec_privkey.secret_scalar).get_public_key_bytes(compressed=True) key = hashlib.sha512(ecdh_key).digest() iv, key_e, key_m = key[0:16], key[16:32], key[32:] if mac != hmac_oneshot(key_m, encrypted[:-32], hashlib.sha256): raise InvalidPassword() return aes_decrypt_with_iv(key_e, iv, ciphertext) def get_ecdh(priv: bytes, pub: bytes) -> bytes: pt = ecc.ECPubkey(pub) * ecc.string_to_number(priv) return sha256(pt.get_public_key_bytes()) def privkey_to_pubkey(priv: bytes) -> bytes: return ecc.ECPrivkey(priv[:32]).get_public_key_bytes() ================================================ FILE: electrum/currencies.json ================================================ { "Bit2C": [ "ILS" ], "BitFinex": [ "EUR", "GBP", "JPY", "TRY", "USD", "UST" ], "BitFlyer": [ "JPY" ], "BitPay": [ "AED", "AFN", "ALL", "AMD", "ANG", "AOA", "APE", "ARS", "AUD", "AWG", "AZN", "BAM", "BBD", "BCH", "BDT", "BGN", "BHD", "BIF", "BMD", "BND", "BOB", "BRL", "BSD", "BTC", "BTN", "BWP", "BYN", "BZD", "CAD", "CDF", "CHF", "CLF", "CLP", "CNY", "COP", "CRC", "CUP", "CVE", "CZK", "DAI", "DJF", "DKK", "DOP", "DZD", "EGP", "ETB", "ETH", "EUR", "FJD", "FKP", "GBP", "GEL", "GHS", "GIP", "GMD", "GNF", "GTQ", "GYD", "HKD", "HNL", "HRK", "HTG", "HUF", "IDR", "ILS", "INR", "IQD", "IRR", "ISK", "JEP", "JMD", "JOD", "JPY", "KES", "KGS", "KHR", "KMF", "KPW", "KRW", "KWD", "KYD", "KZT", "LAK", "LBP", "LKR", "LRD", "LSL", "LTC", "LYD", "MAD", "MDL", "MGA", "MKD", "MMK", "MNT", "MOP", "MRU", "MUR", "MVR", "MWK", "MXN", "MYR", "MZN", "NAD", "NGN", "NIO", "NOK", "NPR", "NZD", "OMR", "PAB", "PAX", "PEN", "PGK", "PHP", "PKR", "PLN", "PYG", "QAR", "RON", "RSD", "RUB", "RWF", "SAR", "SBD", "SCR", "SDG", "SEK", "SGD", "SHP", "SLL", "SOS", "SRD", "STN", "SVC", "SYP", "SZL", "THB", "TJS", "TMT", "TND", "TOP", "TRY", "TTD", "TWD", "TZS", "UAH", "UGX", "USD", "UYU", "UZS", "VES", "VND", "VUV", "WST", "XAF", "XAG", "XAU", "XCD", "XOF", "XPF", "XRP", "YER", "ZAR", "ZMW", "ZWL" ], "BitStamp": [ "USD", "EUR", "GBP" ], "Bitbank": [ "JPY" ], "Bitso": [ "MXN" ], "Bitvalor": [ "BRL" ], "BlockchainInfo": [ "ARS", "AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK", "EUR", "GBP", "HKD", "HRK", "HUF", "INR", "ISK", "JPY", "KRW", "NZD", "PLN", "RON", "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "USD" ], "Bylls": [ "CAD" ], "CoinCap": [ "USD" ], "CoinDesk": [ "AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "AUD", "AWG", "AZN", "BAM", "BBD", "BDT", "BGN", "BHD", "BIF", "BMD", "BND", "BOB", "BRL", "BSD", "BTC", "BTN", "BWP", "BYR", "BZD", "CAD", "CDF", "CHF", "CLF", "CLP", "CNY", "COP", "CRC", "CUC", "CUP", "CVE", "CZK", "DJF", "DKK", "DOP", "DZD", "EGP", "ERN", "ETB", "EUR", "FJD", "FKP", "GBP", "GEL", "GGP", "GHS", "GIP", "GMD", "GNF", "GTQ", "GYD", "HKD", "HNL", "HRK", "HTG", "HUF", "IDR", "ILS", "IMP", "INR", "IQD", "IRR", "ISK", "JEP", "JMD", "JOD", "JPY", "KES", "KGS", "KHR", "KMF", "KPW", "KRW", "KWD", "KYD", "KZT", "LAK", "LBP", "LKR", "LRD", "LSL", "LYD", "MAD", "MDL", "MGA", "MKD", "MMK", "MNT", "MOP", "MRU", "MUR", "MVR", "MWK", "MXN", "MYR", "MZN", "NAD", "NGN", "NIO", "NOK", "NPR", "NZD", "OMR", "PAB", "PEN", "PGK", "PHP", "PKR", "PLN", "PYG", "QAR", "RON", "RSD", "RUB", "RWF", "SAR", "SBD", "SCR", "SDG", "SEK", "SGD", "SHP", "SLL", "SOS", "SRD", "STD", "STN", "SVC", "SYP", "SZL", "THB", "TJS", "TMT", "TND", "TOP", "TRY", "TTD", "TWD", "TZS", "UAH", "UGX", "USD", "UYU", "UZS", "VES", "VND", "VUV", "WST", "XAF", "XAG", "XAU", "XBT", "XCD", "XDR", "XOF", "XPF", "YER", "ZAR", "ZMW", "ZWL" ], "CoinGecko": [ "AED", "ARS", "AUD", "BCH", "BDT", "BHD", "BMD", "BNB", "BRL", "BTC", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK", "DOT", "EOS", "ETH", "EUR", "GBP", "GEL", "HKD", "HUF", "IDR", "ILS", "INR", "JPY", "KRW", "KWD", "LKR", "LTC", "MMK", "MXN", "MYR", "NGN", "NOK", "NZD", "PHP", "PKR", "PLN", "RUB", "SAR", "SEK", "SGD", "THB", "TRY", "TWD", "UAH", "USD", "VEF", "VND", "XAG", "XAU", "XDR", "XLM", "XRP", "YFI", "ZAR" ], "Coinbase": [ "ABT", "ACH", "ACS", "ACX", "ADA", "AED", "AFN", "AKT", "ALL", "AMD", "AMP", "ANG", "ANT", "AOA", "APE", "APT", "ARB", "ARS", "ASM", "AST", "ATA", "AUD", "AVT", "AWG", "AXL", "AXS", "AZN", "BAL", "BAM", "BAT", "BBD", "BCH", "BDT", "BGN", "BHD", "BIF", "BIT", "BLZ", "BMD", "BND", "BNT", "BOB", "BRL", "BSD", "BSV", "BTC", "BTN", "BWP", "BYN", "BYR", "BZD", "C98", "CAD", "CDF", "CHF", "CHZ", "CLF", "CLP", "CLV", "CNH", "CNY", "COP", "COW", "CRC", "CRO", "CRV", "CTX", "CUC", "CUP", "CVC", "CVE", "CVX", "CZK", "DAI", "DAR", "DDX", "DIA", "DJF", "DKK", "DNT", "DOP", "DOT", "DYP", "DZD", "EEK", "EGP", "ELA", "ENJ", "ENS", "EOS", "ERN", "ETB", "ETC", "ETH", "EUR", "FET", "FIL", "FIS", "FJD", "FKP", "FLR", "FOX", "FTM", "GAL", "GBP", "GEL", "GFI", "GGP", "GHS", "GIP", "GLM", "GMD", "GMT", "GNF", "GNO", "GNT", "GRT", "GST", "GTC", "GTQ", "GYD", "HFT", "HKD", "HNL", "HNT", "HRK", "HTG", "HUF", "ICP", "IDR", "ILS", "ILV", "IMP", "IMX", "INJ", "INR", "INV", "IQD", "IRR", "ISK", "JEP", "JMD", "JOD", "JPY", "JTO", "JUP", "KES", "KGS", "KHR", "KMF", "KNC", "KPW", "KRL", "KRW", "KSM", "KWD", "KYD", "KZT", "LAK", "LBP", "LCX", "LDO", "LIT", "LKR", "LPT", "LRC", "LRD", "LSL", "LTC", "LTL", "LVL", "LYD", "MAD", "MDL", "MDT", "MGA", "MIR", "MKD", "MKR", "MLN", "MMK", "MNT", "MOP", "MPL", "MRO", "MRU", "MTL", "MUR", "MVR", "MWK", "MXC", "MXN", "MYR", "MZN", "NAD", "NCT", "NGN", "NIO", "NKN", "NMR", "NOK", "NPR", "NZD", "OGN", "OMG", "OMR", "ORN", "OXT", "PAB", "PAX", "PEN", "PGK", "PHP", "PKR", "PLA", "PLN", "PLU", "PNG", "POL", "PRO", "PRQ", "PYG", "PYR", "QAR", "QNT", "RAD", "RAI", "RBN", "REN", "REP", "REQ", "RGT", "RLC", "RLY", "RON", "RPL", "RSD", "RUB", "RWF", "SAR", "SBD", "SCR", "SDG", "SEI", "SEK", "SGD", "SHP", "SKK", "SKL", "SLL", "SNT", "SNX", "SOL", "SOS", "SPA", "SRD", "SSP", "STD", "STG", "STX", "SUI", "SVC", "SYN", "SYP", "SZL", "THB", "TIA", "TJS", "TMM", "TMT", "TND", "TOP", "TRB", "TRU", "TRY", "TTD", "TVK", "TWD", "TZS", "UAH", "UGX", "UMA", "UNI", "UPI", "USD", "UST", "UYU", "UZS", "VEF", "VES", "VET", "VGX", "VND", "VUV", "WST", "XAF", "XAG", "XAU", "XBC", "XCD", "XCN", "XDR", "XLM", "XOF", "XPD", "XPF", "XPT", "XRP", "XTZ", "XYO", "YER", "YFI", "ZAR", "ZEC", "ZEN", "ZMK", "ZMW", "ZRX", "ZWD" ], "CointraderMonitor": [ "BRL" ], "Kraken": [ "CAD", "EUR", "GBP", "JPY", "USD" ], "Walltime": [ "BRL" ], "Yadio": [ "AED", "ALL", "ANG", "AOA", "ARS", "AUD", "AZN", "BDT", "BGN", "BHD", "BIF", "BMD", "BOB", "BRL", "BTC", "BWP", "BYN", "BZD", "CAD", "CDF", "CHF", "CLP", "CNY", "COP", "CRC", "CUP", "CZK", "DJF", "DKK", "DOP", "DZD", "EGP", "ETB", "EUR", "GBP", "GEL", "GHS", "GNF", "GTQ", "HKD", "HNL", "HUF", "IDR", "ILS", "INR", "IRR", "IRT", "ISK", "JMD", "JOD", "JPY", "KES", "KGS", "KRW", "KZT", "LBP", "LKR", "MAD", "MGA", "MLC", "MRU", "MWK", "MXN", "MYR", "NAD", "NGN", "NIO", "NOK", "NPR", "NZD", "PAB", "PEN", "PHP", "PKR", "PLN", "PYG", "QAR", "RON", "RSD", "RUB", "RWF", "SAR", "SEK", "SGD", "SYP", "THB", "TND", "TRY", "TTD", "TWD", "TZS", "UAH", "UGX", "USD", "UYU", "UZS", "VES", "VND", "XAF", "XAG", "XAU", "XOF", "XPT", "ZAR", "ZMW" ], "Zaif": [ "JPY" ] } ================================================ FILE: electrum/daemon.py ================================================ #!/usr/bin/env python # # Electrum - lightweight Bitcoin client # Copyright (C) 2015 Thomas Voegtlin # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files # (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, # publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import asyncio import ast import errno import os import time import traceback import sys import threading from typing import Dict, Optional, Tuple, Callable, Union, Sequence, Mapping, TYPE_CHECKING from base64 import b64decode, b64encode import json import socket import aiohttp from aiohttp import web, client_exceptions from aiorpcx import ignore_after from . import util from .network import Network from .util import ( json_decode, to_bytes, to_string, profiler, standardize_path, constant_time_compare, InvalidPassword, log_exceptions, randrange, OldTaskGroup, UserFacingException, JsonRPCError ) from .wallet import Wallet, Abstract_Wallet from .storage import WalletStorage from .wallet_db import WalletDB, WalletUnfinished from .commands import known_commands, Commands from .simple_config import SimpleConfig from .exchange_rate import FxThread from .logging import get_logger, Logger from . import GuiImportError from .plugin import run_hook, Plugins if TYPE_CHECKING: from electrum import gui _logger = get_logger(__name__) class DaemonNotRunning(Exception): pass def get_rpcsock_defaultpath(config: SimpleConfig): return os.path.join(config.path, 'daemon_rpc_socket') def get_rpcsock_default_type(config: SimpleConfig): if config.RPC_PORT: return 'tcp' # Use unix domain sockets when available, # with the extra paranoia that in case windows "implements" them, # we want to test it before making it the default there. if hasattr(socket, 'AF_UNIX') and sys.platform != 'win32': return 'unix' return 'tcp' def get_lockfile(config: SimpleConfig): return os.path.join(config.path, 'daemon') def remove_lockfile(lockfile): os.unlink(lockfile) def get_file_descriptor(config: SimpleConfig): '''Tries to create the lockfile, using O_EXCL to prevent races. If it succeeds, it returns the FD. Otherwise, try and connect to the server specified in the lockfile. If this succeeds, the server is returned. Otherwise, remove the lockfile and try again.''' lockfile = get_lockfile(config) while True: try: return os.open(lockfile, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644) except OSError: pass try: request(config, 'ping') return None except DaemonNotRunning: # Couldn't connect; remove lockfile and try again. remove_lockfile(lockfile) def request(config: SimpleConfig, endpoint, args=(), timeout: Union[float, int] = 60): lockfile = get_lockfile(config) for attempt in range(5): create_time = None # type: Optional[float | int] path = None try: with open(lockfile) as f: socktype, address, create_time = ast.literal_eval(f.read()) int(create_time) # raise if not numeric if socktype == 'unix': path = address (host, port) = "127.0.0.1", 0 # We still need a host and port for e.g. HTTP Host header elif socktype == 'tcp': (host, port) = address else: raise Exception(f"corrupt lockfile; socktype={socktype!r}") except Exception: raise DaemonNotRunning() rpc_user, rpc_password = get_rpc_credentials(config) server_url = 'http://%s:%d' % (host, port) auth = aiohttp.BasicAuth(login=rpc_user, password=rpc_password) loop = util.get_asyncio_loop() async def request_coroutine( *, socktype=socktype, path=path, auth=auth, server_url=server_url, endpoint=endpoint, ): if socktype == 'unix': connector = aiohttp.UnixConnector(path=path) elif socktype == 'tcp': connector = None # This will transform into TCP. else: raise Exception(f"impossible socktype ({socktype!r})") async with aiohttp.ClientSession(auth=auth, connector=connector) as session: c = util.JsonRPCClient(session, server_url) return await c.request(endpoint, *args) try: fut = asyncio.run_coroutine_threadsafe(request_coroutine(), loop) return fut.result(timeout=timeout) except aiohttp.client_exceptions.ClientConnectorError as e: _logger.info(f"failed to connect to JSON-RPC server {e}") # We cannot communicate with the daemon. # If daemon's creation time is very recent, it might still be starting up. # In any other case, we raise: - too old create_time means daemon is likely dead, # - create_time in future means our clock cannot be trusted. if not (create_time <= time.time() <= create_time + 1.0): raise DaemonNotRunning() # Sleep a bit and try again; daemon might have just been started time.sleep(1.0) # how did we even get here?! the clock must be going haywire. _logger.error(f"Failed to connect to JSON-RPC server. Exhausted all attempts.") raise DaemonNotRunning() def wait_until_daemon_becomes_ready(*, config: SimpleConfig, timeout=5) -> bool: t0 = time.monotonic() while True: if time.monotonic() > t0 + timeout: return False # timeout try: request(config, 'ping') return True # success except DaemonNotRunning: time.sleep(0.05) continue def get_rpc_credentials(config: SimpleConfig) -> Tuple[str, str]: rpc_user = config.RPC_USERNAME or None rpc_password = config.RPC_PASSWORD or None if rpc_user is None or rpc_password is None: rpc_user = 'user' bits = 128 nbytes = bits // 8 + (bits % 8 > 0) pw_int = randrange(pow(2, bits)) pw_b64 = b64encode( pw_int.to_bytes(nbytes, 'big'), b'-_') rpc_password = to_string(pw_b64, 'ascii') config.RPC_USERNAME = rpc_user config.RPC_PASSWORD = rpc_password return rpc_user, rpc_password class AuthenticationError(Exception): pass class AuthenticationInvalidOrMissing(AuthenticationError): pass class AuthenticationCredentialsInvalid(AuthenticationError): pass class AuthenticatedServer(Logger): def __init__(self, rpc_user, rpc_password): Logger.__init__(self) self.rpc_user = rpc_user self.rpc_password = rpc_password self.auth_lock = asyncio.Lock() self._methods = {} # type: Dict[str, Callable] def register_method(self, name: str, f): assert name not in self._methods, f"name collision for {name}" self._methods[name] = f async def authenticate(self, headers): if self.rpc_password == '': # RPC authentication is disabled return auth_string = headers.get('Authorization', None) if auth_string is None: raise AuthenticationInvalidOrMissing('CredentialsMissing') basic, _, encoded = auth_string.partition(' ') if basic != 'Basic': raise AuthenticationInvalidOrMissing('UnsupportedType') encoded = to_bytes(encoded, 'utf8') credentials = to_string(b64decode(encoded, validate=True), 'utf8') username, _, password = credentials.partition(':') if not (constant_time_compare(username, self.rpc_user) and constant_time_compare(password, self.rpc_password)): await asyncio.sleep(0.050) raise AuthenticationCredentialsInvalid('Invalid Credentials') async def handle(self, request): async with self.auth_lock: try: await self.authenticate(request.headers) except AuthenticationInvalidOrMissing: return web.Response(headers={"WWW-Authenticate": "Basic realm=Electrum"}, text='Unauthorized', status=401) except AuthenticationCredentialsInvalid: return web.Response(text='Forbidden', status=403) try: request = await request.text() request = json.loads(request) method = request['method'] _id = request['id'] params = request.get('params', []) # type: Union[Sequence, Mapping] if method not in self._methods: raise Exception(f"attempting to use unregistered method: {method}") f = self._methods[method] except Exception as e: self.logger.exception("invalid request") return web.Response(text='Invalid Request', status=500) response = { 'id': _id, 'jsonrpc': '2.0', } try: if isinstance(params, dict): response['result'] = await f(**params) else: response['result'] = await f(*params) except UserFacingException as e: response['error'] = { 'code': JsonRPCError.Codes.USERFACING, 'message': str(e), } except BaseException as e: self.logger.exception("internal error while executing RPC") response['error'] = { 'code': JsonRPCError.Codes.INTERNAL, 'message': "internal error while executing RPC", 'data': { "exception": repr(e), "traceback": "".join(traceback.format_exception(e)), }, } return web.json_response(response) class CommandsServer(AuthenticatedServer): def __init__(self, daemon: 'Daemon', fd): rpc_user, rpc_password = get_rpc_credentials(daemon.config) AuthenticatedServer.__init__(self, rpc_user, rpc_password) self.daemon = daemon self.fd = fd self.config = daemon.config sockettype = self.config.RPC_SOCKET_TYPE self.socktype = sockettype if sockettype != 'auto' else get_rpcsock_default_type(self.config) self.sockpath = self.config.RPC_SOCKET_FILEPATH or get_rpcsock_defaultpath(self.config) self.host = self.config.RPC_HOST self.port = self.config.RPC_PORT self.app = web.Application() self.app.router.add_post("/", self.handle) self.register_method('ping', self.ping) self.register_method('gui', self.gui) self.cmd_runner = Commands(config=self.config, network=self.daemon.network, daemon=self.daemon) for cmdname in known_commands: self.register_method(cmdname, getattr(self.cmd_runner, cmdname)) self.register_method('run_cmdline', self.run_cmdline) def _socket_config_str(self) -> str: if self.socktype == 'unix': return f"" elif self.socktype == 'tcp': return f"" else: raise Exception(f"unknown socktype '{self.socktype!r}'") async def run(self): self.runner = web.AppRunner(self.app) await self.runner.setup() if self.socktype == 'unix': site = web.UnixSite(self.runner, self.sockpath) elif self.socktype == 'tcp': site = web.TCPSite(self.runner, self.host, self.port) else: raise Exception(f"unknown socktype '{self.socktype!r}'") try: await site.start() except Exception as e: raise Exception(f"failed to start CommandsServer at {self._socket_config_str()}. got exc: {e!r}") from None socket = site._server.sockets[0] if self.socktype == 'unix': addr = self.sockpath elif self.socktype == 'tcp': addr = socket.getsockname() else: raise Exception(f"impossible socktype ({self.socktype!r})") os.write(self.fd, bytes(repr((self.socktype, addr, time.time())), 'utf8')) os.close(self.fd) self.logger.info(f"now running and listening. socktype={self.socktype}, addr={addr}") async def ping(self): return True async def gui(self, config_options): # note: "config_options" is coming from the short-lived CLI-invocation, # while self.config is the config of the long-lived daemon process. # "config_options" should have priority. if self.daemon.gui_object: if hasattr(self.daemon.gui_object, 'new_window'): if config_options.get(SimpleConfig.NETWORK_OFFLINE.key()) and not self.config.NETWORK_OFFLINE: raise UserFacingException( "error: current GUI is running online, so it cannot open a new wallet offline.") path = config_options.get('wallet_path') or self.config.get_wallet_path() self.daemon.gui_object.new_window(path, config_options.get('url')) return True else: raise UserFacingException("error: current GUI does not support multiple windows") else: raise UserFacingException("error: Electrum is running in daemon mode. Please stop the daemon first.") async def run_cmdline(self, config_options): cmdname = config_options['cmd'] cmd = known_commands.get(cmdname) if not cmd: return f"unknown command: {cmdname}" # arguments passed to function args = [config_options.get(x) for x in cmd.params] # decode json arguments args = [json_decode(i) for i in args] # options kwargs = {} for x in cmd.options: kwargs[x] = config_options.get(x) if 'wallet_path' in cmd.options or 'wallet' in cmd.options: wallet_path = config_options.get('wallet_path') if len(self.daemon._wallets) > 1 and wallet_path is None: raise UserFacingException("error: wallet not specified") kwargs['wallet_path'] = wallet_path func = getattr(self.cmd_runner, cmd.name) # execute requested command now. note: cmd can raise, the caller (self.handle) will wrap it. result = await func(*args, **kwargs) return result class Daemon(Logger): network: Optional[Network] = None gui_object: Optional['gui.BaseElectrumGui'] = None @profiler def __init__( self, config: SimpleConfig, fd=None, *, listen_jsonrpc: bool = True, start_network: bool = True, # setting to False allows customising network settings before starting it ): Logger.__init__(self) self.config = config self.listen_jsonrpc = listen_jsonrpc if fd is None and listen_jsonrpc: fd = get_file_descriptor(config) if fd is None: raise Exception('failed to lock daemon; already running?') self._plugins = None # type: Optional[Plugins] self.asyncio_loop = util.get_asyncio_loop() if not self.config.NETWORK_OFFLINE: self.network = Network(config, daemon=self) self.fx = FxThread(config=config) # wallet_key -> wallet self._wallets = {} # type: Dict[str, Abstract_Wallet] self._wallet_lock = threading.RLock() self._stop_entered = False self._stopping_soon_or_errored = threading.Event() self._stopped_event = threading.Event() self.taskgroup = OldTaskGroup() asyncio.run_coroutine_threadsafe(self._run(), self.asyncio_loop) if start_network and self.network: self.start_network() # Setup commands server self.commands_server = None if listen_jsonrpc: self.commands_server = CommandsServer(self, fd) asyncio.run_coroutine_threadsafe(self.taskgroup.spawn(self.commands_server.run()), self.asyncio_loop) @log_exceptions async def _run(self): self.logger.info("starting taskgroup.") try: async with self.taskgroup as group: await group.spawn(asyncio.Event().wait) # run forever (until cancel) except Exception as e: self.logger.exception("taskgroup died.") util.send_exception_to_crash_reporter(e) finally: self.logger.info("taskgroup stopped.") # note: we could just "await self.stop()", but in that case GUI users would # not see the exception (especially if the GUI did not start yet). self._stopping_soon_or_errored.set() def start_network(self): self.logger.info(f"starting network.") assert not self.config.NETWORK_OFFLINE assert self.network self.network.start(jobs=[self.fx.run]) # prepare lightning functionality, also load channel db early if self.config.LIGHTNING_USE_GOSSIP: self.network.start_gossip() @staticmethod def _wallet_key_from_path(path) -> str: """This does stricter path standardization than 'standardize_path'. It is used for keying the _wallets dict, but MUST NOT be used as a *path* for the actual filesystem operations. (see #8495) """ path = standardize_path(path) # The extra normalisation makes it even harder to open the same wallet file multiple times simultaneously. # - "realpath" resolves symlinks: # note: the path returned by realpath has been observed NOT to work for FS operations! # (e.g. for Cryptomator WinFSP/FUSE mounts, see #8495). # It is okay for us to use it for computing a canonical wallet *key*, but cannot be used as a path! try: path = os.path.realpath(path, strict=False) except OSError as e: # see #10182 _logger.warning(f"could not parse {path!r}: {e!r}") path = path # - "normcase" does Windows-specific case and slash normalisation: path = os.path.normcase(path) # - prepend header to break usage of wallet keys as fs paths header = "WALLETKEY-" return header + str(path) def with_wallet_lock(func): def func_wrapper(self: 'Daemon', *args, **kwargs): with self._wallet_lock: return func(self, *args, **kwargs) return func_wrapper @with_wallet_lock def load_wallet( self, path, password: Optional[str], *, upgrade: bool = False, force_check_password: bool = False, ) -> Optional[Abstract_Wallet]: """ force_check_password: if False, the password arg is only used if it needed to decrypt the storage. if True, the password arg is always validated. """ assert password != '' path = standardize_path(path) wallet_key = self._wallet_key_from_path(path) # wizard will be launched if we return if wallet := self._wallets.get(wallet_key): if force_check_password: wallet.check_password(password) if self.config.get('wallet_path') is None: self.config.CURRENT_WALLET = path return wallet wallet = self._load_wallet( path, password, upgrade=upgrade, config=self.config, force_check_password=force_check_password) if self.network: wallet.start_network(self.network) elif wallet.lnworker: # in offline mode, we need to trigger callbacks coro = wallet.lnworker.lnwatcher.trigger_callbacks(requires_synchronizer=False) asyncio.run_coroutine_threadsafe(coro, self.asyncio_loop) self.add_wallet(wallet) if self.config.get('wallet_path') is None: self.config.CURRENT_WALLET = path self.update_recently_opened_wallets(path) return wallet @staticmethod @profiler def _load_wallet( path, password: Optional[str], *, upgrade: bool = False, config: SimpleConfig, force_check_password: bool = False, # if set, always validate password ) -> Optional[Abstract_Wallet]: path = standardize_path(path) storage = WalletStorage(path, allow_partial_writes=config.WALLET_PARTIAL_WRITES) if not storage.file_exists(): raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), path) if storage.is_encrypted(): if not password: raise InvalidPassword('No password given') storage.decrypt(password) # read data, pass it to db db = WalletDB(storage.read(), storage=storage, upgrade=upgrade) if db.get_action(): raise WalletUnfinished(db) wallet = Wallet(db, config=config) if force_check_password: wallet.check_password(password) return wallet @with_wallet_lock def add_wallet(self, wallet: Abstract_Wallet) -> None: path = wallet.storage.path wallet_key = self._wallet_key_from_path(path) self._wallets[wallet_key] = wallet run_hook('daemon_wallet_loaded', self, wallet) def get_wallet(self, path: str) -> Optional[Abstract_Wallet]: wallet_key = self._wallet_key_from_path(path) return self._wallets.get(wallet_key) @with_wallet_lock def get_wallets(self) -> Dict[str, Abstract_Wallet]: return dict(self._wallets) # copy def delete_wallet(self, path: str) -> bool: self.stop_wallet(path) if os.path.exists(path): os.unlink(path) self.update_recently_opened_wallets(path, remove=True) if self.config.CURRENT_WALLET == path: self.config.CURRENT_WALLET = None return True return False def stop_wallet(self, path: str) -> bool: """Returns True iff a wallet was found.""" assert util.get_running_loop() != util.get_asyncio_loop(), 'must not be called from asyncio thread' fut = asyncio.run_coroutine_threadsafe(self._stop_wallet(path), self.asyncio_loop) return fut.result() @with_wallet_lock async def _stop_wallet(self, path: str) -> bool: """Returns True iff a wallet was found.""" path = standardize_path(path) wallet_key = self._wallet_key_from_path(path) wallet = self._wallets.pop(wallet_key, None) if not wallet: return False await wallet.stop() if self.config.get('wallet_path') is None: wallet_paths = [w.db.storage.path for w in self._wallets.values() if w.db.storage and w.db.storage.path] if self.config.CURRENT_WALLET == path and wallet_paths: self.config.CURRENT_WALLET = wallet_paths[0] return True def run_daemon(self): if 'wallet_path' in self.config.cmdline_options: self.logger.warning("Ignoring parameter 'wallet_path' for daemon. " "Use the load_wallet command instead.") # init plugins self._plugins = Plugins(self.config, 'cmdline') # block until we are stopping try: self._stopping_soon_or_errored.wait() except KeyboardInterrupt: self.logger.info("got KeyboardInterrupt") # we either initiate shutdown now, # or it has already been initiated (in which case this is a no-op): self.logger.info("run_daemon is calling stop()") asyncio.run_coroutine_threadsafe(self.stop(), self.asyncio_loop).result() # wait until "stop" finishes: self._stopped_event.wait() async def stop(self): if self._stop_entered: return self._stop_entered = True self._stopping_soon_or_errored.set() self.logger.info("stop() entered. initiating shutdown") try: if self.gui_object: self.gui_object.stop() self.logger.info("stopping all wallets") async with OldTaskGroup() as group: for k, wallet in self._wallets.items(): await group.spawn(wallet.stop()) self.logger.info("stopping network and taskgroup") async with ignore_after(2): async with OldTaskGroup() as group: if self.network: await group.spawn(self.network.stop(full_shutdown=True)) await group.spawn(self.taskgroup.cancel_remaining()) if self._plugins: self.logger.info("stopping plugins") self._plugins.stop() async with ignore_after(1): await self._plugins.stopped_event_async.wait() finally: if self.listen_jsonrpc: self.logger.info("removing lockfile") remove_lockfile(get_lockfile(self.config)) self.logger.info("stopped") self._stopped_event.set() def run_gui(self) -> None: assert self.config threading.current_thread().name = 'GUI' gui_name = self.config.GUI_NAME if gui_name in ['lite', 'classic']: gui_name = 'qt' self._plugins = Plugins(self.config, gui_name) # init plugins self.logger.info(f'launching GUI: {gui_name}') try: try: gui = __import__('electrum.gui.' + gui_name, fromlist=['electrum']) except GuiImportError as e: sys.exit(str(e)) self.gui_object = gui.ElectrumGui(config=self.config, daemon=self, plugins=self._plugins) if not self._stop_entered: self.gui_object.main() else: # If daemon.stop() was called before gui_object got created, stop gui now. self.gui_object.stop() except BaseException as e: self.logger.error(f'GUI raised exception: {repr(e)}. shutting down.') raise finally: # app will exit now asyncio.run_coroutine_threadsafe(self.stop(), self.asyncio_loop).result() @with_wallet_lock def check_password_for_directory(self, *, old_password, new_password=None, wallet_dir: str) -> Tuple[bool, bool, list[str]]: """Checks password against all wallets (in dir), returns whether they can be unified and whether they are already. If new_password is not None, update all wallet passwords to new_password. """ assert os.path.exists(wallet_dir), f"path {wallet_dir!r} does not exist" succeeded = [] failed = [] is_unified = True for filename in os.listdir(wallet_dir): path = os.path.join(wallet_dir, filename) path = standardize_path(path) if not os.path.isfile(path): continue wallet = self.get_wallet(path) # note: we only create a new wallet object if one was not loaded into the daemon already. # This is to avoid having two wallet objects contending for the same file. # Take care: this only works if the daemon knows about all wallet objects. # if other code already has created a Wallet() for a file but did not tell the daemon, # hard-to-understand bugs will follow... if wallet is None: try: wallet = self._load_wallet(path, old_password, upgrade=True, config=self.config) except util.InvalidPassword: pass except Exception: self.logger.exception(f'failed to load wallet at {path!r}:') if wallet is None: failed.append(path) continue if not wallet.storage.is_encrypted(): is_unified = False try: try: wallet.check_password(old_password) old_password_real = old_password except util.InvalidPassword: wallet.check_password(None) old_password_real = None except Exception: failed.append(path) continue if new_password: self.logger.info(f'updating password for wallet: {path!r}') wallet.update_password(old_password_real, new_password, encrypt_storage=True) succeeded.append(path) can_be_unified = failed == [] is_unified = can_be_unified and is_unified return can_be_unified, is_unified, succeeded @with_wallet_lock def update_password_for_directory( self, *, old_password, new_password, wallet_dir: Optional[str] = None, ) -> bool: """returns whether password is unified""" if new_password is None: # we opened a non-encrypted wallet return False if wallet_dir is None: wallet_dir = os.path.dirname(self.config.get_wallet_path()) can_be_unified, is_unified, _ = self.check_password_for_directory( old_password=old_password, new_password=None, wallet_dir=wallet_dir) if not can_be_unified: return False if is_unified and old_password == new_password: return True self.check_password_for_directory( old_password=old_password, new_password=new_password, wallet_dir=wallet_dir) return True def update_recently_opened_wallets(self, wallet_path, *, remove: bool = False): recent = self.config.RECENTLY_OPEN_WALLET_FILES or [] if wallet_path in recent: recent.remove(wallet_path) if not remove: recent.insert(0, wallet_path) recent = [path for path in recent if os.path.exists(path)] recent = recent[:5] self.config.RECENTLY_OPEN_WALLET_FILES = recent util.trigger_callback('recently_opened_wallets_update') ================================================ FILE: electrum/descriptor.py ================================================ # Copyright (c) 2017 Andrew Chow # Copyright (c) 2023 The Electrum developers # Distributed under the MIT software license, see the accompanying # file LICENCE or http://www.opensource.org/licenses/mit-license.php # # forked from https://github.com/bitcoin-core/HWI/blob/5f300d3dee7b317a6194680ad293eaa0962a3cc7/hwilib/descriptor.py # # Output Script Descriptors # See https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md # # TODO allow xprv # TODO hardened derivation # TODO allow WIF privkeys # TODO impl ADDR descriptors # TODO impl RAW descriptors from binascii import unhexlify import enum from enum import Enum from typing import ( List, NamedTuple, Optional, Tuple, Sequence, Mapping, Set, Union, ) import electrum_ecc as ecc from .bip32 import convert_bip32_strpath_to_intpath, BIP32Node, KeyOriginInfo, BIP32_PRIME from . import bitcoin from .bitcoin import construct_script, opcodes, construct_witness, taproot_output_script from . import constants from .crypto import hash_160, sha256 from . import segwit_addr MAX_TAPROOT_DEPTH = 128 # we guess that signatures will be 72 bytes long # note: DER-encoded ECDSA signatures are 71 or 72 bytes in practice # See https://bitcoin.stackexchange.com/questions/77191/what-is-the-maximum-size-of-a-der-encoded-ecdsa-signature # We assume low S (as that is a bitcoin standardness rule). # We do not assume low R (even though the sigs we create conform), as external sigs, # e.g. from a hw signer cannot be expected to have a low R. DUMMY_DER_SIG = 72 * b"\x00" class ExpandedScripts: def __init__( self, *, output_script: bytes, # "scriptPubKey" redeem_script: Optional[bytes] = None, witness_script: Optional[bytes] = None, scriptcode_for_sighash: Optional[bytes] = None ): self.output_script = output_script self.redeem_script = redeem_script self.witness_script = witness_script self.scriptcode_for_sighash = scriptcode_for_sighash @property def scriptcode_for_sighash(self) -> Optional[bytes]: if self._scriptcode_for_sighash: return self._scriptcode_for_sighash return self.witness_script or self.redeem_script or self.output_script @scriptcode_for_sighash.setter def scriptcode_for_sighash(self, value: Optional[bytes]): self._scriptcode_for_sighash = value def address(self, *, net=None) -> Optional[str]: return bitcoin.script_to_address(self.output_script, net=net) class ScriptSolutionInner(NamedTuple): witness_items: Optional[Sequence] = None class ScriptSolutionTop(NamedTuple): witness: Optional[bytes] = None script_sig: Optional[bytes] = None class MissingSolutionPiece(Exception): pass def PolyMod(c: int, val: int) -> int: """ :meta private: Function to compute modulo over the polynomial used for descriptor checksums From: https://github.com/bitcoin/bitcoin/blob/master/src/script/descriptor.cpp """ c0 = c >> 35 c = ((c & 0x7ffffffff) << 5) ^ val if (c0 & 1): c ^= 0xf5dee51989 if (c0 & 2): c ^= 0xa9fdca3312 if (c0 & 4): c ^= 0x1bab10e32d if (c0 & 8): c ^= 0x3706b1677a if (c0 & 16): c ^= 0x644d626ffd return c _INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ " _INPUT_CHARSET_INV = {c: i for (i, c) in enumerate(_INPUT_CHARSET)} _CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" def DescriptorChecksum(desc: str) -> str: """ Compute the checksum for a descriptor :param desc: The descriptor string to compute a checksum for :return: A checksum """ c = 1 cls = 0 clscount = 0 for ch in desc: try: pos = _INPUT_CHARSET_INV[ch] except KeyError: return "" c = PolyMod(c, pos & 31) cls = cls * 3 + (pos >> 5) clscount += 1 if clscount == 3: c = PolyMod(c, cls) cls = 0 clscount = 0 if clscount > 0: c = PolyMod(c, cls) for j in range(0, 8): c = PolyMod(c, 0) c ^= 1 ret = [''] * 8 for j in range(0, 8): ret[j] = _CHECKSUM_CHARSET[(c >> (5 * (7 - j))) & 31] return ''.join(ret) def AddChecksum(desc: str) -> str: """ Compute and attach the checksum for a descriptor :param desc: The descriptor string to add a checksum to :return: Descriptor with checksum """ return desc + "#" + DescriptorChecksum(desc) class PubkeyProvider(object): """ A public key expression in a descriptor. Can contain the key origin info, the pubkey itself, and subsequent derivation paths for derivation from the pubkey The pubkey can be a typical pubkey or an extended pubkey. """ def __init__( self, origin: Optional['KeyOriginInfo'], pubkey: str, deriv_path: Optional[str] ) -> None: """ :param origin: The key origin if one is available :param pubkey: The public key. Either a hex string or a serialized extended pubkey :param deriv_path: Additional derivation path (suffix) if the pubkey is an extended pubkey """ self.origin = origin self.pubkey = pubkey self.deriv_path = deriv_path if deriv_path: wildcard_count = deriv_path.count("*") if wildcard_count > 1: raise ValueError("only one wildcard(*) is allowed in a descriptor") if wildcard_count == 1: if deriv_path[-1] != "*": raise ValueError("wildcard in descriptor only allowed in last position") if deriv_path[0] != "/": raise ValueError(f"deriv_path suffix must start with a '/'. got {deriv_path!r}") # Make ExtendedKey from pubkey if it isn't hex self.extkey = None try: unhexlify(self.pubkey) # Is hex, normal pubkey except Exception: # Not hex, maybe xpub (but don't allow ypub/zpub) self.extkey = BIP32Node.from_xkey(pubkey, allow_custom_headers=False) if deriv_path and self.extkey is None: raise ValueError("deriv_path suffix present for simple pubkey") @classmethod def parse(cls, s: str) -> 'PubkeyProvider': """ Deserialize a key expression from the string into a ``PubkeyProvider``. :param s: String containing the key expression :return: A new ``PubkeyProvider`` containing the details given by ``s`` """ origin = None deriv_path = None if s[0] == "[": end = s.index("]") origin = KeyOriginInfo.from_string(s[1:end]) s = s[end + 1:] pubkey = s slash_idx = s.find("/") if slash_idx != -1: pubkey = s[:slash_idx] deriv_path = s[slash_idx:] return cls(origin, pubkey, deriv_path) def to_string(self) -> str: """ Serialize the pubkey expression to a string to be used in a descriptor :return: The pubkey expression as a string """ s = "" if self.origin: s += "[{}]".format(self.origin.to_string()) s += self.pubkey if self.deriv_path: s += self.deriv_path return s def get_pubkey_bytes(self, *, pos: Optional[int] = None) -> bytes: if self.is_range() and pos is None: raise ValueError("pos must be set for ranged descriptor") # note: if not ranged, we ignore pos. if self.extkey is not None: compressed = True # bip32 implies compressed pubkeys if self.deriv_path is None: assert not self.is_range() return self.extkey.eckey.get_public_key_bytes(compressed=compressed) else: path_str = self.deriv_path[1:] if self.is_range(): assert path_str[-1] == "*" path_str = path_str[:-1] + str(pos) path = convert_bip32_strpath_to_intpath(path_str) child_key = self.extkey.subkey_at_public_derivation(path) return child_key.eckey.get_public_key_bytes(compressed=compressed) else: assert not self.is_range() return unhexlify(self.pubkey) def get_full_derivation_path(self, *, pos: Optional[int] = None) -> str: """ Returns the full derivation path at the given position, including the origin """ if self.is_range() and pos is None: raise ValueError("pos must be set for ranged descriptor") path = self.origin.get_derivation_path() if self.origin is not None else "m" path += self.deriv_path if self.deriv_path is not None else "" if path[-1] == "*": path = path[:-1] + str(pos) return path def get_full_derivation_int_list(self, *, pos: Optional[int] = None) -> List[int]: """ Returns the full derivation path as an integer list at the given position. Includes the origin and master key fingerprint as an int """ if self.is_range() and pos is None: raise ValueError("pos must be set for ranged descriptor") path: List[int] = self.origin.get_full_int_list() if self.origin is not None else [] path.extend(self.get_der_suffix_int_list(pos=pos)) return path def get_der_suffix_int_list(self, *, pos: Optional[int] = None) -> List[int]: if not self.deriv_path: return [] der_suffix = self.deriv_path assert (wc_count := der_suffix.count("*")) <= 1, wc_count der_suffix = der_suffix.replace("*", str(pos)) return convert_bip32_strpath_to_intpath(der_suffix) def __lt__(self, other: 'PubkeyProvider') -> bool: return self.pubkey < other.pubkey def is_range(self) -> bool: if not self.deriv_path: return False if self.deriv_path[-1] == "*": # TODO hardened return True return False def has_uncompressed_pubkey(self) -> bool: if self.is_range(): # bip32 implies compressed return False return b"\x04" == self.get_pubkey_bytes()[:1] class Descriptor(object): r""" An abstract class for Descriptors themselves. Descriptors can contain multiple :class:`PubkeyProvider`\ s and multiple ``Descriptor`` as subdescriptors. Note: a significant portion of input validation logic is in parse_descriptor(), maybe these checks should be moved to (or also done in) this class? For example, sh() must be top-level, or segwit mandates compressed pubkeys, or bare-multisig cannot have >3 pubkeys. """ def __init__( self, pubkeys: List['PubkeyProvider'], subdescriptors: List['Descriptor'], name: str ) -> None: r""" :param pubkeys: The :class:`PubkeyProvider`\ s that are part of this descriptor :param subdescriptor: The ``Descriptor``\ s that are part of this descriptor :param name: The name of the function for this descriptor """ self.pubkeys = pubkeys self.subdescriptors = subdescriptors self.name = name def to_string_no_checksum(self) -> str: """ Serializes the descriptor as a string without the descriptor checksum :return: The descriptor string """ return "{}({}{})".format( self.name, ",".join([p.to_string() for p in self.pubkeys]), self.subdescriptors[0].to_string_no_checksum() if len(self.subdescriptors) > 0 else "" ) def to_string(self) -> str: """ Serializes the descriptor as a string with the checksum :return: The descriptor with a checksum """ return AddChecksum(self.to_string_no_checksum()) def expand(self, *, pos: Optional[int] = None) -> "ExpandedScripts": """ Returns the scripts for a descriptor at the given `pos` for ranged descriptors. """ raise NotImplementedError("The Descriptor base class does not implement this method") def _satisfy_inner( self, *, sigdata: Mapping[bytes, bytes] = None, # pubkey -> sig allow_dummy: bool = False, ) -> ScriptSolutionInner: raise NotImplementedError("The Descriptor base class does not implement this method") def satisfy( self, *, sigdata: Mapping[bytes, bytes] = None, # pubkey -> sig allow_dummy: bool = False, ) -> ScriptSolutionTop: """Construct a witness and/or scriptSig to be used in a txin, to satisfy the bitcoin SCRIPT. Raises MissingSolutionPiece if satisfaction is not yet possible due to e.g. missing a signature, unless `allow_dummy` is set to True, in which case dummy data is used where needed (e.g. for size estimation). """ assert not self.is_range() sol = self._satisfy_inner(sigdata=sigdata, allow_dummy=allow_dummy) witness = None script_sig = None if self.is_segwit(): witness = construct_witness(sol.witness_items) else: script_sig = construct_script(sol.witness_items) return ScriptSolutionTop( witness=witness, script_sig=script_sig, ) def get_satisfaction_progress( self, *, sigdata: Mapping[bytes, bytes] = None, # pubkey -> sig ) -> Tuple[int, int]: """Returns (num_sigs_we_have, num_sigs_required) towards satisfying this script. Besides signatures, later this can also consider hash-preimages. """ assert not self.is_range() nhave, nreq = 0, 0 for desc in self.subdescriptors: a, b = desc.get_satisfaction_progress(sigdata=sigdata) nhave += a nreq += b return nhave, nreq def is_range(self) -> bool: for pubkey in self.pubkeys: if pubkey.is_range(): return True for desc in self.subdescriptors: if desc.is_range(): return True return False def is_segwit(self) -> bool: return any([desc.is_segwit() for desc in self.subdescriptors]) def is_taproot(self) -> bool: return False def get_all_pubkeys(self) -> Set[bytes]: """Returns set of pubkeys that appear at any level in this descriptor.""" assert not self.is_range() all_pubkeys = set([p.get_pubkey_bytes() for p in self.pubkeys]) for desc in self.subdescriptors: all_pubkeys |= desc.get_all_pubkeys() return all_pubkeys def get_simple_singlesig(self) -> Optional['Descriptor']: """Returns innermost pk/pkh/wpkh descriptor, or None if we are not a simple singlesig. note: besides pk,pkh,sh(wpkh),wpkh, overly complicated stuff such as sh(pk),wsh(sh(pkh),etc is also accepted """ if len(self.subdescriptors) == 1: return self.subdescriptors[0].get_simple_singlesig() return None def get_simple_multisig(self) -> Optional['MultisigDescriptor']: """Returns innermost multi descriptor, or None if we are not a simple multisig.""" if len(self.subdescriptors) == 1: return self.subdescriptors[0].get_simple_multisig() return None def to_legacy_electrum_script_type(self) -> str: if isinstance(self, PKDescriptor): return "p2pk" elif isinstance(self, PKHDescriptor): return "p2pkh" elif isinstance(self, WPKHDescriptor): return "p2wpkh" elif isinstance(self, SHDescriptor) and isinstance(self.subdescriptors[0], WPKHDescriptor): return "p2wpkh-p2sh" elif isinstance(self, SHDescriptor) and isinstance(self.subdescriptors[0], MultisigDescriptor): return "p2sh" elif isinstance(self, WSHDescriptor) and isinstance(self.subdescriptors[0], MultisigDescriptor): return "p2wsh" elif (isinstance(self, SHDescriptor) and isinstance(self.subdescriptors[0], WSHDescriptor) and isinstance(self.subdescriptors[0].subdescriptors[0], MultisigDescriptor)): return "p2wsh-p2sh" return "unknown" class PKDescriptor(Descriptor): """ A descriptor for ``pk()`` descriptors """ def __init__( self, pubkey: 'PubkeyProvider' ) -> None: """ :param pubkey: The :class:`PubkeyProvider` for this descriptor """ super().__init__([pubkey], [], "pk") def expand(self, *, pos: Optional[int] = None) -> "ExpandedScripts": pubkey = self.pubkeys[0].get_pubkey_bytes(pos=pos) script = construct_script([pubkey, opcodes.OP_CHECKSIG]) return ExpandedScripts(output_script=script) def _satisfy_inner(self, *, sigdata=None, allow_dummy=False) -> ScriptSolutionInner: if sigdata is None: sigdata = {} assert not self.is_range() assert not self.subdescriptors pubkey = self.pubkeys[0].get_pubkey_bytes() sig = sigdata.get(pubkey) if sig is None and allow_dummy: sig = DUMMY_DER_SIG if sig is None: raise MissingSolutionPiece(f"no sig for {pubkey.hex()}") return ScriptSolutionInner( witness_items=(sig,), ) def get_satisfaction_progress(self, *, sigdata=None) -> Tuple[int, int]: if sigdata is None: sigdata = {} signatures = list(sigdata.values()) return len(signatures), 1 def get_simple_singlesig(self) -> Optional['Descriptor']: return self class PKHDescriptor(Descriptor): """ A descriptor for ``pkh()`` descriptors """ def __init__( self, pubkey: 'PubkeyProvider' ) -> None: """ :param pubkey: The :class:`PubkeyProvider` for this descriptor """ super().__init__([pubkey], [], "pkh") def expand(self, *, pos: Optional[int] = None) -> "ExpandedScripts": pubkey = self.pubkeys[0].get_pubkey_bytes(pos=pos) pkh = hash_160(pubkey) script = bitcoin.pubkeyhash_to_p2pkh_script(pkh) return ExpandedScripts(output_script=script) def _satisfy_inner(self, *, sigdata=None, allow_dummy=False) -> ScriptSolutionInner: if sigdata is None: sigdata = {} assert not self.is_range() assert not self.subdescriptors pubkey = self.pubkeys[0].get_pubkey_bytes() sig = sigdata.get(pubkey) if sig is None and allow_dummy: sig = DUMMY_DER_SIG if sig is None: raise MissingSolutionPiece(f"no sig for {pubkey.hex()}") return ScriptSolutionInner( witness_items=(sig, pubkey), ) def get_satisfaction_progress(self, *, sigdata=None) -> Tuple[int, int]: if sigdata is None: sigdata = {} signatures = list(sigdata.values()) return len(signatures), 1 def get_simple_singlesig(self) -> Optional['Descriptor']: return self class WPKHDescriptor(Descriptor): """ A descriptor for ``wpkh()`` descriptors """ def __init__( self, pubkey: 'PubkeyProvider' ) -> None: """ :param pubkey: The :class:`PubkeyProvider` for this descriptor """ super().__init__([pubkey], [], "wpkh") def expand(self, *, pos: Optional[int] = None) -> "ExpandedScripts": pkh = hash_160(self.pubkeys[0].get_pubkey_bytes(pos=pos)) output_script = construct_script([0, pkh]) scriptcode = bitcoin.pubkeyhash_to_p2pkh_script(pkh) return ExpandedScripts( output_script=output_script, scriptcode_for_sighash=scriptcode, ) def _satisfy_inner(self, *, sigdata=None, allow_dummy=False) -> ScriptSolutionInner: if sigdata is None: sigdata = {} assert not self.is_range() assert not self.subdescriptors pubkey = self.pubkeys[0].get_pubkey_bytes() sig = sigdata.get(pubkey) if sig is None and allow_dummy: sig = DUMMY_DER_SIG if sig is None: raise MissingSolutionPiece(f"no sig for {pubkey.hex()}") return ScriptSolutionInner( witness_items=(sig, pubkey), ) def get_satisfaction_progress(self, *, sigdata=None) -> Tuple[int, int]: if sigdata is None: sigdata = {} signatures = list(sigdata.values()) return len(signatures), 1 def is_segwit(self) -> bool: return True def get_simple_singlesig(self) -> Optional['Descriptor']: return self class MultisigDescriptor(Descriptor): """ A descriptor for ``multi()`` and ``sortedmulti()`` descriptors """ def __init__( self, pubkeys: List['PubkeyProvider'], thresh: int, is_sorted: bool ) -> None: r""" :param pubkeys: The :class:`PubkeyProvider`\ s for this descriptor :param thresh: The number of keys required to sign this multisig :param is_sorted: Whether this is a ``sortedmulti()`` descriptor """ super().__init__(pubkeys, [], "sortedmulti" if is_sorted else "multi") if not (1 <= thresh <= len(pubkeys) <= 15): raise ValueError(f'{thresh=}, {len(pubkeys)=}') self.thresh = thresh self.is_sorted = is_sorted if self.is_sorted: if not self.is_range(): # sort xpubs using the order of pubkeys der_pks = [p.get_pubkey_bytes() for p in self.pubkeys] self.pubkeys = [x[1] for x in sorted(zip(der_pks, self.pubkeys))] else: # not possible to sort according to final order in expanded scripts, # but for easier visual comparison, we do a lexicographical sort self.pubkeys.sort() def to_string_no_checksum(self) -> str: return "{}({},{})".format(self.name, self.thresh, ",".join([p.to_string() for p in self.pubkeys])) def expand(self, *, pos: Optional[int] = None) -> "ExpandedScripts": der_pks = [p.get_pubkey_bytes(pos=pos) for p in self.pubkeys] if self.is_sorted: der_pks.sort() script = construct_script([self.thresh, *der_pks, len(der_pks), opcodes.OP_CHECKMULTISIG]) return ExpandedScripts(output_script=script) def _satisfy_inner(self, *, sigdata=None, allow_dummy=False) -> ScriptSolutionInner: if sigdata is None: sigdata = {} assert not self.is_range() assert not self.subdescriptors der_pks = [p.get_pubkey_bytes() for p in self.pubkeys] if self.is_sorted: der_pks.sort() signatures = [] for pubkey in der_pks: if sig := sigdata.get(pubkey): signatures.append(sig) if len(signatures) >= self.thresh: break if allow_dummy: dummy_sig = DUMMY_DER_SIG signatures += (self.thresh - len(signatures)) * [dummy_sig] if len(signatures) < self.thresh: raise MissingSolutionPiece(f"not enough sigs") assert len(signatures) == self.thresh, f"thresh={self.thresh}, but got {len(signatures)} sigs" return ScriptSolutionInner( witness_items=(0, *signatures), ) def get_satisfaction_progress(self, *, sigdata=None) -> Tuple[int, int]: if sigdata is None: sigdata = {} signatures = list(sigdata.values()) return len(signatures), self.thresh def get_simple_multisig(self) -> Optional['MultisigDescriptor']: return self class SHDescriptor(Descriptor): """ A descriptor for ``sh()`` descriptors """ def __init__( self, subdescriptor: 'Descriptor' ) -> None: """ :param subdescriptor: The :class:`Descriptor` that is a sub-descriptor for this descriptor """ super().__init__([], [subdescriptor], "sh") def expand(self, *, pos: Optional[int] = None) -> "ExpandedScripts": assert len(self.subdescriptors) == 1 sub_scripts = self.subdescriptors[0].expand(pos=pos) redeem_script = sub_scripts.output_script witness_script = sub_scripts.witness_script script = construct_script([opcodes.OP_HASH160, hash_160(redeem_script), opcodes.OP_EQUAL]) return ExpandedScripts( output_script=script, redeem_script=redeem_script, witness_script=witness_script, scriptcode_for_sighash=sub_scripts.scriptcode_for_sighash, ) def _satisfy_inner(self, *, sigdata=None, allow_dummy=False) -> ScriptSolutionInner: raise Exception("does not make sense for sh()") def satisfy(self, *, sigdata=None, allow_dummy=False) -> ScriptSolutionTop: assert not self.is_range() assert len(self.subdescriptors) == 1 subdesc = self.subdescriptors[0] redeem_script = self.expand().redeem_script witness = None if isinstance(subdesc, (WSHDescriptor, WPKHDescriptor)): # witness_v0 nested in p2sh witness = subdesc.satisfy(sigdata=sigdata, allow_dummy=allow_dummy).witness script_sig = construct_script([redeem_script]) else: # legacy p2sh subsol = subdesc._satisfy_inner(sigdata=sigdata, allow_dummy=allow_dummy) script_sig = construct_script([*subsol.witness_items, redeem_script]) return ScriptSolutionTop( witness=witness, script_sig=script_sig, ) class WSHDescriptor(Descriptor): """ A descriptor for ``wsh()`` descriptors """ def __init__( self, subdescriptor: 'Descriptor' ) -> None: """ :param subdescriptor: The :class:`Descriptor` that is a sub-descriptor for this descriptor """ super().__init__([], [subdescriptor], "wsh") def expand(self, *, pos: Optional[int] = None) -> "ExpandedScripts": assert len(self.subdescriptors) == 1 sub_scripts = self.subdescriptors[0].expand(pos=pos) witness_script = sub_scripts.output_script output_script = construct_script([0, sha256(witness_script)]) return ExpandedScripts( output_script=output_script, witness_script=witness_script, ) def _satisfy_inner(self, *, sigdata=None, allow_dummy=False) -> ScriptSolutionInner: raise Exception("does not make sense for wsh()") def satisfy(self, *, sigdata=None, allow_dummy=False) -> ScriptSolutionTop: assert not self.is_range() assert len(self.subdescriptors) == 1 subsol = self.subdescriptors[0]._satisfy_inner(sigdata=sigdata, allow_dummy=allow_dummy) witness_script = self.expand().witness_script witness = construct_witness([*subsol.witness_items, witness_script]) return ScriptSolutionTop( witness=witness, ) def is_segwit(self) -> bool: return True class TRDescriptor(Descriptor): """ A descriptor for ``tr()`` descriptors """ def __init__( self, internal_key: 'PubkeyProvider', desc_tree: List[Union['Descriptor', List]] = None, ) -> None: r""" :param internal_key: The :class:`PubkeyProvider` that is the internal key for this descriptor :param desc_tree: Taproot script binary tree, as a nested list of Descriptors """ if desc_tree is None: desc_tree = [] self.desc_tree = desc_tree desc_list = [] if desc_tree: if self.get_max_tree_depth() > MAX_TAPROOT_DEPTH: raise ValueError(f"tr() supports at most {MAX_TAPROOT_DEPTH} nesting levels") def flatten(tree_node): if isinstance(tree_node, Descriptor): return [tree_node] assert len(tree_node) == 2, len(tree_node) return flatten(tree_node[0]) + flatten(tree_node[1]) desc_list = flatten(desc_tree) super().__init__( pubkeys=[internal_key], subdescriptors=desc_list, # FIXME we could do without the flattened list (dupl) name="tr", ) def to_string_no_checksum(self) -> str: ret = f"{self.name}({self.pubkeys[0].to_string()}" if self.desc_tree: ret += "," def tree_to_str(tree_node): if isinstance(tree_node, Descriptor): return tree_node.to_string_no_checksum() assert len(tree_node) == 2, len(tree_node) return "{" + tree_to_str(tree_node[0]) + "," + tree_to_str(tree_node[1]) + "}" ret += tree_to_str(self.desc_tree) ret += ")" return ret def is_segwit(self) -> bool: return True def is_taproot(self) -> bool: return True # TODO add more test vectors from BIP-0386 def expand(self, *, pos: Optional[int] = None) -> "ExpandedScripts": internal_pubkey = self.pubkeys[0].get_pubkey_bytes(pos=pos) script_tree = None if self.desc_tree: def transform(tree_node): if isinstance(tree_node, Descriptor): leaf_version = 0xc0 leaf_script = tree_node.expand(pos=pos).scriptcode_for_sighash # FIXME maybe rename scriptcode_for_sighash return (leaf_version, leaf_script) assert len(tree_node) == 2, len(tree_node) return [transform(tree_node[0]), transform(tree_node[1])] script_tree = transform(self.desc_tree) output_script = taproot_output_script(internal_pubkey, script_tree=script_tree) return ExpandedScripts( output_script=output_script, ) def get_max_tree_depth(self) -> Optional[int]: if not self.desc_tree: return None def depth(tree_node) -> int: if isinstance(tree_node, Descriptor): return 0 assert len(tree_node) == 2, len(tree_node) return 1 + max(depth(tree_node[0]), depth(tree_node[1])) return depth(self.desc_tree) def _get_func_expr(s: str) -> Tuple[str, str]: """ Get the function name and then the expression inside :param s: The string that begins with a function name :return: The function name as the first element of the tuple, and the expression contained within the function as the second element :raises: ValueError: if a matching pair of parentheses cannot be found """ try: start = s.index("(") end = s.rindex(")") return s[0:start], s[start + 1:end] except ValueError: raise ValueError("A matching pair of parentheses cannot be found") def _get_const(s: str, const: str) -> str: """ Get the first character of the string, make sure it is the expected character, and return the rest of the string :param s: The string that begins with a constant character :param const: The constant character :return: The remainder of the string without the constant character :raises: ValueError: if the first character is not the constant character """ if s[0] != const: raise ValueError(f"Expected '{const}' but got '{s[0]}'") return s[1:] def _get_expr(s: str) -> Tuple[str, str]: """ Extract the expression that ``s`` begins with. This will return the initial part of ``s``, up to the first comma or closing brace, skipping ones that are surrounded by braces. :param s: The string to extract the expression from :return: A pair with the first item being the extracted expression and the second the rest of the string """ level: int = 0 for i, c in enumerate(s): if c in ["(", "{"]: level += 1 elif level > 0 and c in [")", "}"]: level -= 1 elif level == 0 and c in [")", "}", ","]: break else: return s, "" return s[0:i], s[i:] def parse_pubkey(expr: str, *, ctx: '_ParseDescriptorContext') -> Tuple['PubkeyProvider', str]: """ Parses an individual pubkey expression from a string that may contain more than one pubkey expression. :param expr: The expression to parse a pubkey expression from :return: The :class:`PubkeyProvider` that is parsed as the first item of a tuple, and the remainder of the expression as the second item. """ end = len(expr) comma_idx = expr.find(",") next_expr = "" if comma_idx != -1: end = comma_idx next_expr = expr[end + 1:] pubkey_provider = PubkeyProvider.parse(expr[:end]) permit_uncompressed = ctx in (_ParseDescriptorContext.TOP, _ParseDescriptorContext.P2SH) if not permit_uncompressed and pubkey_provider.has_uncompressed_pubkey(): raise ValueError("uncompressed pubkeys are not allowed") return pubkey_provider, next_expr class _ParseDescriptorContext(Enum): """ :meta private: Enum representing the level that we are in when parsing a descriptor. Some expressions aren't allowed at certain levels, this helps us track those. """ TOP = enum.auto() # The top level, not within any descriptor P2SH = enum.auto() # Within an sh() descriptor P2WPKH = enum.auto() # Within wpkh() descriptor P2WSH = enum.auto() # Within a wsh() descriptor P2TR = enum.auto() # Within a tr() descriptor def _parse_descriptor(desc: str, *, ctx: '_ParseDescriptorContext') -> 'Descriptor': """ :meta private: Parse a descriptor given the context level we are in. Used recursively to parse subdescriptors :param desc: The descriptor string to parse :param ctx: The :class:`_ParseDescriptorContext` indicating the level we are in :return: The parsed descriptor :raises: ValueError: if the descriptor is malformed """ func, expr = _get_func_expr(desc) if func == "pk": pubkey, expr = parse_pubkey(expr, ctx=ctx) if expr: raise ValueError("more than one pubkey in pk descriptor") return PKDescriptor(pubkey) if func == "pkh": if not (ctx == _ParseDescriptorContext.TOP or ctx == _ParseDescriptorContext.P2SH or ctx == _ParseDescriptorContext.P2WSH): raise ValueError("Can only have pkh at top level, in sh(), or in wsh()") pubkey, expr = parse_pubkey(expr, ctx=ctx) if expr: raise ValueError("More than one pubkey in pkh descriptor") return PKHDescriptor(pubkey) if func == "sortedmulti" or func == "multi": if not (ctx == _ParseDescriptorContext.TOP or ctx == _ParseDescriptorContext.P2SH or ctx == _ParseDescriptorContext.P2WSH): raise ValueError("Can only have multi/sortedmulti at top level, in sh(), or in wsh()") is_sorted = func == "sortedmulti" comma_idx = expr.index(",") thresh = int(expr[:comma_idx]) expr = expr[comma_idx + 1:] pubkeys = [] while expr: pubkey, expr = parse_pubkey(expr, ctx=ctx) pubkeys.append(pubkey) if len(pubkeys) == 0 or len(pubkeys) > 15: raise ValueError("Cannot have {} keys in a multisig; must have between 1 and 15 keys, inclusive".format(len(pubkeys))) elif thresh < 1: raise ValueError("Multisig threshold cannot be {}, must be at least 1".format(thresh)) elif thresh > len(pubkeys): raise ValueError("Multisig threshold cannot be larger than the number of keys; threshold is {} but only {} keys specified".format(thresh, len(pubkeys))) if ctx == _ParseDescriptorContext.TOP and len(pubkeys) > 3: raise ValueError("Cannot have {} pubkeys in bare multisig: only at most 3 pubkeys") return MultisigDescriptor(pubkeys, thresh, is_sorted) if func == "wpkh": if not (ctx == _ParseDescriptorContext.TOP or ctx == _ParseDescriptorContext.P2SH): raise ValueError("Can only have wpkh() at top level or inside sh()") pubkey, expr = parse_pubkey(expr, ctx=_ParseDescriptorContext.P2WPKH) if expr: raise ValueError("More than one pubkey in pkh descriptor") return WPKHDescriptor(pubkey) if func == "sh": if ctx != _ParseDescriptorContext.TOP: raise ValueError("Can only have sh() at top level") subdesc = _parse_descriptor(expr, ctx=_ParseDescriptorContext.P2SH) return SHDescriptor(subdesc) if func == "wsh": if not (ctx == _ParseDescriptorContext.TOP or ctx == _ParseDescriptorContext.P2SH): raise ValueError("Can only have wsh() at top level or inside sh()") subdesc = _parse_descriptor(expr, ctx=_ParseDescriptorContext.P2WSH) return WSHDescriptor(subdesc) if func == "tr": if ctx != _ParseDescriptorContext.TOP: raise ValueError("Can only have tr at top level") internal_key, expr = parse_pubkey(expr, ctx=ctx) desc_tree = [] if expr: def parse_tree(tree_str): if len(tree_str) == 0: raise ValueError("Invalid Taproot tree expression") if tree_str[0] != "{": # leaf sarg, remaining = _get_expr(tree_str) return _parse_descriptor(sarg, ctx=_ParseDescriptorContext.P2TR), remaining if len(tree_str) < len("{x,y}") or tree_str[-1] != "}": raise ValueError("Invalid Taproot tree expression") left, remaining = parse_tree(tree_str[1:]) if remaining[0] != ",": raise ValueError right, remaining = parse_tree(remaining[1:]) if remaining[0] != "}": raise ValueError return [left, right], remaining[1:] desc_tree, _remaining = parse_tree(expr) if len(_remaining) != 0: raise ValueError return TRDescriptor(internal_key, desc_tree) if ctx == _ParseDescriptorContext.P2SH: raise ValueError("A function is needed within P2SH") elif ctx == _ParseDescriptorContext.P2WSH: raise ValueError("A function is needed within P2WSH") raise ValueError("{} is not a valid descriptor function".format(func)) def parse_descriptor(desc: str) -> 'Descriptor': """ Parse a descriptor string into a :class:`Descriptor`. Validates the checksum if one is provided in the string :param desc: The descriptor string :return: The parsed :class:`Descriptor` :raises: ValueError: if the descriptor string is malformed """ i = desc.find("#") if i != -1: checksum = desc[i + 1:] desc = desc[:i] computed = DescriptorChecksum(desc) if computed != checksum: raise ValueError("The checksum does not match; Got {}, expected {}".format(checksum, computed)) return _parse_descriptor(desc, ctx=_ParseDescriptorContext.TOP) ##### class NotLegacySinglesigScriptType(Exception): pass def get_singlesig_descriptor_from_legacy_leaf(*, pubkey: str, script_type: str) -> Optional[Descriptor]: pubkey = PubkeyProvider.parse(pubkey) if script_type == 'p2pk': return PKDescriptor(pubkey=pubkey) elif script_type == 'p2pkh': return PKHDescriptor(pubkey=pubkey) elif script_type == 'p2wpkh': return WPKHDescriptor(pubkey=pubkey) elif script_type == 'p2wpkh-p2sh': wpkh = WPKHDescriptor(pubkey=pubkey) return SHDescriptor(subdescriptor=wpkh) else: raise NotLegacySinglesigScriptType(f"unexpected {script_type=}") def create_dummy_descriptor_from_address(addr: Optional[str]) -> 'Descriptor': # It's not possible to tell the script type in general just from an address. # - "1" addresses are of course p2pkh # - "3" addresses are p2sh but we don't know the redeem script... # - "bc1" addresses (if they are 42-long) are p2wpkh # - "bc1" addresses that are 62-long are p2wsh but we don't know the script... # If we don't know the script, we _guess_ it is pubkeyhash. # As this method is used e.g. for tx size estimation, # the estimation will not be precise. def guess_script_type(addr: Optional[str]) -> str: if addr is None: return 'p2wpkh' # the default guess witver, witprog = segwit_addr.decode_segwit_address(constants.net.SEGWIT_HRP, addr) if witprog is not None: return 'p2wpkh' addrtype, hash_160_ = bitcoin.b58_address_to_hash160(addr) if addrtype == constants.net.ADDRTYPE_P2PKH: return 'p2pkh' elif addrtype == constants.net.ADDRTYPE_P2SH: return 'p2wpkh-p2sh' raise Exception(f'unrecognized address: {repr(addr)}') script_type = guess_script_type(addr) # guess pubkey-len to be 33-bytes: pubkey = ecc.GENERATOR.get_public_key_bytes(compressed=True).hex() desc = get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey, script_type=script_type) return desc ================================================ FILE: electrum/dns_hacks.py ================================================ # Copyright (C) 2020 The Electrum developers # Distributed under the MIT software license, see the accompanying # file LICENCE or http://www.opensource.org/licenses/mit-license.php import sys import socket import concurrent from concurrent import futures import ipaddress import asyncio import dns import dns.asyncresolver from .logging import get_logger from .util import get_asyncio_loop from . import util _logger = get_logger(__name__) def configure_dns_resolver() -> None: # Store this somewhere so we can un-monkey-patch: if not hasattr(socket, "_getaddrinfo"): socket._getaddrinfo = socket.getaddrinfo if sys.platform == 'win32': # On Windows, socket.getaddrinfo takes a mutex, and might hold it for up to 10 seconds # when dns-resolving. To speed it up drastically, we resolve dns ourselves, outside that lock. # See https://github.com/spesmilo/electrum/issues/4421 try: _prepare_windows_dns_hack() except Exception as e: _logger.exception('failed to apply windows dns hack.') else: socket.getaddrinfo = _fast_getaddrinfo def _prepare_windows_dns_hack(): # enable dns cache resolver = dns.asyncresolver.get_default_resolver() if resolver.cache is None: resolver.cache = dns.resolver.Cache() # ensure overall timeout for requests is long enough resolver.lifetime = max(resolver.lifetime or 1, 30.0) def _is_force_system_dns_for_host(host: str) -> bool: return str(host) in ('localhost', 'localhost.',) def _fast_getaddrinfo(host, *args, **kwargs): def needs_dns_resolving(host): try: ipaddress.ip_address(host) return False # already valid IP except ValueError: pass # not an IP if _is_force_system_dns_for_host(host): return False return True def resolve_with_dnspython(host): addrs = [] expected_errors = (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, concurrent.futures.CancelledError, concurrent.futures.TimeoutError) loop = get_asyncio_loop() assert util.get_running_loop() != loop, 'must not be called from asyncio thread' ipv6_fut = asyncio.run_coroutine_threadsafe( dns.asyncresolver.resolve(host, dns.rdatatype.AAAA), loop, ) ipv4_fut = asyncio.run_coroutine_threadsafe( dns.asyncresolver.resolve(host, dns.rdatatype.A), loop, ) # try IPv6 try: answers = ipv6_fut.result() addrs += [str(answer) for answer in answers] except expected_errors as e: pass except BaseException as e: _logger.info(f'dnspython failed to resolve dns (AAAA) for {repr(host)} with error: {repr(e)}') # try IPv4 try: answers = ipv4_fut.result() addrs += [str(answer) for answer in answers] except expected_errors as e: # dns failed for some reason, e.g. dns.resolver.NXDOMAIN this is normal. # Simply report back failure; except if we already have some results. if not addrs: raise socket.gaierror(11001, 'getaddrinfo failed') from e except BaseException as e: # Possibly internal error in dnspython :( see #4483 and #5638 _logger.info(f'dnspython failed to resolve dns (A) for {repr(host)} with error: {repr(e)}') if addrs: return addrs # Fall back to original socket.getaddrinfo to resolve dns. return [host] addrs = [host] if needs_dns_resolving(host): addrs = resolve_with_dnspython(host) list_of_list_of_socketinfos = [socket._getaddrinfo(addr, *args, **kwargs) for addr in addrs] list_of_socketinfos = [item for lst in list_of_list_of_socketinfos for item in lst] return list_of_socketinfos ================================================ FILE: electrum/dnssec.py ================================================ #!/usr/bin/env python # # Electrum - lightweight Bitcoin client # Copyright (C) 2015 Thomas Voegtlin # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files # (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, # publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # Check DNSSEC trust chain. # Todo: verify expiration dates # # Based on # http://backreference.org/2010/11/17/dnssec-verification-with-dig/ # https://github.com/rthalley/dnspython/blob/master/tests/test_dnssec.py import logging import dns import dns.name import dns.asyncquery import dns.dnssec import dns.message import dns.asyncresolver import dns.rdatatype import dns.rdtypes.ANY.NS import dns.rdtypes.ANY.CNAME import dns.rdtypes.ANY.DLV import dns.rdtypes.ANY.DNSKEY import dns.rdtypes.ANY.DS import dns.rdtypes.ANY.NSEC import dns.rdtypes.ANY.NSEC3 import dns.rdtypes.ANY.NSEC3PARAM import dns.rdtypes.ANY.RRSIG import dns.rdtypes.ANY.SOA import dns.rdtypes.ANY.TXT import dns.rdtypes.IN.A import dns.rdtypes.IN.AAAA from .logging import get_logger from typing import Tuple _logger = get_logger(__name__) # hard-coded trust anchors (root KSKs) trust_anchors = [ # KSK-2017: dns.rrset.from_text('.', 1 , 'IN', 'DNSKEY', '257 3 8 AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3+/4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kvArMtNROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF0jLHwVN8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+eoZG+SrDK6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfdRUfhHdY6+cn8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwNR1AkUTV74bU='), # KSK-2010: dns.rrset.from_text('.', 15202, 'IN', 'DNSKEY', '257 3 8 AwEAAagAIKlVZrpC6Ia7gEzahOR+9W29euxhJhVVLOyQbSEW0O8gcCjF FVQUTf6v58fLjwBd0YI0EzrAcQqBGCzh/RStIoO8g0NfnfL2MTJRkxoX bfDaUeVPQuYEhg37NZWAJQ9VnMVDxP/VHL496M/QZxkjf5/Efucp2gaD X6RS6CXpoY68LsvPVjR0ZSwzz1apAzvN9dlzEheX7ICJBBtuA6G3LQpz W5hOA2hzCTMjJPJ8LbqF6dsV6DoBQzgul0sGIcGOYl7OyQdXfZ57relS Qageu+ipAdTTJ25AsRTAoub8ONGcLmqrAmRLKBP1dfwhYB4N7knNnulq QxA+Uk1ihz0='), ] async def _check_query(ns, sub, _type, keys) -> dns.rrset.RRset: q = dns.message.make_query(sub, _type, want_dnssec=True) response = await dns.asyncquery.tcp(q, ns, timeout=5) assert response.rcode() == 0, 'No answer' answer = response.answer assert len(answer) != 0, ('No DNS record found', sub, _type) assert len(answer) != 1, ('No DNSSEC record found', sub, _type) if answer[0].rdtype == dns.rdatatype.RRSIG: rrsig, rrset = answer elif answer[1].rdtype == dns.rdatatype.RRSIG: rrset, rrsig = answer else: raise Exception('No signature set in record') if keys is None: keys = {dns.name.from_text(sub):rrset} dns.dnssec.validate(rrset, rrsig, keys) return rrset async def _get_and_validate(ns, url, _type) -> dns.rrset.RRset: # get trusted root key root_rrset = None for dnskey_rr in trust_anchors: try: # Check if there is a valid signature for the root dnskey root_rrset = await _check_query(ns, '', dns.rdatatype.DNSKEY, {dns.name.root: dnskey_rr}) break except dns.dnssec.ValidationFailure: # It's OK as long as one key validates continue if not root_rrset: raise dns.dnssec.ValidationFailure('None of the trust anchors found in DNS') keys = {dns.name.root: root_rrset} # top-down verification parts = url.split('.') for i in range(len(parts), 0, -1): sub = '.'.join(parts[i-1:]) name = dns.name.from_text(sub) # If server is authoritative, don't fetch DNSKEY query = dns.message.make_query(sub, dns.rdatatype.NS) response = await dns.asyncquery.udp(query, ns, 3) assert response.rcode() == dns.rcode.NOERROR, "query error" rrset = response.authority[0] if len(response.authority) > 0 else response.answer[0] rr = rrset[0] if rr.rdtype == dns.rdatatype.SOA: continue # get DNSKEY (self-signed) rrset = await _check_query(ns, sub, dns.rdatatype.DNSKEY, None) # get DS (signed by parent) ds_rrset = await _check_query(ns, sub, dns.rdatatype.DS, keys) # verify that a signed DS validates DNSKEY for ds in ds_rrset: for dnskey in rrset: htype = 'SHA256' if ds.digest_type == 2 else 'SHA1' good_ds = dns.dnssec.make_ds(name, dnskey, htype) if ds == good_ds: break else: continue break else: raise Exception("DS does not match DNSKEY") # set key for next iteration keys = {name: rrset} # get TXT record (signed by zone) rrset = await _check_query(ns, url, _type, keys) return rrset async def query(url: str, rtype: dns.rdatatype.RdataType) -> Tuple[dns.rrset.RRset, bool]: """Try to do DNS resolution, including DNSSEC. 'validated' shows whether the DNSSEC checks passed. DNS is completely INSECURE without DNSSEC, so the caller must carefully consider whether the response can be used for anything if validated=False. """ # FIXME this method is not using the network proxy. (although the proxy might not support UDP?) # 8.8.8.8 is Google's public DNS server nameservers = ['8.8.8.8'] ns = nameservers[0] try: out = await _get_and_validate(ns, url, rtype) validated = True except Exception as e: log_level = logging.WARNING if isinstance(e, ImportError) else logging.INFO _logger.log(log_level, f"DNSSEC error: {repr(e)}") out = await dns.asyncresolver.resolve(url, rtype) validated = False return out, validated ================================================ FILE: electrum/exchange_rate.py ================================================ import asyncio from datetime import datetime import inspect import sys import os import json import time import csv import decimal from decimal import Decimal from typing import Sequence, Optional, Mapping, Dict, Union, Tuple from aiorpcx.curio import timeout_after, ignore_after import aiohttp from . import util from .bitcoin import COIN from .i18n import _ from .util import ( ThreadJob, make_dir, log_exceptions, OldTaskGroup, make_aiohttp_session, resource_path, EventListener, event_listener, to_decimal, timestamp_to_datetime ) from .util import NetworkRetryManager from .network import Network from .simple_config import SimpleConfig from .logging import Logger # See https://en.wikipedia.org/wiki/ISO_4217 CCY_PRECISIONS = {'BHD': 3, 'BIF': 0, 'BYR': 0, 'CLF': 4, 'CLP': 0, 'CVE': 0, 'DJF': 0, 'GNF': 0, 'IQD': 3, 'ISK': 0, 'JOD': 3, 'JPY': 0, 'KMF': 0, 'KRW': 0, 'KWD': 3, 'LYD': 3, 'MGA': 1, 'MRO': 1, 'OMR': 3, 'PYG': 0, 'RWF': 0, 'TND': 3, 'UGX': 0, 'UYI': 0, 'VND': 0, 'VUV': 0, 'XAF': 0, 'XAU': 4, 'XOF': 0, 'XPF': 0, # Cryptocurrencies 'BTC': 8, 'LTC': 6, 'XRP': 4, 'ETH': 8, } SPOT_RATE_REFRESH_TARGET = 150 # approx. every 2.5 minutes, try to refresh spot price SPOT_RATE_CLOSE_TO_STALE = 450 # try harder to fetch an update if price is getting old SPOT_RATE_EXPIRY = 600 # spot price becomes stale after 10 minutes -> we no longer show/use it class ExchangeBase(Logger): def __init__(self, on_quotes, on_history): Logger.__init__(self) self._history = {} # type: Dict[str, Dict[str, str | float]] self._quotes = {} # type: Dict[str, Optional[Decimal]] self._quotes_timestamp = 0 # type: Union[int, float] self.on_quotes = on_quotes self.on_history = on_history async def get_raw(self, site, get_string): # APIs must have https url = ''.join(['https://', site, get_string]) network = Network.get_instance() proxy = network.proxy if network else None async with make_aiohttp_session(proxy) as session: async with session.get(url) as response: response.raise_for_status() return await response.text() async def get_json(self, site, get_string): # APIs must have https url = ''.join(['https://', site, get_string]) network = Network.get_instance() proxy = network.proxy if network else None async with make_aiohttp_session(proxy) as session: async with session.get(url) as response: response.raise_for_status() # set content_type to None to disable checking MIME type return await response.json(content_type=None) async def get_csv(self, site, get_string): raw = await self.get_raw(site, get_string) reader = csv.DictReader(raw.split('\n')) return list(reader) def name(self): return self.__class__.__name__ async def update_safe(self, ccy: str) -> None: try: self.logger.info(f"getting fx quotes for {ccy}") self._quotes = await self.get_rates(ccy) assert all(isinstance(rate, (Decimal, type(None))) for rate in self._quotes.values()), \ f"fx rate must be Decimal, got {self._quotes}" except (aiohttp.ClientError, asyncio.TimeoutError, OSError) as e: self.logger.info(f"failed fx quotes: {repr(e)}") self.on_quotes() except Exception as e: self.logger.exception(f"failed fx quotes: {repr(e)}") self.on_quotes() else: self.logger.debug("received fx quotes") self._quotes_timestamp = time.time() self.on_quotes(received_new_data=True) @staticmethod def _read_historical_rates_from_file( *, exchange_name: str, ccy: str, cache_dir: str, ) -> Tuple[Optional[Dict[str, str]], Optional[float]]: filename = os.path.join(cache_dir, f"{exchange_name}_{ccy}") if not os.path.exists(filename): return None, None timestamp = os.stat(filename).st_mtime try: with open(filename, 'r', encoding='utf-8') as f: h = json.loads(f.read()) except Exception: return None, None if not h: # e.g. empty dict return None, None # cast rates to str h = {date_str: str(rate) for (date_str, rate) in h.items()} return h, timestamp def read_historical_rates(self, ccy: str, cache_dir: str) -> Optional[dict]: h, timestamp = self._read_historical_rates_from_file( exchange_name=self.name(), ccy=ccy, cache_dir=cache_dir, ) if not h: return None assert timestamp is not None h['timestamp'] = timestamp self._history[ccy] = h self.on_history() return h @staticmethod def _write_historical_rates_to_file( *, exchange_name: str, ccy: str, cache_dir: str, history: Dict[str, str], ) -> None: # sanity check types of history dict assert 'timestamp' not in history for key, rate in history.items(): assert isinstance(key, str), f"{exchange_name=}. {ccy=}. {key=!r}. {rate=!r}" assert isinstance(rate, str), f"{exchange_name=}. {ccy=}. {key=!r}. {rate=!r}" # write to file filename = os.path.join(cache_dir, f"{exchange_name}_{ccy}") with open(filename, 'w', encoding='utf-8') as f: f.write(json.dumps(history, sort_keys=True)) @log_exceptions async def get_historical_rates_safe(self, ccy: str, cache_dir: str) -> None: try: self.logger.info(f"requesting fx history for {ccy}") h_new = await self.request_history(ccy) self.logger.debug(f"received fx history for {ccy}") except (aiohttp.ClientError, asyncio.TimeoutError, OSError) as e: self.logger.info(f"failed fx history: {repr(e)}") return except Exception as e: self.logger.exception(f"failed fx history: {repr(e)}") return # cast rates to str h_new = {date_str: str(rate) for (date_str, rate) in h_new.items()} # type: Dict[str, str] # merge old history and new history. resolve duplicate dates using new data. h_old, _timestamp = self._read_historical_rates_from_file( exchange_name=self.name(), ccy=ccy, cache_dir=cache_dir, ) h_old = h_old or {} h = {**h_old, **h_new} # write merged data to disk cache self._write_historical_rates_to_file( exchange_name=self.name(), ccy=ccy, cache_dir=cache_dir, history=h, ) h['timestamp'] = time.time() # note: this is the only item in h that has a float value self._history[ccy] = h self.on_history() def get_historical_rates(self, ccy: str, cache_dir: str) -> None: if ccy not in self.history_ccys(): return h = self._history.get(ccy) if h is None: h = self.read_historical_rates(ccy, cache_dir) if h is None or h['timestamp'] < time.time() - 24*3600: util.get_asyncio_loop().create_task(self.get_historical_rates_safe(ccy, cache_dir)) def history_ccys(self) -> Sequence[str]: return [] def historical_rate(self, ccy: str, d_t: datetime) -> Decimal: date_str = d_t.strftime('%Y-%m-%d') rate = self._history.get(ccy, {}).get(date_str) or 'NaN' try: return Decimal(rate) except Exception: # guard against garbage coming from exchange #self.logger.debug(f"found corrupted historical_rate: {rate=!r}. for {ccy=} at {date_str}") return Decimal('NaN') async def request_history(self, ccy: str) -> Dict[str, Union[str, float]]: raise NotImplementedError() # implemented by subclasses async def get_rates(self, ccy: str) -> Mapping[str, Optional[Decimal]]: raise NotImplementedError() # implemented by subclasses async def get_currencies(self) -> Sequence[str]: rates = await self.get_rates('') return sorted([str(a) for (a, b) in rates.items() if b is not None and len(a)==3]) def get_cached_spot_quote(self, ccy: str) -> Decimal: """Returns the cached exchange rate as a Decimal""" if ccy == 'BTC': return Decimal(1) rate = self._quotes.get(ccy) if not rate: # don't return 0 to prevent DivisionByZero exceptions return Decimal('NaN') if self._quotes_timestamp + SPOT_RATE_EXPIRY < time.time(): # Our rate is stale. Probably better to return no rate than an incorrect one. return Decimal('NaN') return Decimal(rate) class Yadio(ExchangeBase): async def get_currencies(self): dicts = await self.get_json('api.yadio.io', '/currencies') return list(dicts.keys()) async def get_rates(self, ccy: str) -> Mapping[str, Optional[Decimal]]: json = await self.get_json('api.yadio.io', '/rate/%s/BTC' % ccy) return {ccy: to_decimal(json['rate'])} class BitcoinAverage(ExchangeBase): # note: historical rates used to be freely available # but this is no longer the case. see #5188 async def get_rates(self, ccy): json = await self.get_json('apiv2.bitcoinaverage.com', '/indices/global/ticker/short') return dict([(r.replace("BTC", ""), to_decimal(json[r]['last'])) for r in json if r != 'timestamp']) class Bitcointoyou(ExchangeBase): async def get_rates(self, ccy): json = await self.get_json('bitcointoyou.com', "/API/ticker.aspx") return {'BRL': to_decimal(json['ticker']['last'])} class BitcoinVenezuela(ExchangeBase): async def get_rates(self, ccy): json = await self.get_json('api.bitcoinvenezuela.com', '/') rates = [(r, to_decimal(json['BTC'][r])) for r in json['BTC'] if json['BTC'][r] is not None] # Giving NULL for LTC return dict(rates) def history_ccys(self): return ['ARS', 'EUR', 'USD', 'VEF'] async def request_history(self, ccy): json = await self.get_json('api.bitcoinvenezuela.com', "/historical/index.php?coin=BTC") return json[ccy + '_BTC'] class Bitbank(ExchangeBase): async def get_rates(self, ccy): json = await self.get_json('public.bitbank.cc', '/btc_jpy/ticker') return {'JPY': to_decimal(json['data']['last'])} class BitFinex(ExchangeBase): async def get_currencies(self): json = await self.get_json( 'api-pub.bitfinex.com', f"/v2/conf/pub:list:pair:exchange") pairs = [pair for pair in json[0] if len(pair) == 6 and pair[:3] == "BTC"] return [pair[3:] for pair in pairs] def history_ccys(self): return CURRENCIES[self.name()] async def get_rates(self, ccy): # ref https://docs.bitfinex.com/reference/rest-public-ticker json = await self.get_json( 'api-pub.bitfinex.com', f"/v2/ticker/tBTC{ccy}") return {ccy: to_decimal(json[6])} async def request_history(self, ccy): # ref https://docs.bitfinex.com/reference/rest-public-candles history = await self.get_json( 'api.bitfinex.com', f"/v2/candles/trade:1D:tBTC{ccy}/hist?limit=10000") return dict([(timestamp_to_datetime(h[0] // 1000, utc=True).strftime('%Y-%m-%d'), str(h[2])) for h in history]) class BitFlyer(ExchangeBase): async def get_rates(self, ccy): json = await self.get_json('bitflyer.jp', '/api/echo/price') return {'JPY': to_decimal(json['mid'])} class BitPay(ExchangeBase): async def get_rates(self, ccy): json = await self.get_json('bitpay.com', '/api/rates') return dict([(r['code'], to_decimal(r['rate'])) for r in json]) class Bitso(ExchangeBase): async def get_rates(self, ccy): json = await self.get_json('api.bitso.com', '/v2/ticker') return {'MXN': to_decimal(json['last'])} class BitStamp(ExchangeBase): async def get_currencies(self): # ref https://www.bitstamp.net/api/#tag/Tickers/operation/GetCurrencyPairTickers json = await self.get_json( 'www.bitstamp.net', f"/api/v2/ticker/") pairs = [ticker["pair"] for ticker in json] pairs = [pair for pair in pairs if len(pair) == 7 and pair[:4] == "BTC/"] return [pair[4:] for pair in pairs] async def get_rates(self, ccy): # ref https://www.bitstamp.net/api/#tag/Tickers/operation/GetMarketTicker if ccy in CURRENCIES[self.name()]: json = await self.get_json('www.bitstamp.net', f'/api/v2/ticker/btc{ccy.lower()}/') return {ccy: to_decimal(json['last'])} return {} def history_ccys(self): return CURRENCIES[self.name()] async def request_history(self, ccy): # ref https://www.bitstamp.net/api/#tag/Market-info/operation/GetOHLCData merged_history = {} history_starts = 1313625600 # for BTCUSD pair (probably earliest) items_per_request = 1000 step = 86400 async def populate_history(endtime: int): history = await self.get_json( 'www.bitstamp.net', f"/api/v2/ohlc/btc{ccy.lower()}/?step={step}&limit={items_per_request}&end={endtime}") history = dict([ (timestamp_to_datetime(int(h["timestamp"]), utc=True).strftime('%Y-%m-%d'), str(h["close"])) for h in history["data"]["ohlc"]]) merged_history.update(history) async with OldTaskGroup() as group: endtime = int(time.time()) while True: if endtime < history_starts: break await group.spawn(populate_history(endtime=endtime)) endtime = endtime - items_per_request * step return merged_history class Bitvalor(ExchangeBase): async def get_rates(self,ccy): json = await self.get_json('api.bitvalor.com', '/v1/ticker.json') return {'BRL': to_decimal(json['ticker_1h']['total']['last'])} class BlockchainInfo(ExchangeBase): async def get_rates(self, ccy): json = await self.get_json('blockchain.info', '/ticker') return dict([(r, to_decimal(json[r]['15m'])) for r in json]) class Bylls(ExchangeBase): async def get_rates(self, ccy): json = await self.get_json('bylls.com', '/api/price?from_currency=BTC&to_currency=CAD') return {'CAD': to_decimal(json['public_price']['to_price'])} class Coinbase(ExchangeBase): async def get_rates(self, ccy): json = await self.get_json('api.coinbase.com', '/v2/exchange-rates?currency=BTC') return {ccy: to_decimal(rate) for (ccy, rate) in json["data"]["rates"].items()} class CoinCap(ExchangeBase): async def get_rates(self, ccy): json = await self.get_json('api.coincap.io', '/v2/rates/bitcoin/') return {'USD': to_decimal(json['data']['rateUsd'])} def history_ccys(self): return ['USD'] async def request_history(self, ccy): # Currently 2000 days is the maximum in 1 API call # (and history starts on 2017-03-23) history = await self.get_json('api.coincap.io', '/v2/assets/bitcoin/history?interval=d1&limit=2000') return dict([(timestamp_to_datetime(h['time']/1000, utc=True).strftime('%Y-%m-%d'), str(h['priceUsd'])) for h in history['data']]) class CoinDesk(ExchangeBase): async def get_currencies(self): dicts = await self.get_json('api.coindesk.com', '/v1/bpi/supported-currencies.json') return [d['currency'] for d in dicts] async def get_rates(self, ccy): json = await self.get_json('api.coindesk.com', '/v1/bpi/currentprice/%s.json' % ccy) result = {ccy: to_decimal(json['bpi'][ccy]['rate_float'])} return result def history_starts(self): return {'USD': '2012-11-30', 'EUR': '2013-09-01'} def history_ccys(self): return self.history_starts().keys() async def request_history(self, ccy): start = self.history_starts()[ccy] end = datetime.today().strftime('%Y-%m-%d') # Note ?currency and ?index don't work as documented. Sigh. query = ('/v1/bpi/historical/close.json?start=%s&end=%s' % (start, end)) json = await self.get_json('api.coindesk.com', query) return json['bpi'] class CoinGecko(ExchangeBase): async def get_rates(self, ccy): json = await self.get_json('api.coingecko.com', '/api/v3/exchange_rates') return dict([(ccy.upper(), to_decimal(d['value'])) for ccy, d in json['rates'].items()]) def history_ccys(self): # CoinGecko seems to have historical data for all ccys it supports return CURRENCIES[self.name()] async def request_history(self, ccy): # ref https://docs.coingecko.com/v3.0.1/reference/coins-id-market-chart num_days = 365 # Setting `num_days = "max"` started erroring (around 2024-04) with: # > Your request exceeds the allowed time range. Public API users are limited to querying # > historical data within the past 365 days. Upgrade to a paid plan to enjoy full historical data access history = await self.get_json('api.coingecko.com', f"/api/v3/coins/bitcoin/market_chart?vs_currency={ccy}&days={num_days}") return dict([(timestamp_to_datetime(h[0]/1000, utc=True).strftime('%Y-%m-%d'), str(h[1])) for h in history['prices']]) class Bit2C(ExchangeBase): async def get_rates(self, ccy): json = await self.get_json('bit2c.co.il', '/Exchanges/BtcNis/Ticker.json') return {'ILS': to_decimal(json['ll'])} def history_ccys(self): return CURRENCIES[self.name()] async def request_history(self, ccy): history = await self.get_json('bit2c.co.il', '/Exchanges/BtcNis/KLines?resolution=1D&from=1357034400&to=%s' % int(time.time())) return dict([(timestamp_to_datetime(h[0], utc=True).strftime('%Y-%m-%d'), str(h[6])) for h in history]) class CointraderMonitor(ExchangeBase): async def get_rates(self, ccy): json = await self.get_json('cointradermonitor.com', '/api/pbb/v1/ticker') return {'BRL': to_decimal(json['last'])} class itBit(ExchangeBase): async def get_rates(self, ccy): ccys = ['USD', 'EUR', 'SGD'] json = await self.get_json('api.itbit.com', '/v1/markets/XBT%s/ticker' % ccy) result = dict.fromkeys(ccys) if ccy in ccys: result[ccy] = to_decimal(json['lastPrice']) return result class Kraken(ExchangeBase): async def get_rates(self, ccy): # ref https://docs.kraken.com/api/docs/rest-api/get-ticker-information ccys = ['EUR', 'USD', 'CAD', 'GBP', 'JPY'] pairs = ['XBT%s' % c for c in ccys] json = await self.get_json('api.kraken.com', '/0/public/Ticker?pair=%s' % ','.join(pairs)) return dict((k[-3:], to_decimal(v['c'][0])) for k, v in json['result'].items()) # async def request_history(self, ccy): # # ref https://docs.kraken.com/api/docs/rest-api/get-ohlc-data # pass # limited to last 720 steps (step can by 1 day / 7 days / 15 days) class MercadoBitcoin(ExchangeBase): async def get_rates(self, ccy): json = await self.get_json('api.bitvalor.com', '/v1/ticker.json') return {'BRL': to_decimal(json['ticker_1h']['exchanges']['MBT']['last'])} class Winkdex(ExchangeBase): async def get_rates(self, ccy): json = await self.get_json('winkdex.com', '/api/v0/price') return {'USD': to_decimal(json['price']) / 100} def history_ccys(self): return ['USD'] async def request_history(self, ccy): json = await self.get_json('winkdex.com', "/api/v0/series?start_time=1342915200") history = json['series'][0]['results'] return dict([(h['timestamp'][:10], str(to_decimal(h['price']) / 100)) for h in history]) class Zaif(ExchangeBase): async def get_rates(self, ccy): json = await self.get_json('api.zaif.jp', '/api/1/last_price/btc_jpy') return {'JPY': to_decimal(json['last_price'])} class Bitragem(ExchangeBase): async def get_rates(self,ccy): json = await self.get_json('api.bitragem.com', '/v1/index?asset=BTC&market=BRL') return {'BRL': to_decimal(json['response']['index'])} class Biscoint(ExchangeBase): async def get_rates(self,ccy): json = await self.get_json('api.biscoint.io', '/v1/ticker?base=BTC"e=BRL') return {'BRL': to_decimal(json['data']['last'])} class Walltime(ExchangeBase): async def get_rates(self, ccy): json = await self.get_json('s3.amazonaws.com', '/data-production-walltime-info/production/dynamic/walltime-info.json') return {'BRL': to_decimal(json['BRL_XBT']['last_inexact'])} def dictinvert(d): inv = {} for k, vlist in d.items(): for v in vlist: keys = inv.setdefault(v, []) keys.append(k) return inv def get_exchanges_and_currencies(): # load currencies.json from disk path = resource_path('currencies.json') try: with open(path, 'r', encoding='utf-8') as f: return json.loads(f.read()) except Exception: pass # or if not present, generate it now. print("cannot find currencies.json. will regenerate it now.") d = {} is_exchange = lambda obj: (inspect.isclass(obj) and issubclass(obj, ExchangeBase) and obj != ExchangeBase) exchanges = dict(inspect.getmembers(sys.modules[__name__], is_exchange)) async def get_currencies_safe(name, exchange): try: d[name] = await exchange.get_currencies() print(name, "ok") except Exception: print(name, "error") async def query_all_exchanges_for_their_ccys_over_network(): async with timeout_after(10): async with OldTaskGroup() as group: for name, klass in exchanges.items(): exchange = klass(None, None) await group.spawn(get_currencies_safe(name, exchange)) loop = asyncio.new_event_loop() try: loop.run_until_complete(query_all_exchanges_for_their_ccys_over_network()) except Exception as e: pass finally: loop.close() with open(path, 'w', encoding='utf-8') as f: f.write(json.dumps(d, indent=4, sort_keys=True)) return d CURRENCIES = get_exchanges_and_currencies() def get_exchanges_by_ccy(history=True): if not history: return dictinvert(CURRENCIES) d = {} exchanges = CURRENCIES.keys() for name in exchanges: klass = globals()[name] exchange = klass(None, None) d[name] = exchange.history_ccys() return dictinvert(d) class FxThread(ThreadJob, EventListener, NetworkRetryManager[str]): def __init__(self, *, config: SimpleConfig): ThreadJob.__init__(self) NetworkRetryManager.__init__( self, max_retry_delay_normal=SPOT_RATE_REFRESH_TARGET, init_retry_delay_normal=SPOT_RATE_REFRESH_TARGET, max_retry_delay_urgent=SPOT_RATE_REFRESH_TARGET, init_retry_delay_urgent=1, ) # note: we poll every 5 seconds for action, so we won't attempt connections more frequently than that. self.config = config self.register_callbacks() self.ccy = self.get_currency() self.history_used_spot = False self.ccy_combo = None self.hist_checkbox = None self.cache_dir = os.path.join(config.path, 'cache') # type: str self._trigger = asyncio.Event() self._trigger.set() self.set_exchange(self.config_exchange()) make_dir(self.cache_dir) @event_listener def on_event_proxy_set(self, *args): self._clear_addr_retry_times() self._trigger.set() @staticmethod def get_currencies(history: bool) -> Sequence[str]: d = get_exchanges_by_ccy(history) return sorted(d.keys()) @staticmethod def get_exchanges_by_ccy(ccy: str, history: bool) -> Sequence[str]: d = get_exchanges_by_ccy(history) return d.get(ccy, []) @staticmethod def remove_thousands_separator(text: str) -> str: return text.replace(util.THOUSANDS_SEP, "") def ccy_amount_str(self, amount, *, add_thousands_sep: bool = False, ccy=None) -> str: prec = CCY_PRECISIONS.get(self.ccy if ccy is None else ccy, 2) fmt_str = "{:%s.%df}" % ("," if add_thousands_sep else "", max(0, prec)) try: rounded_amount = round(amount, prec) except decimal.InvalidOperation: rounded_amount = amount text = fmt_str.format(rounded_amount) # replace "," -> THOUSANDS_SEP # replace "." -> DECIMAL_POINT dp_loc = text.find(".") text = text.replace(",", util.THOUSANDS_SEP) if dp_loc == -1: return text return text[:dp_loc] + util.DECIMAL_POINT + text[dp_loc+1:] def ccy_precision(self, ccy=None) -> int: return CCY_PRECISIONS.get(self.ccy if ccy is None else ccy, 2) async def run(self): while True: # keep polling and see if we should refresh spot price or historical prices manually_triggered = False async with ignore_after(5): await self._trigger.wait() self._trigger.clear() manually_triggered = True if not self.is_enabled(): continue if manually_triggered and self.has_history(): # maybe refresh historical prices self.exchange.get_historical_rates(self.ccy, self.cache_dir) now = time.time() if not manually_triggered and self.exchange._quotes_timestamp + SPOT_RATE_REFRESH_TARGET > now: continue # last quote still fresh # If the last quote is relatively recent, we poll at fixed time intervals. # Once it gets close to cache expiry, we change to an exponential backoff, to try to get # a quote before it expires. Also, on Android, we might come back from a sleep after a long time, # with the last quote close to expiry or already expired, in that case we go into exponential backoff. is_urgent = self.exchange._quotes_timestamp + SPOT_RATE_CLOSE_TO_STALE < now addr_name = "spot-urgent" if is_urgent else "spot" # this separates retry-counters if self._can_retry_addr(addr_name, urgent=is_urgent): self._trying_addr_now(addr_name) # refresh spot price await self.exchange.update_safe(self.ccy) def is_enabled(self) -> bool: return self.config.FX_USE_EXCHANGE_RATE def set_enabled(self, b: bool) -> None: self.config.FX_USE_EXCHANGE_RATE = b self.trigger_update() def can_have_history(self): return self.is_enabled() and self.ccy in self.exchange.history_ccys() def has_history(self) -> bool: return self.can_have_history() and self.config.FX_HISTORY_RATES def get_currency(self) -> str: '''Use when dynamic fetching is needed''' return self.config.FX_CURRENCY def config_exchange(self): return self.config.FX_EXCHANGE def set_currency(self, ccy: str): self.ccy = ccy self.config.FX_CURRENCY = ccy self.trigger_update() self.on_quotes() def trigger_update(self): self._clear_addr_retry_times() loop = util.get_asyncio_loop() loop.call_soon_threadsafe(self._trigger.set) def set_exchange(self, name): class_ = globals().get(name) or globals().get(self.config.cv.FX_EXCHANGE.get_default_value()) self.logger.info(f"using exchange {name}") if self.config_exchange() != name: self.config.FX_EXCHANGE = name assert issubclass(class_, ExchangeBase), f"unexpected type {class_} for {name}" self.exchange = class_(self.on_quotes, self.on_history) # type: ExchangeBase # A new exchange means new fx quotes, initially empty. Force # a quote refresh self.trigger_update() self.exchange.read_historical_rates(self.ccy, self.cache_dir) def on_quotes(self, *, received_new_data: bool = False): if received_new_data: self._clear_addr_retry_times() util.trigger_callback('on_quotes') def on_history(self): util.trigger_callback('on_history') def exchange_rate(self) -> Decimal: """Returns the exchange rate as a Decimal""" if not self.is_enabled(): return Decimal('NaN') return self.exchange.get_cached_spot_quote(self.ccy) def format_amount(self, btc_balance, *, timestamp: int = None) -> str: if timestamp is None: rate = self.exchange_rate() else: rate = self.timestamp_rate(timestamp) return '' if rate.is_nan() else "%s" % self.value_str(btc_balance, rate) def format_amount_and_units(self, btc_balance, *, timestamp: int = None) -> str: if timestamp is None: rate = self.exchange_rate() else: rate = self.timestamp_rate(timestamp) return '' if rate.is_nan() else "%s %s" % (self.value_str(btc_balance, rate), self.ccy) def get_fiat_status_text(self, btc_balance, base_unit, decimal_point): rate = self.exchange_rate() if rate.is_nan(): return _(" (No FX rate available)") amount = 1000 if decimal_point == 0 else 1 value = self.value_str(amount * COIN / (10**(8 - decimal_point)), rate) return " %d %s~%s %s" % (amount, base_unit, value, self.ccy) def fiat_value(self, satoshis, rate) -> Decimal: return Decimal('NaN') if satoshis is None else Decimal(satoshis) / COIN * Decimal(rate) def value_str(self, satoshis, rate, *, add_thousands_sep: bool = None) -> str: fiat_val = self.fiat_value(satoshis, rate) return self.format_fiat(fiat_val, add_thousands_sep=add_thousands_sep) def format_fiat(self, value: Decimal, *, add_thousands_sep: bool = None) -> str: if value.is_nan(): return _("No data") if add_thousands_sep is None: add_thousands_sep = True return self.ccy_amount_str(value, add_thousands_sep=add_thousands_sep) def history_rate(self, d_t: Optional[datetime]) -> Decimal: if d_t is None: return Decimal('NaN') rate = self.exchange.historical_rate(self.ccy, d_t) # Frequently there is no rate for today, until tomorrow :) # Use spot quotes in that case if rate.is_nan() and (datetime.today().date() - d_t.date()).days <= 2: rate = self.exchange.get_cached_spot_quote(self.ccy) self.history_used_spot = True if rate is None: rate = 'NaN' return Decimal(rate) def historical_value_str(self, satoshis, d_t: Optional[datetime]) -> str: return self.format_fiat(self.historical_value(satoshis, d_t)) def historical_value(self, satoshis, d_t: Optional[datetime]) -> Decimal: return self.fiat_value(satoshis, self.history_rate(d_t)) def timestamp_rate(self, timestamp: Optional[int]) -> Decimal: from .util import timestamp_to_datetime date = timestamp_to_datetime(timestamp) return self.history_rate(date) assert globals().get(SimpleConfig.FX_EXCHANGE.get_default_value()), f"default exchange {SimpleConfig.FX_EXCHANGE.get_default_value()} does not exist" ================================================ FILE: electrum/fee_policy.py ================================================ from typing import Optional, Sequence, Tuple, Union, TYPE_CHECKING, Dict from decimal import Decimal from numbers import Real from enum import IntEnum import math from .i18n import _ from .util import NoDynamicFeeEstimates, quantize_feerate, format_fee_satoshis, FEERATE_PRECISION from . import util, constants from .logging import Logger if TYPE_CHECKING: from .network import Network # 1008 = max conf target of core's estimatesmartfee, requesting more results in rpc error. # estimatesmartfee guarantees that the fee will get accepted into the mempool FEE_ETA_TARGETS = [1008, 144, 25, 10, 5, 2, 1] FEE_DEPTH_TARGETS = [10_000_000, 5_000_000, 2_000_000, 1_000_000, 800_000, 600_000, 400_000, 250_000, 100_000] FEERATE_STATIC_VALUES = [1000, 2000, 5000, 10000, 20000, 30000, 50000, 70000, 100000, 150000, 200000, 300000] # satoshi per kbyte FEERATE_MAX_DYNAMIC = 1500000 FEERATE_WARNING_HIGH_FEE = 600000 FEERATE_FALLBACK_STATIC_FEE = 150000 FEERATE_REGTEST_STATIC_FEE = FEERATE_FALLBACK_STATIC_FEE # hardcoded fee used on regtest FEERATE_MIN_RELAY = 100 FEERATE_DEFAULT_RELAY = 1000 # conservative "min relay fee" FEERATE_MAX_RELAY = 50000 assert FEERATE_MIN_RELAY <= FEERATE_DEFAULT_RELAY <= FEERATE_MAX_RELAY # warn user if fee/amount for on-chain tx is higher than this FEE_RATIO_HIGH_WARNING = 0.05 # note: make sure the network is asking for estimates for these targets FEE_LN_ETA_TARGET = 2 FEE_LN_LOW_ETA_TARGET = 25 FEE_LN_MINIMUM_ETA_TARGET = 1008 # The min feerate_per_kw that can be used in lightning so that # the resulting onchain tx pays the min relay fee. # This would be FEERATE_DEFAULT_RELAY / 4 if not for rounding errors, # see https://github.com/ElementsProject/lightning/commit/2e687b9b352c9092b5e8bd4a688916ac50b44af0 FEERATE_PER_KW_MIN_RELAY_LIGHTNING = 253 def closest_index(value, array) -> int: dist = list(map(lambda x: abs(x - value), array)) return min(range(len(dist)), key=dist.__getitem__) class FeeMethod(IntEnum): # note: careful changing these names! they appear in the config files. FIXED = 0 # fixed absolute fee FEERATE = 1 # fixed fee rate ETA = 2 # dynamic, ETA based MEMPOOL = 3 # dynamic, mempool based @classmethod def slider_values(cls): return [FeeMethod.FEERATE, FeeMethod.ETA, FeeMethod.MEMPOOL] def name_for_GUI(self): names = { FeeMethod.FIXED: _('FIXED'), FeeMethod.FEERATE: _('Feerate'), FeeMethod.ETA: _('ETA'), FeeMethod.MEMPOOL: _('Mempool') } return names[self] @classmethod def slider_index_of_method(cls, method): try: i = FeeMethod.slider_values().index(method) except ValueError: i = -1 return i class FeePolicy(Logger): # object associated to a fee slider def __init__(self, descriptor: str): Logger.__init__(self) try: name, value = descriptor.split(':') self.method = FeeMethod[name.upper()] self.value = int(value) # target (e.g. num blocks, nbytes from mempool tip, sat/kbyte) except Exception: self.logger.warning(f"Could not parse fee policy descriptor '{descriptor}'. Falling back to 'eta:2'") self.method = FeeMethod.ETA self.value = 2 def __repr__(self): return self.get_descriptor() def get_descriptor(self) -> str: return self.method.name.lower() + ':' + str(self.value) def set_method(self, method: FeeMethod): assert isinstance(method, FeeMethod) self.method = method # default values if self.method == FeeMethod.MEMPOOL: self.value = 1000000 # 1 mb from tip elif self.method == FeeMethod.ETA: self.value = 2 # 2 blocks elif self.method == FeeMethod.FEERATE: self.value = 5000 # sats per vkb else: self.value = 10 # sats def _get_array(self) -> Sequence[int]: if self.method == FeeMethod.MEMPOOL: return FEE_DEPTH_TARGETS elif self.method == FeeMethod.ETA: return FEE_ETA_TARGETS elif self.method == FeeMethod.FEERATE: return FEERATE_STATIC_VALUES else: raise Exception('') def set_value_from_slider_pos(self, slider_pos: int): array = self._get_array() slider_pos = max(0, min(slider_pos, len(array)-1)) self.value = array[slider_pos] def get_slider_pos(self) -> int: array = self._get_array() return closest_index(self.value, array) def get_slider_max(self) -> int: array = self._get_array() maxp = len(array) - 1 return maxp @property def use_dynamic_estimates(self): return self.method in [FeeMethod.ETA, FeeMethod.MEMPOOL] @classmethod def depth_target(cls, slider_pos: int) -> int: """Returns mempool depth target in bytes for a fee slider position.""" slider_pos = max(slider_pos, 0) slider_pos = min(slider_pos, len(FEE_DEPTH_TARGETS)-1) return FEE_DEPTH_TARGETS[slider_pos] def eta_target(self, slider_pos: int) -> int: """Returns 'num blocks' ETA target for a fee slider position.""" return FEE_ETA_TARGETS[slider_pos] @classmethod def eta_tooltip(cls, x): if x < 0: return _('Low fee') elif x == 1: return _('In the next block') elif x == 144: return _('Within one day') elif x == 1008: return _("Within one week") else: return _('Within {} blocks').format(x) def get_target_text(self): """ Description of what the target is: static fee / num blocks to confirm in / mempool depth """ if self.method == FeeMethod.ETA: return self.eta_tooltip(self.value) elif self.method == FeeMethod.MEMPOOL: return self.depth_tooltip(self.value) elif self.method == FeeMethod.FEERATE: fee_per_byte = self.value/1000 return format_fee_satoshis(fee_per_byte) + f" {util.UI_UNIT_NAME_FEERATE_SAT_PER_VBYTE}" elif self.method == FeeMethod.FIXED: return f'{self.value} {util.UI_UNIT_NAME_FIXED_SAT}' def get_estimate_text(self, network: 'Network') -> str: """ Description of the current fee estimate corresponding to the target """ fee_per_kb = self.fee_per_kb(network) fee_per_byte = fee_per_kb/1000 if fee_per_kb is not None else None tooltip = '' if self.use_dynamic_estimates: if fee_per_byte is not None: tooltip = format_fee_satoshis(fee_per_byte) + f" {util.UI_UNIT_NAME_FEERATE_SAT_PER_VBYTE}" elif self.method == FeeMethod.FEERATE: assert fee_per_kb is not None assert fee_per_byte is not None if network and network.mempool_fees.has_data(): depth = network.mempool_fees.fee_to_depth(fee_per_byte) tooltip = self.depth_tooltip(depth) if network and network.fee_estimates.has_data(): eta = network.fee_estimates.fee_to_eta(fee_per_kb) tooltip += '\n' + self.eta_tooltip(eta) return tooltip def get_tooltip(self, network: 'Network'): target = self.get_target_text() estimate = self.get_estimate_text(network) if self.use_dynamic_estimates: return _('Target') + ': ' + target + '\n' + _('Current rate') + ': ' + estimate else: return _('Fixed rate') + ': ' + target + '\n' + _('Estimate') + ': ' + estimate @classmethod def depth_tooltip(cls, depth: Optional[int]) -> str: """Returns text tooltip for given mempool depth (in vbytes).""" if depth is None: return "unknown from tip" depth_mb = cls.get_depth_mb_str(depth) return _("{} from tip").format(depth_mb) @classmethod def get_depth_mb_str(cls, depth: int) -> str: # e.g. 500_000 -> "0.50 MB" depth_mb = "{:.2f}".format(depth / 1_000_000) # maybe .rstrip("0") ? return f"{depth_mb} {util.UI_UNIT_NAME_MEMPOOL_MB}" def fee_per_kb(self, network: 'Network') -> Optional[int]: """Returns sat/kvB fee to pay for a txn. Note: might return None. """ if self.method == FeeMethod.FEERATE: fee_rate = self.value elif self.method == FeeMethod.MEMPOOL: if network: fee_rate = network.mempool_fees.depth_to_fee(self.get_slider_pos()) else: fee_rate = None elif self.method == FeeMethod.ETA: if network: fee_rate = network.fee_estimates.eta_to_fee(self.get_slider_pos()) else: fee_rate = None elif self.method == FeeMethod.FIXED: fee_rate = None else: raise Exception(self.method) if fee_rate is not None: fee_rate = int(fee_rate) return fee_rate def fee_per_byte(self, network: 'Network') -> Optional[int]: """Returns sat/vB fee to pay for a txn. Note: might return None. """ fee_per_kb = self.fee_per_kb(network) return fee_per_kb / 1000 if fee_per_kb is not None else None def estimate_fee( self, size: Union[int, float, Decimal], *, network: 'Network' = None, allow_fallback_to_static_rates: bool = False, ) -> int: if self.method == FeeMethod.FIXED: return self.value fee_per_kb = self.fee_per_kb(network) if fee_per_kb is None and self.use_dynamic_estimates: if allow_fallback_to_static_rates: fee_per_kb = FEERATE_FALLBACK_STATIC_FEE else: raise NoDynamicFeeEstimates() return self.estimate_fee_for_feerate(fee_per_kb=fee_per_kb, size=size) @classmethod def estimate_fee_for_feerate( cls, *, fee_per_kb: Union[int, float, Decimal], size: Union[int, float, Decimal], ) -> int: # note: 'size' is in vbytes size = Decimal(size) fee_per_kb = Decimal(fee_per_kb) fee_per_byte = fee_per_kb / 1000 # to be consistent with what is displayed in the GUI, # the calculation needs to use the same precision: fee_per_byte = quantize_feerate(fee_per_byte) return math.ceil(fee_per_byte * size) class FixedFeePolicy(FeePolicy): def __init__(self, fee): FeePolicy.__init__(self, 'fixed:%d' % fee) def impose_hard_limits_on_fee(func): def get_fee_within_limits(self, *args, **kwargs): fee = func(self, *args, **kwargs) if fee is None: return fee fee = min(FEERATE_MAX_DYNAMIC, fee) # Clamp dynamic feerates with conservative min relay fee, # to ensure txs propagate well: fee = max(FEERATE_DEFAULT_RELAY, fee) return fee return get_fee_within_limits class FeeHistogram: def __init__(self): self._data = None # type: Optional[Sequence[Tuple[Union[float, int], int]]] def has_data(self) -> bool: return self._data is not None def set_data(self, data): self._data = data def fee_to_depth(self, target_fee: Real) -> Optional[int]: """For a given sat/vbyte fee, returns an estimate of how deep it would be in the current mempool in vbytes. Pessimistic == overestimates the depth. """ if self._data is None: return None depth = 0 for fee, s in self._data: depth += s if fee <= target_fee: break return depth @impose_hard_limits_on_fee def depth_target_to_fee(self, target: int) -> Optional[int]: """Returns fee in sat/kbyte. target: desired mempool depth in vbytes """ if self._data is None: return None depth = 0 for fee, s in self._data: depth += s if depth > target: break else: return 0 # add one sat/byte as currently that is the max precision of the histogram # note: precision depends on server. # old ElectrumX <1.16 has 1 s/b prec, >=1.16 has 0.1 s/b prec. # electrs seems to use untruncated double-precision floating points. # # TODO decrease this to 0.1 s/b next time we bump the required protocol version fee += 1 # convert to sat/kbyte return int(fee * 1000) def depth_to_fee(self, slider_pos) -> Optional[int]: """Returns fee in sat/kbyte.""" target = FeePolicy.depth_target(slider_pos) return self.depth_target_to_fee(target) def get_capped_data(self): """ used by QML """ data = self._data or [[FEERATE_DEFAULT_RELAY/1000, 1]] # cap the histogram to a limited number of megabytes bytes_limit = 10*1000*1000 bytes_current = 0 capped_histogram = [] for item in sorted(data, key=lambda x: x[0], reverse=True): if bytes_current >= bytes_limit: break slot = min(item[1], bytes_limit - bytes_current) bytes_current += slot # round & limit precision value = int(item[0] * 10**FEERATE_PRECISION) / 10**FEERATE_PRECISION capped_histogram.append([ max(FEERATE_MIN_RELAY/1000, value), # clamped to [FEERATE_MIN_RELAY/1000, inf) slot, # width of bucket bytes_current, # cumulative depth at far end of bucket ]) return capped_histogram, bytes_current class FeeTimeEstimates: def __init__(self): self.data = {} # type: Dict[int, int] def get_data(self): return self.data def has_data(self) -> bool: """Returns if we have estimates for *all* targets requested. Note: if wanting an estimate for a specific target, instead of checking has_data(), just try to do the estimate and handle a potential None result. That way, estimation works for targets we have, even if some targets are missing. """ targets = set(FEE_ETA_TARGETS) targets.discard(1) # rm "next block" target return all(target in self.data for target in targets) def set_data(self, nblock_target: int, fee_per_kb: int): assert isinstance(nblock_target, int), f"expected int, got {nblock_target!r}" assert isinstance(fee_per_kb, int), f"expected int, got {fee_per_kb!r}" self.data[nblock_target] = fee_per_kb def fee_to_eta(self, fee_per_kb: Optional[int]) -> int: """Returns 'num blocks' ETA estimate for given fee rate, or -1 for low fee. """ import operator lst = list(self.data.items()) next_block_fee = self.eta_target_to_fee(1) if next_block_fee is not None: lst += [(1, next_block_fee)] if not lst or fee_per_kb is None: return -1 dist = map(lambda x: (x[0], abs(x[1] - fee_per_kb)), lst) min_target, min_value = min(dist, key=operator.itemgetter(1)) if fee_per_kb < self.data.get(FEE_ETA_TARGETS[0])/2: min_target = -1 return min_target def eta_to_fee(self, slider_pos) -> Optional[int]: """Returns fee in sat/kbyte.""" slider_pos = max(slider_pos, 0) slider_pos = min(slider_pos, len(FEE_ETA_TARGETS) - 1) if slider_pos < len(FEE_ETA_TARGETS) - 1: num_blocks = FEE_ETA_TARGETS[int(slider_pos)] fee = self.eta_target_to_fee(num_blocks) else: fee = self.eta_target_to_fee(1) return fee @impose_hard_limits_on_fee def eta_target_to_fee(self, num_blocks: int) -> Optional[int]: """Returns fee in sat/kbyte.""" if num_blocks == 1: fee = self.data.get(2) if fee is not None: fee += fee / 2 fee = int(fee) else: fee = self.data.get(num_blocks) if fee is not None: fee = int(fee) # fallback for regtest if fee is None and constants.net is constants.BitcoinRegtest: return FEERATE_REGTEST_STATIC_FEE return fee ================================================ FILE: electrum/gui/__init__.py ================================================ # To create a new GUI, please add its code to this directory. # Three objects are passed to the ElectrumGui: config, daemon and plugins # The Wallet object is instantiated by the GUI # Notifications about network events are sent to the GUI by using network.register_callback() from typing import TYPE_CHECKING, Mapping, Optional if TYPE_CHECKING: from . import qt from electrum.simple_config import SimpleConfig from electrum.daemon import Daemon from electrum.plugin import Plugins class BaseElectrumGui: def __init__(self, *, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins'): self.config = config self.daemon = daemon self.plugins = plugins def main(self) -> None: raise NotImplementedError() def stop(self) -> None: """Stops the GUI. This method must be thread-safe. """ pass @classmethod def version_info(cls) -> Mapping[str, Optional[str]]: return {} ================================================ FILE: electrum/gui/common_qt/__init__.py ================================================ # Copyright (C) 2023 The Electrum developers # Distributed under the MIT software license, see the accompanying # file LICENCE or http://www.opensource.org/licenses/mit-license.php ================================================ FILE: electrum/gui/common_qt/i18n.py ================================================ from PyQt6.QtCore import QTranslator from electrum.i18n import _ class ElectrumTranslator(QTranslator): """Delegator for Qt translations to gettext""" def __init__(self, parent=None): super().__init__(parent) # explicit enumeration of translatable strings from Qt standard library, so these # will be included in the electrum gettext translation template self._strings = [_('&Undo'), _('&Redo'), _('Cu&t'), _('&Copy'), _('&Paste'), _('Select All'), _('Copy &Link Location')] def translate(self, context, source_text: str, disambiguation, n): return _(source_text, context=context) ================================================ FILE: electrum/gui/common_qt/plugins.py ================================================ import sys from typing import TYPE_CHECKING, Optional from PyQt6.QtCore import pyqtSignal, pyqtProperty, QObject from electrum.logging import get_logger if TYPE_CHECKING: from electrum.gui.qml import ElectrumQmlApplication from electrum.plugin import BasePlugin class PluginQObject(QObject): logger = get_logger(__name__) pluginChanged = pyqtSignal() busyChanged = pyqtSignal() pluginEnabledChanged = pyqtSignal() def __init__(self, plugin: 'BasePlugin', parent: Optional['ElectrumQmlApplication']): super().__init__(parent) self._busy = False self.plugin = plugin self.app = parent @pyqtProperty(str, notify=pluginChanged) def name(self): return self._name @pyqtProperty(bool, notify=busyChanged) def busy(self): return self._busy # below only used for QML, not compatible yet with Qt @pyqtProperty(bool, notify=pluginEnabledChanged) def pluginEnabled(self): return self.plugin.is_enabled() @pluginEnabled.setter def pluginEnabled(self, enabled): if enabled != self.plugin.is_enabled(): self.logger.debug(f'can {self.plugin.can_user_disable()}, {self.plugin.is_available()}') if not self.plugin.can_user_disable() and not enabled: return if enabled: self.app.plugins.enable(self.plugin.name) else: self.app.plugins.disable(self.plugin.name) self.pluginEnabledChanged.emit() ================================================ FILE: electrum/gui/common_qt/util.py ================================================ import queue import sys from functools import wraps from typing import Optional, NamedTuple, Callable import os.path from PyQt6 import QtGui from PyQt6.QtCore import Qt, QThread, pyqtSignal from PyQt6.QtGui import QColor, QPen, QPaintDevice, QFontDatabase, QImage import qrcode from electrum.i18n import _ from electrum.logging import Logger from electrum.util import EventListener, event_listener _cached_font_ids: dict[str, int] = {} def get_font_id(filename: str) -> int: font_id = _cached_font_ids.get(filename) if font_id is not None: return font_id # font_id will be negative on error font_id = QFontDatabase.addApplicationFont( os.path.join(os.path.dirname(__file__), '..', 'fonts', filename) ) _cached_font_ids[filename] = font_id return font_id def draw_qr( *, qr: Optional[qrcode.main.QRCode], paint_device: QPaintDevice, # target to paint on is_enabled: bool = True, min_boxsize: int = 2, # min size in pixels of single black/white unit box of the qr code ) -> None: """Draw 'qr' onto 'paint_device'. - qr.box_size is ignored. We will calculate our own boxsize to fill the whole size of paint_device. - qr.border is respected. """ black = QColor(0, 0, 0, 255) grey = QColor(196, 196, 196, 255) white = QColor(255, 255, 255, 255) black_pen = QPen(black) if is_enabled else QPen(grey) black_pen.setJoinStyle(Qt.PenJoinStyle.MiterJoin) if not qr: qp = QtGui.QPainter() qp.begin(paint_device) qp.setBrush(white) qp.setPen(white) r = qp.viewport() qp.drawRect(0, 0, r.width(), r.height()) qp.end() return # note: next line can raise qrcode.exceptions.DataOverflowError (or ValueError) matrix = qr.get_matrix() # includes qr.border k = len(matrix) qp = QtGui.QPainter() qp.begin(paint_device) r = qp.viewport() framesize = min(r.width(), r.height()) boxsize = int(framesize / k) if boxsize < min_boxsize: # The amount of data is still within what can fit into a QR code, # however we don't have enough pixels to draw it. qp.setBrush(white) qp.setPen(white) qp.drawRect(0, 0, r.width(), r.height()) qp.setBrush(black) qp.setPen(black) qp.drawText(0, 20, _("Cannot draw QR code") + ":") qp.drawText(0, 40, _("Not enough space available.")) qp.end() return size = k * boxsize left = (framesize - size) / 2 top = (framesize - size) / 2 # Draw white background with margin qp.setBrush(white) qp.setPen(white) qp.drawRect(0, 0, framesize, framesize) # Draw qr code qp.setBrush(black if is_enabled else grey) qp.setPen(black_pen) for r in range(k): for c in range(k): if matrix[r][c]: qp.drawRect( int(left + c * boxsize), int(top + r * boxsize), boxsize - 1, boxsize - 1) qp.end() def paintQR(data) -> Optional[QImage]: if not data: return None # Create QR code qr = qrcode.QRCode() qr.add_data(data) # Create a QImage to draw on matrix = qr.get_matrix() k = len(matrix) boxsize = 5 size = k * boxsize # Create the image with appropriate size base_img = QImage(size, size, QImage.Format.Format_ARGB32) # Use draw_qr to paint on the image draw_qr( qr=qr, paint_device=base_img, is_enabled=True, min_boxsize=boxsize ) return base_img class TaskThread(QThread, Logger): """Thread that runs background tasks. Callbacks are guaranteed to happen in the context of its parent.""" class Task(NamedTuple): task: Callable cb_success: Optional[Callable] cb_done: Optional[Callable] cb_error: Optional[Callable] cancel: Optional[Callable] = None doneSig = pyqtSignal(object, object, object) def __init__(self, parent, on_error=None): QThread.__init__(self, parent) Logger.__init__(self) self.on_error = on_error self.tasks = queue.Queue() self._cur_task = None # type: Optional[TaskThread.Task] self._stopping = False self.doneSig.connect(self.on_done) self.start() def add(self, task, on_success=None, on_done=None, on_error=None, *, cancel=None): if self._stopping: self.logger.warning(f"stopping or already stopped but tried to add new task.") return on_error = on_error or self.on_error task_ = TaskThread.Task(task, on_success, on_done, on_error, cancel=cancel) self.tasks.put(task_) def run(self): while True: if self._stopping: break task = self.tasks.get() # type: TaskThread.Task self._cur_task = task if not task or self._stopping: break try: result = task.task() self.doneSig.emit(result, task.cb_done, task.cb_success) except BaseException: self.doneSig.emit(sys.exc_info(), task.cb_done, task.cb_error) def on_done(self, result, cb_done, cb_result): # This runs in the parent's thread. if cb_done: cb_done() if cb_result: cb_result(result) def stop(self): self._stopping = True # try to cancel currently running task now. # if the task does not implement "cancel", we will have to wait until it finishes. task = self._cur_task if task and task.cancel: task.cancel() # cancel the remaining tasks in the queue while True: try: task = self.tasks.get_nowait() except queue.Empty: break if task and task.cancel: task.cancel() self.tasks.put(None) # in case the thread is still waiting on the queue self.exit() self.wait() class QtEventListener(EventListener): qt_callback_signal = pyqtSignal(tuple) def register_callbacks(self): self.qt_callback_signal.connect(self.on_qt_callback_signal) EventListener.register_callbacks(self) def unregister_callbacks(self): try: self.qt_callback_signal.disconnect() except (RuntimeError, TypeError): # wrapped Qt object might be deleted # "TypeError: disconnect() failed between 'qt_callback_signal' and all its connections" pass EventListener.unregister_callbacks(self) def on_qt_callback_signal(self, args): func = args[0] return func(self, *args[1:]) # decorator for members of the QtEventListener class def qt_event_listener(func): func = event_listener(func) @wraps(func) def decorator(self, *args): self.qt_callback_signal.emit((func,) + args) return decorator ================================================ FILE: electrum/gui/default_lang.py ================================================ # Copyright (C) 2023 The Electrum developers # Distributed under the MIT software license, see the accompanying # file LICENCE or http://www.opensource.org/licenses/mit-license.php # # Note: try not to import modules from electrum, or at least from GUIs. # This is to avoid evaluating module-level string-translations before we get # a chance to set the default language. import os from typing import Optional from electrum.i18n import languages jLocale = None if "ANDROID_DATA" in os.environ: from jnius import autoclass, cast jLocale = autoclass("java.util.Locale") def get_default_language(*, gui_name: Optional[str] = None) -> str: if gui_name == "qt": from PyQt6.QtCore import QLocale name = QLocale.system().name() return name if name in languages else "en_UK" elif gui_name == "qml": from PyQt6.QtCore import QLocale # On Android QLocale does not return the system locale try: name = str(jLocale.getDefault().toString()) except Exception: name = QLocale.system().name() return name if name in languages else "en_GB" return "" ================================================ FILE: electrum/gui/fonts/PTMono.LICENSE ================================================ Copyright (c) 2011, ParaType Ltd. (http://www.paratype.com/public), with Reserved Font Names "PT Sans", "PT Serif", "PT Mono" and "ParaType". This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL ----------------------------------------------------------- SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ----------------------------------------------------------- PREAMBLE The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. DEFINITIONS "Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. "Reserved Font Name" refers to any names specified as such after the copyright statement(s). "Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). "Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. "Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. PERMISSION & CONDITIONS Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: 1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. 2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. 3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. 5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. TERMINATION This license becomes null and void if any of the above conditions are not met. DISCLAIMER THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. ================================================ FILE: electrum/gui/messages.py ================================================ from electrum.i18n import _ from electrum.submarine_swaps import MIN_FINAL_CLTV_DELTA_FOR_CLIENT def to_rtf(msg): return '\n'.join(['

' + x + '

' for x in msg.split('\n\n')]) MSG_COOPERATIVE_CLOSE = _( """Your node will negotiate the transaction fee with the remote node. This method of closing the channel usually results in the lowest fees.""" ) MSG_REQUEST_FORCE_CLOSE = _( """If you request a force-close, your node will pretend that it has lost its data and ask the remote node to broadcast their latest state. Doing so from time to time helps make sure that nodes are honest, because your node can punish them if they broadcast a revoked state.""" ) MSG_CREATED_NON_RECOVERABLE_CHANNEL = _( """The channel you created is not recoverable from seed. To prevent fund losses, please save this backup on another device. It may be imported in another Electrum wallet with the same seed.""" ) MSG_LIGHTNING_WARNING = _( """Electrum uses static channel backups. If you lose your wallet file, you will need to request your channel to be force-closed by the remote peer in order to recover your funds. This assumes that the remote peer is reachable, and has not lost its own data.""" ) MSG_THIRD_PARTY_PLUGIN_WARNING = ' '.join([ '' + _('Warning: Third-party plugins have access to your wallet!') + '', '

', _('Installing this plugin will grant third-party software access to your wallet. You must trust the plugin not to be malicious.'), _('You should at minimum check who the author of the plugin is, and be careful of imposters.'), '

', _('Third-party plugins are not endorsed by Electrum.'), _('Electrum will not be responsible in case of theft, loss of funds or privacy that might result from third-party plugins.'), '

', _('To install this plugin, please enter your plugin authorization password') + ':' ]) MSG_CONFLICTING_BACKUP_INSTANCE = _( """Another instance of this wallet (same seed) has an open channel with the same remote node. If you create this channel, you will not be able to use both wallets at the same time. Are you sure?""" ) MSG_LN_EXPLAIN_SCB_BACKUPS = "".join([ _("Channel backups can be imported in another instance of the same wallet."), " ", _("In the Electrum mobile app, use the 'Send' button to scan this QR code."), " ", "\n\n", _("Please note that channel backups cannot be used to restore your channels."), " ", _("If you lose your wallet file, the only thing you can do with a backup is to request your channel to be closed, so that your funds will be sent on-chain."), ]) MSG_CAPITAL_GAINS = _( """This summary covers only on-chain transactions (no lightning!). Capital gains are computed by attaching an acquisition price to each UTXO in the wallet, and uses the order of blockchain events (not FIFO).""" ) MSG_NON_TRAMPOLINE_CHANNEL_FROZEN_WITHOUT_GOSSIP = _( """This channel is with a non-trampoline node; it cannot be used if trampoline is enabled. If you want to keep using this channel, you need to disable trampoline routing in your preferences.""" ) MSG_FREEZE_ADDRESS = _("When you freeze an address, the funds in that address will not be used for sending bitcoins.") MSG_FREEZE_COIN = _("When you freeze a coin, it will not be used for sending bitcoins.") MSG_FORWARD_SWAP_FUNDING_MEMPOOL = ( _('Your funding transaction has been broadcast.') + " " + _("Please remain online until the funding transaction is confirmed.") + "\n\n" + _('The swap will be finalized once your transaction is confirmed.') + " " + _("After the funding transaction is mined, the server will reveal the preimage needed to " "fulfill the pending received lightning HTLCs. The HTLCs expire in {} blocks. " "You will need to be online after the funding transaction is confirmed but before the HTLCs expire, " "to claim your money. If you go offline for several days while the swap is pending, " "you risk losing the swap amount!").format(MIN_FINAL_CLTV_DELTA_FOR_CLIENT) ) MSG_REVERSE_SWAP_FUNDING_MEMPOOL = ( _('The funding transaction has been detected.') + " " + _('Your claiming transaction will be broadcast when the funding transaction is confirmed.') + " " + _("If you go offline before broadcasting the claiming transaction and let the swap time out, " "you will not get back the already pre-paid mining fees.") ) MSG_FORCE_CLOSE_WARNING = ( _('You will need to come back online after the commitment transaction is confirmed, in order to broadcast second-stage htlc transactions.') + ' ' + _('If you remain offline for more than {} blocks, your channel counterparty will be able to sweep those funds.') ) MSG_FORWARD_SWAP_WARNING = ( _('You will need to come back online after the funding transaction is confirmed, in order to settle the swap.') + ' ' + _('If you remain offline for more than {} blocks, your channel will be force closed and you might lose the funds you sent in the swap.') ) MSG_REVERSE_SWAP_WARNING = ( _('You will need to come back online after the funding transaction is confirmed, in order to settle the swap.') + ' ' + _('If you remain offline for more than {} blocks, the swap will be cancelled and you will lose the prepaid mining fees.') ) MSG_LN_UTXO_RESERVE = ( _("You do not have enough on-chain funds to protect your Lightning channels.") + ' ' + _("You should have at least {} on-chain in order to be able to sweep channel outputs.") ) # not to be translated MSG_TERMS_OF_USE = ( """1. Electrum is distributed under the MIT licence by Electrum Technologies GmbH. Most notably, this means that the Electrum software is provided as is, and that it comes without warranty. 2. We are neither a bank nor a financial service provider. In addition, we do not store user account data, and we are not an intermediary in the interaction between our software and the Bitcoin blockchain. Therefore, we do not have the possibility to freeze funds or to undo a fraudulent transaction. 3. We do not provide private user support. All issue resolutions are public, and take place on Github or public forums. If someone posing as 'Electrum support' proposes to help you via a private channel, this person is most likely an imposter trying to steal your bitcoins.""" ) TERMS_OF_USE_LATEST_VERSION : int = 1 # bump this if we want users re-prompted due to changes MSG_CONNECTMODE_AUTOCONNECT = _('Auto-connect') MSG_CONNECTMODE_MANUAL = _('Manual server selection') MSG_CONNECTMODE_ONESERVER = _('Connect only to a single server') MSG_CONNECTMODE_SERVER_HELP = _( "Electrum connects to a unique server in order to receive your transaction history. " "This server will learn your wallet addresses." ) MSG_CONNECTMODE_NODES_HELP = _( "In addition to your history server, Electrum will try to maintain connections with ~10 extra servers, in order to download block headers and find out the longest blockchain. " "These servers are only used for block header notifications and fee estimates; they do not learn your wallet addresses. " "Getting block headers from multiple sources is useful to detect lagging servers and forks. " "Fork detection is security-critical for determining number of confirmations." ) MSG_CONNECTMODE_AUTOCONNECT_HELP = _( "Electrum will always use a history server that is on the longest blockchain. " "If your current server is unresponsive or lagging, Electrum will switch to another server." ) MSG_CONNECTMODE_MANUAL_HELP = _( "Electrum will stay with the server you selected. It will warn you if your server is lagging." ) MSG_CONNECTMODE_ONESERVER_HELP = _( "Electrum will stay with the server you selected, and it will not connect to additional nodes. " "This will disable fork detection. " "This mode is only intended for connecting to your own fully trusted server. " "Using this option on a public server is a security risk and is discouraged." ) MSG_SUBMARINE_PAYMENT_HELP_TEXT = ''.join(( _("Submarine Payments use a reverse submarine swap to do on-chain transactions directly " "from your lightning balance."), '\n\n', _("Submarine Payments happen in two stages. In the first stage, your wallet sends a lightning " "payment to the submarine swap provider. The swap provider will lock funds to a " "funding output in an on-chain transaction (the funding transaction)."), '\n', _("Once the funding transaction has one confirmation, your wallet will broadcast a claim " "transaction as the second stage of the payment. This claim transaction spends the funding " "output to the payee's address."), '\n\n', _("Warning:"), '\n', _('The funding transaction is not visible to the payee. They will only see a pending ' 'transaction in the mempool after your wallet broadcasts the claim transaction. ' 'Since confirmation of the funding transaction can take over 30 minutes, avoid using ' 'Submarine Payments when the payee expects to see the transaction within a limited ' 'time frame (e.g., an online shop checkout). Use a regular on-chain payment instead.'), )) MSG_RELAYFEE = ' '.join([ _("This transaction requires a higher fee, or it will not be propagated by your current server."), _("Try to raise your transaction fee, or use a server with a lower relay fee.") ]) ================================================ FILE: electrum/gui/qml/__init__.py ================================================ import os import signal import sys import threading from typing import TYPE_CHECKING try: import PyQt6 except Exception as e: from electrum import GuiImportError raise GuiImportError( "Error: Could not import PyQt6. On Linux systems, " "you may try 'sudo apt-get install python3-pyqt6'") from e try: import PyQt6.QtQml except Exception as e: from electrum import GuiImportError raise GuiImportError( "Error: Could not import PyQt6.QtQml. On Linux systems, " "you may try 'sudo apt-get install python3-pyqt6.qtquick'") from e from PyQt6.QtCore import (Qt, QCoreApplication, QLocale, QTimer, QT_VERSION_STR, PYQT_VERSION_STR) from PyQt6.QtGui import QGuiApplication from electrum.plugin import run_hook from electrum.util import profiler from electrum.logging import Logger from electrum.gui import BaseElectrumGui from electrum.gui.common_qt.i18n import ElectrumTranslator if TYPE_CHECKING: from electrum.daemon import Daemon from electrum.simple_config import SimpleConfig from electrum.plugin import Plugins from .qeapp import ElectrumQmlApplication, Exception_Hook class ElectrumGui(BaseElectrumGui, Logger): @profiler def __init__(self, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins'): BaseElectrumGui.__init__(self, config=config, daemon=daemon, plugins=plugins) Logger.__init__(self) # uncomment to debug plugin and import tracing # os.environ['QML_IMPORT_TRACE'] = '1' # os.environ['QT_DEBUG_PLUGINS'] = '1' os.environ['QT_ANDROID_DISABLE_ACCESSIBILITY'] = '1' # set default locale to en_GB. This is for l10n (e.g. number formatting, number input etc), # but not for i18n, which is handled by the Translator # this can be removed once the backend wallet is fully l10n aware QLocale.setDefault(QLocale('en_GB')) self.logger.info(f"Qml GUI starting up... Qt={QT_VERSION_STR}, PyQt={PYQT_VERSION_STR}") self.logger.info("CWD=%s" % os.getcwd()) # Uncomment this call to verify objects are being properly # GC-ed when windows are closed #plugins.add_jobs([DebugMem([Abstract_Wallet, SPV, Synchronizer, # ElectrumWindow], interval=5)]) if hasattr(Qt, "AA_ShareOpenGLContexts"): QCoreApplication.setAttribute(Qt.AA_ShareOpenGLContexts) if hasattr(QGuiApplication, 'setDesktopFileName'): QGuiApplication.setDesktopFileName('electrum') if "QT_QUICK_CONTROLS_STYLE" not in os.environ: os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material" self.gui_thread = threading.current_thread() self.app = ElectrumQmlApplication(sys.argv, config=config, daemon=daemon, plugins=plugins) self.translator = ElectrumTranslator() self.app.installTranslator(self.translator) # timer self.timer = QTimer(self.app) self.timer.setSingleShot(False) self.timer.setInterval(500) # msec self.timer.timeout.connect(lambda: None) # periodically enter python scope # hook for crash reporter Exception_Hook.maybe_setup(slot=self.app.appController.crash) # Initialize any QML plugins run_hook('init_qml', self.app) self.app.engine.load('electrum/gui/qml/components/main.qml') def close(self): self.app.quit() def main(self): if not self.app._valid: return self.timer.start() signal.signal(signal.SIGINT, lambda *args: self._handle_sigint()) self.logger.info('Entering main loop') self.app.exec() def _handle_sigint(self): self.app.appController.wantClose = True self.stop() def stop(self): self.logger.info('closing GUI') self.app.quit() ================================================ FILE: electrum/gui/qml/android_res/layout/scanner_layout.xml ================================================