plugins: psbt_nostr: implement for qml
This commit is contained in:
@@ -76,6 +76,7 @@ ElDialog {
|
||||
}
|
||||
|
||||
ButtonContainer {
|
||||
id: buttons
|
||||
Layout.fillWidth: true
|
||||
|
||||
FlatButton {
|
||||
@@ -97,6 +98,17 @@ ElDialog {
|
||||
AppController.doShare(dialog.text, dialog.title)
|
||||
}
|
||||
}
|
||||
function beforeLayout() {
|
||||
var export_tx_buttons = app.pluginsComponentsByName('export_tx_button')
|
||||
for (var i=0; i < export_tx_buttons.length; i++) {
|
||||
var b = export_tx_buttons[i].createObject(buttons, {
|
||||
dialog: dialog
|
||||
})
|
||||
b.Layout.fillWidth = true
|
||||
b.Layout.preferredWidth = 1
|
||||
buttons.addItem(b)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,13 @@ Container {
|
||||
contentItem = contentRoot
|
||||
}
|
||||
|
||||
Component.onCompleted: fillContentItem()
|
||||
// override this function to dynamically add buttons.
|
||||
function beforeLayout() {}
|
||||
|
||||
Component.onCompleted: {
|
||||
beforeLayout()
|
||||
fillContentItem()
|
||||
}
|
||||
|
||||
Component {
|
||||
id: containerLayout
|
||||
|
||||
@@ -39,6 +39,8 @@ ApplicationWindow
|
||||
|
||||
property var _exceptionDialog
|
||||
|
||||
property var pluginobjects: ({})
|
||||
|
||||
property QtObject appMenu: Menu {
|
||||
id: menu
|
||||
|
||||
@@ -640,6 +642,43 @@ ApplicationWindow
|
||||
})
|
||||
app._exceptionDialog.open()
|
||||
}
|
||||
function onPluginLoaded(name) {
|
||||
console.log('plugin ' + name + ' loaded')
|
||||
var loader = AppController.plugin(name).loader
|
||||
if (loader == undefined)
|
||||
return
|
||||
var url = Qt.resolvedUrl('../../../plugins/' + name + '/qml/' + loader)
|
||||
var comp = Qt.createComponent(url)
|
||||
if (comp.status == Component.Error) {
|
||||
console.log('Could not find/parse PluginLoader for plugin ' + name)
|
||||
console.log(comp.errorString())
|
||||
return
|
||||
}
|
||||
var obj = comp.createObject(app)
|
||||
if (obj != null)
|
||||
app.pluginobjects[name] = obj
|
||||
}
|
||||
}
|
||||
|
||||
function pluginsComponentsByName(comp_name) {
|
||||
// return named QML components from plugins
|
||||
var plugins = AppController.plugins
|
||||
var result = []
|
||||
for (var i=0; i < plugins.length; i++) {
|
||||
if (!plugins[i].enabled)
|
||||
continue
|
||||
var pluginobject = app.pluginobjects[plugins[i].name]
|
||||
if (!pluginobject)
|
||||
continue
|
||||
if (!(comp_name in pluginobject))
|
||||
continue
|
||||
var comp = pluginobject[comp_name]
|
||||
if (!comp)
|
||||
continue
|
||||
|
||||
result.push(comp)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
Connections {
|
||||
|
||||
@@ -74,6 +74,8 @@ class QEAppController(BaseCrashReporter, QObject):
|
||||
sendingBugreportFailure = pyqtSignal(str)
|
||||
secureWindowChanged = pyqtSignal()
|
||||
wantCloseChanged = pyqtSignal()
|
||||
pluginLoaded = pyqtSignal(str)
|
||||
startupFinished = pyqtSignal()
|
||||
|
||||
def __init__(self, qeapp: 'ElectrumQmlApplication', plugins: 'Plugins'):
|
||||
BaseCrashReporter.__init__(self, None, None, None)
|
||||
@@ -229,8 +231,11 @@ class QEAppController(BaseCrashReporter, QObject):
|
||||
if scheme == BITCOIN_BIP21_URI_SCHEME or scheme == LIGHTNING_URI_SCHEME:
|
||||
self.uriReceived.emit(data)
|
||||
|
||||
def startupFinished(self):
|
||||
def startup_finished(self):
|
||||
self._app_started = True
|
||||
self.startupFinished.emit()
|
||||
for plugin_name in self._plugins.plugins.keys():
|
||||
self.pluginLoaded.emit(plugin_name)
|
||||
if self._intent:
|
||||
self.on_new_intent(self._intent)
|
||||
|
||||
@@ -304,18 +309,16 @@ class QEAppController(BaseCrashReporter, QObject):
|
||||
self.logger.debug('None!')
|
||||
return None
|
||||
|
||||
@pyqtProperty('QVariant', notify=_dummy)
|
||||
@pyqtProperty('QVariantList', notify=_dummy)
|
||||
def plugins(self):
|
||||
s = []
|
||||
for item in self._plugins.descriptions:
|
||||
self.logger.info(item)
|
||||
s.append({
|
||||
'name': item,
|
||||
'fullname': self._plugins.descriptions[item]['fullname'],
|
||||
'enabled': bool(self._plugins.get(item))
|
||||
})
|
||||
|
||||
self.logger.debug(f'{str(s)}')
|
||||
return s
|
||||
|
||||
@pyqtSlot(str, bool)
|
||||
@@ -511,10 +514,11 @@ class ElectrumQmlApplication(QGuiApplication):
|
||||
# slot is called after loading root QML. If object is None, it has failed.
|
||||
@pyqtSlot('QObject*', 'QUrl')
|
||||
def objectCreated(self, object, url):
|
||||
self.engine.objectCreated.disconnect(self.objectCreated)
|
||||
if object is None:
|
||||
self._valid = False
|
||||
self.engine.objectCreated.disconnect(self.objectCreated)
|
||||
self.appController.startupFinished()
|
||||
else:
|
||||
self.appController.startup_finished()
|
||||
|
||||
def message_handler(self, line, funct, file):
|
||||
# filter out common harmless messages
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"fullname": "Nostr Multisig",
|
||||
"description": "This plugin facilitates the use of multi-signatures wallets. It sends and receives partially signed transactions from/to your cosigner wallet. PSBTs are sent and retrieved from Nostr relays.",
|
||||
"author": "The Electrum Developers",
|
||||
"available_for": ["qt"],
|
||||
"icon":"nostr_multisig.png",
|
||||
"available_for": ["qt", "qml"],
|
||||
"version": "0.0.1"
|
||||
}
|
||||
|
||||
@@ -203,6 +203,9 @@ class CosignerWallet(Logger):
|
||||
# note that tx could also be unrelated from wallet?... (not ismine inputs)
|
||||
return True
|
||||
|
||||
def mark_event_rcvd(self, event_id):
|
||||
self.known_events[event_id] = now()
|
||||
|
||||
def prepare_messages(self, tx: Union[Transaction, PartialTransaction]) -> List[Tuple[str, str]]:
|
||||
messages = []
|
||||
for xpub, pubkey in self.cosigner_list:
|
||||
|
||||
@@ -22,10 +22,19 @@
|
||||
# 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 TYPE_CHECKING
|
||||
import asyncio
|
||||
import concurrent
|
||||
from typing import TYPE_CHECKING, List, Tuple, Optional
|
||||
|
||||
from PyQt6.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
|
||||
|
||||
from electrum import util
|
||||
from electrum.plugin import hook
|
||||
from electrum.transaction import PartialTransaction, tx_from_any
|
||||
from electrum.wallet import Multisig_Wallet
|
||||
from electrum.util import EventListener, event_listener
|
||||
|
||||
from electrum.gui.qml.qewallet import QEWallet
|
||||
|
||||
from .psbt_nostr import PsbtNostrPlugin, CosignerWallet
|
||||
|
||||
@@ -34,40 +43,101 @@ if TYPE_CHECKING:
|
||||
from electrum.gui.qml import ElectrumQmlApplication
|
||||
|
||||
|
||||
class QReceiveSignalObject(QObject):
|
||||
def __init__(self, plugin: 'Plugin'):
|
||||
QObject.__init__(self)
|
||||
self._plugin = plugin
|
||||
|
||||
cosignerReceivedPsbt = pyqtSignal(str, str, str)
|
||||
sendPsbtFailed = pyqtSignal(str, arguments=['reason'])
|
||||
sendPsbtSuccess = pyqtSignal()
|
||||
|
||||
@pyqtProperty(str)
|
||||
def loader(self):
|
||||
return 'main.qml'
|
||||
|
||||
@pyqtSlot(QEWallet, str)
|
||||
def sendPsbt(self, wallet: 'QEWallet', tx: str):
|
||||
cosigner_wallet = self._plugin.cosigner_wallets[wallet.wallet]
|
||||
if not cosigner_wallet:
|
||||
return
|
||||
cosigner_wallet.send_psbt(tx_from_any(tx, deserialize=True))
|
||||
|
||||
@pyqtSlot(QEWallet, str)
|
||||
def acceptPsbt(self, wallet: 'QEWallet', event_id: str):
|
||||
cosigner_wallet = self._plugin.cosigner_wallets[wallet.wallet]
|
||||
if not cosigner_wallet:
|
||||
return
|
||||
cosigner_wallet.accept_psbt(event_id)
|
||||
|
||||
|
||||
class Plugin(PsbtNostrPlugin):
|
||||
def __init__(self, parent, config, name):
|
||||
super().__init__(parent, config, name)
|
||||
self.so = QReceiveSignalObject(self)
|
||||
self._app = None
|
||||
|
||||
@hook
|
||||
def init_qml(self, app: 'ElectrumQmlApplication'):
|
||||
# if self._init_qt_received: # only need/want the first signal
|
||||
# return
|
||||
# self._init_qt_received = True
|
||||
self._app = app
|
||||
# plugin enable for already open wallets
|
||||
for wallet in app.daemon.get_wallets():
|
||||
self.so.setParent(app) # parent in QObject tree
|
||||
# plugin enable for already open wallet
|
||||
wallet = app.daemon.currentWallet.wallet if app.daemon.currentWallet else None
|
||||
if wallet:
|
||||
self.load_wallet(wallet)
|
||||
|
||||
@hook
|
||||
def load_wallet(self, wallet: 'Abstract_Wallet'):
|
||||
# remove existing, only foreground wallet active
|
||||
if len(self.cosigner_wallets):
|
||||
self.remove_cosigner_wallet(self.cosigner_wallets[0])
|
||||
if not isinstance(wallet, Multisig_Wallet):
|
||||
return
|
||||
self.add_cosigner_wallet(wallet, CosignerWallet(wallet))
|
||||
self.add_cosigner_wallet(wallet, QmlCosignerWallet(wallet, self))
|
||||
|
||||
# @hook
|
||||
# def on_close_window(self, window):
|
||||
# wallet = window.wallet
|
||||
# self.remove_cosigner_wallet(wallet)
|
||||
#
|
||||
# @hook
|
||||
# def transaction_dialog(self, d: 'TxDialog'):
|
||||
# if cw := self.cosigner_wallets.get(d.wallet):
|
||||
# assert isinstance(cw, QtCosignerWallet)
|
||||
# cw.hook_transaction_dialog(d)
|
||||
#
|
||||
# @hook
|
||||
# def transaction_dialog_update(self, d: 'TxDialog'):
|
||||
# if cw := self.cosigner_wallets.get(d.wallet):
|
||||
# assert isinstance(cw, QtCosignerWallet)
|
||||
# cw.hook_transaction_dialog_update(d)
|
||||
|
||||
class QmlCosignerWallet(EventListener, CosignerWallet):
|
||||
|
||||
def __init__(self, wallet: 'Multisig_Wallet', plugin: 'Plugin'):
|
||||
CosignerWallet.__init__(self, wallet)
|
||||
self.plugin = plugin
|
||||
self.register_callbacks()
|
||||
|
||||
self.pending = None
|
||||
|
||||
@event_listener
|
||||
def on_event_psbt_nostr_received(self, wallet, pubkey, event, tx: 'PartialTransaction'):
|
||||
if self.wallet == wallet:
|
||||
self.plugin.so.cosignerReceivedPsbt.emit(pubkey, event, tx.serialize())
|
||||
self.on_receive(pubkey, event, tx)
|
||||
|
||||
def close(self):
|
||||
super().close()
|
||||
self.unregister_callbacks()
|
||||
|
||||
def do_send(self, messages: List[Tuple[str, str]], txid: Optional[str] = None):
|
||||
if not messages:
|
||||
return
|
||||
coro = self.send_direct_messages(messages)
|
||||
|
||||
loop = util.get_asyncio_loop()
|
||||
assert util.get_running_loop() != loop, 'must not be called from asyncio thread'
|
||||
self._result = None
|
||||
self._future = asyncio.run_coroutine_threadsafe(coro, loop)
|
||||
|
||||
try:
|
||||
self._result = self._future.result()
|
||||
self.plugin.so.sendPsbtSuccess.emit()
|
||||
except concurrent.futures.CancelledError:
|
||||
pass
|
||||
except Exception as e:
|
||||
self.plugin.so.sendPsbtFailed.emit(str(e))
|
||||
|
||||
def on_receive(self, pubkey, event_id, tx):
|
||||
self.pending = (pubkey, event_id, tx)
|
||||
|
||||
def accept_psbt(self, my_event_id):
|
||||
pubkey, event_id, tx = self.pending
|
||||
if event_id == my_event_id:
|
||||
self.mark_event_rcvd(event_id)
|
||||
self.pending = None
|
||||
|
||||
54
electrum/plugins/psbt_nostr/qml/main.qml
Normal file
54
electrum/plugins/psbt_nostr/qml/main.qml
Normal file
@@ -0,0 +1,54 @@
|
||||
import QtQuick
|
||||
|
||||
import org.electrum
|
||||
|
||||
import "../../../gui/qml/components/controls"
|
||||
|
||||
Item {
|
||||
Connections {
|
||||
target: AppController ? AppController.plugin('psbt_nostr') : null
|
||||
function onCosignerReceivedPsbt(pubkey, event, tx) {
|
||||
var dialog = app.messageDialog.createObject(app, {
|
||||
text: [
|
||||
qsTr('A transaction was received from your cosigner.'),
|
||||
qsTr('Do you want to open it now?')
|
||||
].join('\n'),
|
||||
yesno: true
|
||||
})
|
||||
dialog.accepted.connect(function () {
|
||||
app.stack.push(Qt.resolvedUrl('../../../gui/qml/components/TxDetails.qml'), {
|
||||
rawtx: tx
|
||||
})
|
||||
target.acceptPsbt(Daemon.currentWallet, event)
|
||||
})
|
||||
dialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
property variant export_tx_button: Component {
|
||||
FlatButton {
|
||||
id: psbt_nostr_send_button
|
||||
property variant dialog
|
||||
text: qsTr('Nostr')
|
||||
icon.source: Qt.resolvedUrl('../../../gui/icons/network.png')
|
||||
visible: Daemon.currentWallet.isMultisig && Daemon.currentWallet.walletType != '2fa'
|
||||
onClicked: {
|
||||
console.log('about to psbt nostr send')
|
||||
psbt_nostr_send_button.enabled = false
|
||||
AppController.plugin('psbt_nostr').sendPsbt(Daemon.currentWallet, dialog.text)
|
||||
}
|
||||
Connections {
|
||||
target: AppController ? AppController.plugin('psbt_nostr') : null
|
||||
function onSendPsbtFailed(message) {
|
||||
psbt_nostr_send_button.enabled = true
|
||||
var dialog = app.messageDialog.createObject(app, {
|
||||
text: qsTr('Sending PSBT to co-signer failed:\n%1').arg(message)
|
||||
})
|
||||
dialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -34,10 +34,9 @@ from electrum.wallet import Multisig_Wallet, Abstract_Wallet
|
||||
from electrum.util import UserCancelled, event_listener, EventListener
|
||||
from electrum.gui.qt.transaction_dialog import show_transaction, TxDialog
|
||||
|
||||
from .psbt_nostr import PsbtNostrPlugin, CosignerWallet, now
|
||||
from .psbt_nostr import PsbtNostrPlugin, CosignerWallet
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from electrum.gui.qt import ElectrumGui
|
||||
from electrum.gui.qt.main_window import ElectrumWindow
|
||||
|
||||
|
||||
@@ -133,5 +132,5 @@ class QtCosignerWallet(EventListener, CosignerWallet):
|
||||
_("An transaction was received from your cosigner.") + '\n' +
|
||||
_("Do you want to open it now?")):
|
||||
return
|
||||
self.known_events[event_id] = now()
|
||||
self.mark_event_rcvd(event_id)
|
||||
show_transaction(tx, parent=window, prompt_if_unsaved=True)
|
||||
|
||||
Reference in New Issue
Block a user