diff --git a/CMakeLists.txt b/CMakeLists.txt
index 55f426cc3f..67923505ed 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -12,6 +12,7 @@ option(STATIC "Link libraries statically, requires static Qt")
option(USE_DEVICE_TREZOR "Trezor support compilation" ON)
option(WITH_SCANNER "Enable webcam QR scanner" OFF)
+option(WITH_OTS_UR "Enable offline transaction signing via UR" OFF)
option(WITH_DESKTOP_ENTRY "Ask to install desktop entry on first startup" ON)
option(WITH_UPDATER "Regularly check for new updates" ON)
option(DEV_MODE "Checkout latest monero master on build" OFF)
@@ -80,9 +81,12 @@ include(VersionGui)
message(STATUS "${CMAKE_MODULE_PATH}")
-if(WITH_SCANNER)
+if(WITH_SCANNER AND NOT WITH_OTS_UR)
add_definitions(-DWITH_SCANNER)
endif()
+if(WITH_OTS_UR)
+ add_definitions(-DWITH_OTS_UR)
+endif()
if(WITH_DESKTOP_ENTRY)
add_definitions(-DWITH_DESKTOP_ENTRY)
@@ -122,7 +126,7 @@ set(QT5_LIBRARIES
Qt5Xml
)
-if(WITH_SCANNER)
+if(WITH_SCANNER OR OTS_UR_WITH_QTQUICK)
list(APPEND QT5_LIBRARIES Qt5Multimedia)
endif()
@@ -241,7 +245,7 @@ if(STATIC)
modelsplugin
)
- if(WITH_SCANNER)
+ if(WITH_SCANNER OR OTS_UR_WITH_QTQUICK)
list(APPEND QT5_EXTRA_LIBRARIES_LIST
declarative_multimedia
Qt5MultimediaQuick
diff --git a/components/QRCodeScanner.qml b/components/QRCodeScanner.qml
index 7479c75f6a..48bb5e39f4 100644
--- a/components/QRCodeScanner.qml
+++ b/components/QRCodeScanner.qml
@@ -29,6 +29,7 @@
import QtQuick 2.9
import QtMultimedia 5.4
import QtQuick.Dialogs 1.2
+import "../components" as MoneroComponents
import moneroComponents.QRCodeScanner 1.0
Rectangle {
@@ -51,7 +52,7 @@ Rectangle {
name: "Capture"
StateChangeScript {
script: {
- root.visible = true
+ root.visible = true
camera.captureMode = Camera.CaptureStillImage
camera.cameraState = Camera.ActiveState
camera.start()
@@ -64,7 +65,7 @@ Rectangle {
StateChangeScript {
script: {
camera.stop()
- root.visible = false
+ root.visible = false
finder.enabled = false
camera.cameraState = Camera.UnloadedState
}
@@ -135,6 +136,17 @@ Rectangle {
}
}
+ MoneroComponents.StandardButton {
+ id: btnClose
+ text: qsTr("Cancel")
+ z: viewfinder.z + 1
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.bottom: parent.bottom
+ anchors.bottomMargin: 20
+ anchors.topMargin: 20
+ onClicked: root.state = "Stopped"
+ }
+
MessageDialog {
id: messageDialog
title: qsTr("QrCode Scanned") + translationManager.emptyString
diff --git a/components/TxConfirmationDialog.qml b/components/TxConfirmationDialog.qml
index dc55a6f520..e4036d925e 100644
--- a/components/TxConfirmationDialog.qml
+++ b/components/TxConfirmationDialog.qml
@@ -163,10 +163,12 @@ Rectangle {
fontFamily: "Arial"
horizontalAlignment: Text.AlignHCenter
text: {
- if (appWindow.viewOnly) {
+ if (appWindow.viewOnly && !persistentSettings.useURCode) {
return qsTr("Create transaction file") + translationManager.emptyString;
} else if (root.sweepUnmixable) {
return qsTr("Sweep unmixable outputs") + translationManager.emptyString;
+ } else if (appWindow.viewOnly && persistentSettings.useURCode) { // intentionally behind sweepUnmixable
+ return qsTr("Create transaction") + translationManager.emptyString;
} else {
return qsTr("Confirm send") + translationManager.emptyString;
}
diff --git a/components/UrCode.qml b/components/UrCode.qml
new file mode 100644
index 0000000000..adb964582a
--- /dev/null
+++ b/components/UrCode.qml
@@ -0,0 +1,165 @@
+import QtQuick 2.9
+import QtMultimedia 5.4
+import OtsUr 0.1
+import "." as MoneroComponents
+
+Rectangle {
+ id : root
+
+ x: 0
+ y: 0
+ z: parent.z+1
+ width: parent.width
+ height: parent.height
+
+ property bool active: false
+ property bool ur: true
+
+ visible: root.active
+ focus: root.active
+ color: "black"
+
+ Image {
+ id: qrCodeImage
+ cache: false
+ width: qrCodeImage.height
+ height: Math.max(300, Math.min(parent.height - frameInfo.height - displayType.height - 240, parent.width - 40))
+ anchors.centerIn: parent
+ function reload() {
+ var tmp = qrCodeImage.source
+ qrCodeImage.source = ""
+ qrCodeImage.source = tmp
+ }
+ }
+
+ Rectangle {
+ id: frameInfo
+ visible: textFrameInfo.visible
+ height: textFrameInfo.height + 5
+ width: textFrameInfo.width + 20
+ z: parent.z + 1
+ radius: 16
+ color: "#FA6800"
+ anchors.centerIn: textFrameInfo
+ opacity: 0.4
+ }
+
+ Text {
+ id: textFrameInfo
+ z: frameInfo.z + 1
+ visible: textFrameInfo.text !== ""
+ text: urSender.currentFrameInfo
+ anchors.top: parent.top
+ anchors.horizontalCenter: qrCodeImage.horizontalCenter
+ anchors.margins: 30
+ font.pixelSize: 22
+ color: "white"
+ opacity: 0.7
+ }
+
+ Rectangle {
+ id: displayType
+ visible: textDisplayType.text !== ""
+ height: textDisplayType.height + 5
+ width: textDisplayType.width + 20
+ z: parent.z + 1
+ radius: 16
+ color: "#FA6800"
+ anchors.centerIn: textDisplayType
+ opacity: 0.4
+ }
+
+ Text {
+ id: textDisplayType
+ visible: displayType.visible
+ z: displayType.z + 1
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.bottom: btnClose.top
+ anchors.margins: 30
+ text: ""
+ font.pixelSize: 22
+ color: "white"
+ opacity: 0.7
+ }
+
+ MoneroComponents.StandardButton {
+ id: btnClose
+ text: qsTr("Close")
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.bottom: parent.bottom
+ anchors.bottomMargin: 20
+ anchors.topMargin: 20
+ focus: true
+ onClicked: root.close()
+ }
+
+ Connections {
+ target: urSender
+ function onUpdateQrCode() {
+ qrCodeImage.reload()
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ propagateComposedEvents: true
+ onDoubleClicked: {
+ root.close()
+ }
+ }
+
+ function showQr(text) {
+ urSender.sendQrCode(text)
+ root.ur = false
+ root.active = true
+ }
+
+ function showWalletData(address, spendKey, viewKey, mnemonic, height) {
+ textDisplayType.text = qsTr("Wallet")
+ urSender.sendWallet(address, spendKey, viewKey, mnemonic, height)
+ root.ur = false
+ root.active = true
+ }
+
+ function showTxData(address, amount, paymentId, recipient, description) {
+ textDisplayType.text = qsTr("TX Data")
+ urSender.sendTx(address, amount, paymentId, recipient, description)
+ root.ur = false
+ root.active = true
+ }
+
+ function showOutputs(outputs) {
+ textDisplayType.text = qsTr("Outputs")
+ urSender.sendOutputs(outputs)
+ root.active = true
+ }
+
+ function showKeyImages(keyImages) {
+ textDisplayType.text = qsTr("Key Images")
+ urSender.sendKeyImages(keyImages)
+ root.active = true
+ }
+
+ function showUnsignedTx(tx) {
+ textDisplayType.text = qsTr("Unsigned TX")
+ urSender.sendTxUnsigned(tx)
+ root.active = true
+ }
+
+ function showSignedTx(tx) {
+ textDisplayType.text = qsTr("Signed TX")
+ urSender.sendTxSigned(tx)
+ root.active = true
+ }
+
+ function close() {
+ textDisplayType.text = ""
+ urSender.sendClear()
+ root.ur = true
+ root.active = false
+ }
+
+ Component.onCompleted: {
+ qrCodeImage.source = "image://urcode/qr"
+ }
+}
diff --git a/components/UrCodeScanner.qml b/components/UrCodeScanner.qml
new file mode 100644
index 0000000000..d128cfdbe9
--- /dev/null
+++ b/components/UrCodeScanner.qml
@@ -0,0 +1,358 @@
+import QtQuick 2.9
+import QtQml.Models 2.2
+import QtMultimedia 5.4
+import QtQuick.Dialogs 1.2
+import OtsUr 0.1
+import "." as MoneroComponents
+
+Rectangle {
+ id : root
+
+ x: 0
+ y: 0
+ z: parent.z+1
+ width: parent.width
+ height: parent.height
+
+ visible: false
+ color: "black"
+ state: "Stopped"
+
+ property bool active: false
+ property bool ur: true
+ property string errorMessage: ""
+
+ signal canceled()
+ signal qrCode(string data)
+ signal wallet(MoneroWalletData walletData)
+ signal txData(MoneroTxData data)
+ signal unsignedTx(var tx)
+ signal signedTx(var tx)
+ signal keyImages(var keyImages)
+ signal outputs(var outputs)
+
+ states: [
+ State {
+ name: "Capture"
+ when: root.active
+ StateChangeScript {
+ script: {
+ root.visible = true
+ for(var i = 0; i < QtMultimedia.availableCameras.length; i++)
+ if(QtMultimedia.availableCameras[i].deviceId === persistentSettings.lastUsedCamera) {
+ urCamera.deviceId = persistentSettings.lastUsedCamera
+ break
+ }
+ urCamera.captureMode = Camera.CaptureStillImage
+ urCamera.cameraState = Camera.ActiveState
+ urCamera.start()
+ }
+ }
+ },
+ State {
+ name: "Stopped"
+ when: !root.active
+ StateChangeScript {
+ script: {
+ urCamera.stop()
+ urScanner.stop()
+ root.ur = true
+ scanProgress.reset()
+ root.visible = false
+ urCamera.cameraState = Camera.UnloadedState
+ }
+ }
+ }
+ ]
+
+ ListModel {
+ id: availableCameras
+ Component.onCompleted: {
+ availableCameras.clear()
+ for(var i = 0; i < QtMultimedia.availableCameras.length; i++) {
+ var cam = QtMultimedia.availableCameras[i]
+ availableCameras.append({
+ column1: cam.displayName,
+ column2: cam.deviceId,
+ priority: i
+ })
+ }
+ }
+ }
+
+ UrCodeScannerImpl {
+ id: urScanner
+ objectName: "urScanner"
+ onQrDataReceived: function(data) {
+ root.active = false
+ }
+
+ onUrDataReceived: function(type, data) {
+ root.active = false
+ }
+
+ onUrDataFailed: function(error) {
+ root.cancel()
+ }
+ }
+
+ MoneroComponents.StandardButton {
+ id: btnSwitchCamera
+ visible: QtMultimedia.availableCameras.length === 2 // if the system has exact to cams, show a switch button
+ text: qsTr("Switch Camera")
+ z: viewfinder.z + 1
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: parent.top
+ anchors.topMargin: 20
+ anchors.bottomMargin: 20
+ onClicked: {
+ btnSwitchCamera.visible = false
+ urCamera.deviceId = urCamera.deviceId === QtMultimedia.availableCameras[0].deviceId ? QtMultimedia.availableCameras[1].deviceId : QtMultimedia.availableCameras[0].deviceId
+ persistentSettings.lastUsedCamera = urCamera.deviceId
+ btnSwitchCamera.visible = true
+ }
+ }
+
+ StandardDropdown {
+ id: cameraChooser
+ visible: QtMultimedia.availableCameras.length > 2 // if the system has more then 2 cams, show a list
+ z: viewfinder.z + 1
+ width: 300
+ height: 30
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: parent.top
+ anchors.topMargin: 20
+ anchors.bottomMargin: 20
+ dataModel: availableCameras
+ onChanged: {
+ urCamera.deviceId = QtMultimedia.availableCameras[cameraChooser.currentIndex].deviceId
+ persistentSettings.lastUsedCamera = urCamera.deviceId
+ }
+ }
+
+ Camera {
+ id: urCamera
+ objectName: "urCamera"
+ captureMode: Camera.CaptureStillImage
+ cameraState: Camera.UnloadedState
+
+ focus {
+ focusMode: Camera.FocusContinuous
+ }
+ }
+
+ VideoOutput {
+ id: viewfinder
+ visible: root.active == true
+
+ x: 0
+ y: btnSwitchCamera.height + 40 // 2 x 20 (margin)
+ z: parent.z+1
+ width: parent.width
+ height: parent.height - btnClose.height - btnSwitchCamera.height - 80 // 4 x 20 (margin)
+
+ source: urCamera
+ autoOrientation: true
+ focus: visible
+
+ MouseArea {
+ anchors.fill: parent
+ onPressAndHold: {
+ if (camera.lockStatus === Camera.locked)camera.unlock()
+ camera.searchAndLock()
+ }
+ onDoubleClicked: root.cancel()
+ }
+
+ Rectangle {
+ id: scanTypeFrame
+ height: scanType.height + 20
+ width: scanType.width + 30
+ z: parent.z + 1
+ radius: 16
+ color: "#FA6800"
+ opacity: 0.4
+ anchors.centerIn: scanType
+ }
+
+ Text {
+ z: scanTypeFrame.z + 1
+ anchors.centerIn: parent
+ id: scanType
+ text: ""
+ font.pixelSize: 22
+ color: "white"
+ opacity: 0.7
+ }
+
+ Rectangle {
+ id: unexpectedTypeFrame
+ visible: root.errorMessage !== ""
+ height: Math.max(unexpectedType.height + 20, scanTypeFrame.height)
+ width: Math.max(unexpectedType.width + 30, scanTypeFrame.width)
+ z: parent.z + 100
+ radius: 3
+ color: "black"
+ anchors.centerIn: unexpectedType
+ }
+
+ Text {
+ id: unexpectedType
+ visible: unexpectedTypeFrame.visible
+ text: root.errorMessage
+ z: unexpectedTypeFrame.z + 1
+ anchors.centerIn: parent
+ font.pixelSize: 22
+ color: "#FA6800"
+ }
+
+ Rectangle {
+ id: scanProgress
+ property int scannedFrames: 0
+ property int totalFrames: 0
+ property int progress: 0
+ visible: root.ur
+ height: textScanProgress.height + 10
+ width: viewfinder.contentRect.width - 40
+ z: viewfinder.z + 1
+ radius: 20
+ color: "#FA6800"
+ opacity: 0.4
+ anchors.horizontalCenter: viewfinder.horizontalCenter
+ anchors.bottom: viewfinder.bottom
+ anchors.bottomMargin: 20
+
+ function onScannedFrames(count, total) {
+ scanProgress.scannedFrames = count
+ scanProgress.totalFrames = total
+ }
+
+ function onProgress(complete) {
+ scanProgress.progress = Math.floor(complete * 100)
+ }
+
+ function reset() {
+ scanProgress.scannedFrames = 0
+ scanProgress.totalFrames = 0
+ scanProgress.progress = 0
+ }
+ }
+
+ Rectangle {
+ id: scanProgressBar
+ visible: root.ur && scanProgressBar.width > 36
+ height: scanProgress.height - 8
+ width: Math.floor((scanProgress.width - 8) * scanProgress.progress / 100)
+ x: scanProgress.x + 4
+ y: scanProgress.y + 4
+ z: scanProgress.z + 1
+ color: "#FA6800"
+ opacity: 0.8
+ radius: 16
+ }
+
+ Text {
+ id: textScanProgress
+ visible: root.ur
+ z: scanProgress.z + 2
+ anchors.centerIn: scanProgress
+ text: (scanProgress.progress > 0 || scanProgress.totalFrames > 0) ? (scanProgress.progress + "% (" + scanProgress.scannedFrames + "/" + scanProgress.totalFrames + ")") : ""
+ font.pixelSize: 22
+ color: "white"
+ opacity: 0.7
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ enabled: true
+ }
+
+ MoneroComponents.StandardButton {
+ id: btnClose
+ text: qsTr("Cancel")
+ z: viewfinder.z + 1
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.bottom: parent.bottom
+ anchors.bottomMargin: 20
+ anchors.topMargin: 20
+ onClicked: root.cancel()
+ }
+
+ function cancel() {
+ root.active = false
+ root.canceled()
+ }
+
+ function scanQrCode() {
+ root.ur = false
+ root.active = true
+ scanType.text = qsTr("Scan QR Code")
+ urScanner.qr()
+ }
+
+ function scanWallet() {
+ root.ur = false
+ root.active = true
+ scanType.text = qsTr("Scan Wallet QR Code")
+ urScanner.scanWallet()
+ }
+
+ function scanTxData() {
+ root.ur = false
+ root.active = true
+ scanType.text = qsTr("Scan Tx Data QR Code")
+ urScanner.scanTxData()
+ }
+
+ function scanOutputs() {
+ root.active = true
+ scanType.text = qsTr("Scan Outputs UR Code")
+ urScanner.scanOutputs()
+ }
+
+ function scanKeyImages() {
+ root.active = true
+ scanType.text = qsTr("Scan Key Images UR Code")
+ urScanner.scanKeyImages()
+ }
+
+ function scanUnsignedTx() {
+ root.active = true
+ scanType.text = qsTr("Scan Unsigned Transaction UR Code")
+ urScanner.scanUnsignedTx()
+ }
+
+ function scanSignedTx() {
+ root.active = true
+ scanType.text = qsTr("Scan Signed Transaction UR Code")
+ urScanner.scanSignedTx()
+ }
+
+ function onUnexpectedFrame(urType){
+ root.errorMessage = qsTr("Unexpected UR type: ") + urType
+ }
+
+ function onDecodedFrame(unused) {
+ root.errorMessage = ""
+ }
+
+ Component.onCompleted: {
+ if( QtMultimedia.availableCameras.length === 0) {
+ console.warn("No camera available. Disable qrScannerEnabled")
+ appWindow.qrScannerEnabled = false
+ return
+ }
+ urScanner.outputs.connect(root.outputs)
+ urScanner.keyImages.connect(root.keyImages)
+ urScanner.unsignedTx.connect(root.unsignedTx)
+ urScanner.signedTx.connect(root.signedTx)
+ urScanner.txData.connect(root.txData)
+ urScanner.wallet.connect(root.wallet)
+ urScanner.qrCaptureStarted.connect(scanProgress.reset)
+ urScanner.scannedFrames.connect(scanProgress.onScannedFrames)
+ urScanner.estimatedCompletedPercentage.connect(scanProgress.onProgress)
+ urScanner.unexpectedUrType.connect(root.onUnexpectedFrame)
+ urScanner.decodedFrame.connect(root.onDecodedFrame)
+ }
+}
diff --git a/images/restore-wallet-from-qr.png b/images/restore-wallet-from-qr.png
new file mode 100644
index 0000000000..856329748d
Binary files /dev/null and b/images/restore-wallet-from-qr.png differ
diff --git a/images/restore-wallet-from-qr@2x.png b/images/restore-wallet-from-qr@2x.png
new file mode 100644
index 0000000000..ecf580371f
Binary files /dev/null and b/images/restore-wallet-from-qr@2x.png differ
diff --git a/main.qml b/main.qml
index 98fb43c01d..1acaa5b3b7 100644
--- a/main.qml
+++ b/main.qml
@@ -82,12 +82,14 @@ ApplicationWindow {
property string walletName
property bool viewOnly: false
property bool foundNewBlock: false
- property bool qrScannerEnabled: (typeof builtWithScanner != "undefined") && builtWithScanner
+ property bool qrScannerEnabled: ((typeof builtWithScanner != "undefined") && builtWithScanner) || ((typeof builtWithOtsUr != "undefined") && builtWithOtsUr)
property int blocksToSync: 1
property int firstBlockSeen
property bool isMining: false
property int walletMode: persistentSettings.walletMode
property var cameraUi
+ property var urScannerUi
+ property var urDisplay
property bool androidCloseTapped: false;
property int userLastActive; // epoch
// Default daemon addresses
@@ -298,6 +300,7 @@ ApplicationWindow {
currentWallet.deviceButtonPressed.disconnect(onDeviceButtonPressed);
currentWallet.walletPassphraseNeeded.disconnect(onWalletPassphraseNeededWallet);
currentWallet.transactionCommitted.disconnect(onTransactionCommitted);
+ currentWallet.transactionCommittedForExport.disconnect(onTransactionCommittedForExport);
middlePanel.paymentClicked.disconnect(handlePayment);
middlePanel.sweepUnmixableClicked.disconnect(handleSweepUnmixable);
middlePanel.getProofClicked.disconnect(handleGetProof);
@@ -345,6 +348,7 @@ ApplicationWindow {
currentWallet.deviceButtonPressed.connect(onDeviceButtonPressed);
currentWallet.walletPassphraseNeeded.connect(onWalletPassphraseNeededWallet);
currentWallet.transactionCommitted.connect(onTransactionCommitted);
+ currentWallet.transactionCommittedForExport.connect(onTransactionCommittedForExport);
currentWallet.proxyAddress = Qt.binding(persistentSettings.getWalletProxyAddress);
middlePanel.paymentClicked.connect(handlePayment);
middlePanel.sweepUnmixableClicked.connect(handleSweepUnmixable);
@@ -850,7 +854,7 @@ ApplicationWindow {
// here we update txConfirmationPopup
txConfirmationPopup.transactionAmount = Utils.removeTrailingZeros(walletManager.displayAmount(transaction.amount));
txConfirmationPopup.transactionFee = Utils.removeTrailingZeros(walletManager.displayAmount(transaction.fee));
- txConfirmationPopup.confirmButton.text = viewOnly ? qsTr("Save as file") : qsTr("Confirm") + translationManager.emptyString;
+ txConfirmationPopup.confirmButton.text = viewOnly ? (persistentSettings.useURCode?qsTr("Show UR code"):qsTr("Save as file")) : qsTr("Confirm") + translationManager.emptyString;
txConfirmationPopup.confirmButton.rightIcon = viewOnly ? "" : "qrc:///images/rightArrow.png"
}
}
@@ -923,13 +927,13 @@ ApplicationWindow {
txConfirmationPopup.sweepUnmixable = true;
transaction = currentWallet.createSweepUnmixableTransaction();
- if (transaction.status !== PendingTransaction.Status_Ok) {
+ if (transaction.status !== PendingTransaction.Status_Ok) { // TODO: Dialog should only inform and offer only OK button IMO, informationPopup instead?
console.error("Can't create transaction: ", transaction.errorString);
txConfirmationPopup.errorText.text = qsTr("Can't create transaction: ") + transaction.errorString + translationManager.emptyString
// deleting transaction object, we don't want memleaks
currentWallet.disposeTransaction(transaction);
- } else if (transaction.txCount == 0) {
+ } else if (transaction.txCount == 0) { // TODO: Dialog should only inform and offer only OK button IMO, informationPopup instead?
console.error("No unmixable outputs to sweep");
txConfirmationPopup.errorText.text = qsTr("No unmixable outputs to sweep") + translationManager.emptyString
// deleting transaction object, we don't want memleaks
@@ -946,8 +950,13 @@ ApplicationWindow {
// called after user confirms transaction
function handleTransactionConfirmed(fileName) {
- // View only wallet - we save the tx
- if(viewOnly){
+ if(viewOnly && persistentSettings.useURCode) {
+ appWindow.showProcessingSplash(qsTr("Preparing transaction ..."));
+ currentWallet.commitTransactionForExportAsync(transaction);
+ return
+ }
+ // View only wallet - we save the tx to file if UR is not enabled
+ if(viewOnly && !persistentSettings.useURCode){
// No file specified - abort
if(!saveTxDialog.fileUrl) {
currentWallet.disposeTransaction(transaction)
@@ -992,6 +1001,35 @@ ApplicationWindow {
});
}
+ function onTransactionCommittedForExport(txString, transaction, txid) {
+ hideProcessingSplash();
+ if (txString.length === 0) {
+ console.log("Error committing transaction: " + transaction.errorString);
+ informationPopup.title = qsTr("Error") + translationManager.emptyString
+ informationPopup.text = qsTr("Couldn't send the money: ") + transaction.errorString
+ informationPopup.icon = StandardIcon.Critical
+ informationPopup.onCloseCallback = null;
+ informationPopup.open();
+ } else {
+ if (txConfirmationPopup.transactionDescription.length > 0) {
+ for (var i = 0; i < txid.length; ++i)
+ currentWallet.setUserNote(txid[i], txConfirmationPopup.transactionDescription);
+ }
+
+ // Clear tx fields
+ middlePanel.transferView.clearFields()
+ txConfirmationPopup.clearFields()
+ urDisplay.showUnsignedTx(txString)
+ }
+ currentWallet.refresh()
+ currentWallet.disposeTransaction(transaction)
+ currentWallet.storeAsync(function(success) {
+ if (!success) {
+ appWindow.showStatusMessage(qsTr("Failed to store the wallet"), 3);
+ }
+ });
+ }
+
function doSearchInHistory(searchTerm) {
middlePanel.searchInHistory(searchTerm);
leftPanel.selectItem(middlePanel.state)
@@ -1330,7 +1368,7 @@ ApplicationWindow {
// Connect app exit to qml window exit handling
mainApp.closing.connect(appWindow.close);
- if( appWindow.qrScannerEnabled ){
+ if(builtWithScanner && appWindow.qrScannerEnabled){
console.log("qrScannerEnabled : load component QRCodeScanner");
var component = Qt.createComponent("components/QRCodeScanner.qml");
if (component.status == Component.Ready) {
@@ -1340,7 +1378,26 @@ ApplicationWindow {
console.log("component not READY !!!");
appWindow.qrScannerEnabled = false;
}
- } else console.log("qrScannerEnabled disabled");
+ }
+
+ if(builtWithOtsUr && appWindow.qrScannerEnabled){
+ var urScanComponent = Qt.createComponent("components/UrCodeScanner.qml");
+ console.warn(urScanComponent.errorString())
+ if (urScanComponent.status == Component.Ready) {
+ urScannerUi = urScanComponent.createObject(appWindow);
+ } else {
+ console.warn("UR Scanner component not READY !!!");
+ }
+ }
+
+ if(builtWithOtsUr) {
+ var urComponent = Qt.createComponent("components/UrCode.qml");
+ if (urComponent.status == Component.Ready) {
+ urDisplay = urComponent.createObject(appWindow);
+ } else {
+ console.warn("UR Display component not READY !!!");
+ }
+ }
if(!walletsFound()) {
wizard.wizardState = "wizardLanguage";
@@ -1439,6 +1496,8 @@ ApplicationWindow {
property int lockOnUserInActivityInterval: 10 // minutes
property bool blackTheme: MoneroComponents.Style.blackTheme
property bool checkForUpdates: true
+ property bool useURCode: false
+ property string lastUsedCamera: ""
property bool autosave: true
property int autosaveMinutes: 10
property bool pruneBlockchain: false
@@ -1584,7 +1643,7 @@ ApplicationWindow {
onAccepted: {
var handleAccepted = function() {
// Save transaction to file if view only wallet
- if (viewOnly) {
+ if (viewOnly && !persistentSettings.useURCode) {
saveTxDialog.open();
} else {
handleTransactionConfirmed()
@@ -1605,7 +1664,7 @@ ApplicationWindow {
passwordDialog.open(
"",
"",
- (appWindow.viewOnly ? qsTr("Save transaction file") : qsTr("Send transaction")) + translationManager.emptyString,
+ (appWindow.viewOnly ? (persistentSettings.useURCode?qsTr("Show transaction UR code"):qsTr("Save transaction file")) : qsTr("Send transaction")) + translationManager.emptyString,
appWindow.viewOnly ? "" : FontAwesome.arrowCircleRight);
}
}
@@ -2131,9 +2190,13 @@ ApplicationWindow {
onClosing: {
close.accepted = false;
console.log("blocking close event");
+ if(builtWithOtsUr) {
+ urScannerUi.cancel()
+ urDisplay.close()
+ }
if(isAndroid) {
console.log("blocking android exit");
- if(qrScannerEnabled)
+ if(qrScannerEnabled && builtWithScanner)
cameraUi.state = "Stopped"
if(!androidCloseTapped) {
@@ -2143,8 +2206,6 @@ ApplicationWindow {
// first close
return;
}
-
-
}
// If daemon is running - prompt user before exiting
diff --git a/monero b/monero
index b089f9ee69..7e73ecd120 160000
--- a/monero
+++ b/monero
@@ -1 +1 @@
-Subproject commit b089f9ee69924882c5d14dd1a6991deb05d9d1cd
+Subproject commit 7e73ecd120795fd1abe95810ad56053c948dcc8c
diff --git a/pages/Transfer.qml b/pages/Transfer.qml
index 66e0b66b49..bb898aa15b 100644
--- a/pages/Transfer.qml
+++ b/pages/Transfer.qml
@@ -300,8 +300,15 @@ Rectangle {
visible: appWindow.qrScannerEnabled
tooltip: qsTr("Scan QR code") + translationManager.emptyString
onClicked: {
- cameraUi.state = "Capture"
- cameraUi.qrcode_decoded.connect(updateFromQrCode)
+ if(builtWithOtsUr) {
+ urScannerUi.txData.connect(root.txDataFromScanner)
+ urScannerUi.canceled.connect(root.scanCanceled)
+ urScannerUi.scanTxData()
+
+ } else {
+ cameraUi.state = "Capture"
+ cameraUi.qrcode_decoded.connect(updateFromQrCode)
+ }
}
}
@@ -881,13 +888,24 @@ Rectangle {
button1.enabled: appWindow.viewOnly
button1.onClicked: {
console.log("Transfer: export outputs clicked")
- exportOutputsDialog.open();
+ if(persistentSettings.useURCode) {
+ var outputs = currentWallet.exportOutputsAsString(true);
+ urDisplay.showOutputs(outputs)
+ } else {
+ exportOutputsDialog.open();
+ }
}
button2.text: qsTr("Import") + translationManager.emptyString
button2.enabled: !appWindow.viewOnly
button2.onClicked: {
console.log("Transfer: import outputs clicked")
- importOutputsDialog.open();
+ if(persistentSettings.useURCode) {
+ urScannerUi.outputs.connect(root.importOutputs)
+ urScannerUi.canceled.connect(root.scanCanceled)
+ urScannerUi.scanOutputs()
+ } else {
+ importOutputsDialog.open();
+ }
}
tooltip: {
var header = qsTr("Required for cold wallets to sign their corresponding key images") + translationManager.emptyString;
@@ -907,13 +925,24 @@ Rectangle {
button1.enabled: !appWindow.viewOnly
button1.onClicked: {
console.log("Transfer: export key images clicked")
- exportKeyImagesDialog.open();
+ if(persistentSettings.useURCode) {
+ var keyImages = currentWallet.exportKeyImagesAsString(true);
+ urDisplay.showKeyImages(keyImages)
+ } else {
+ exportKeyImagesDialog.open();
+ }
}
button2.text: qsTr("Import") + translationManager.emptyString
button2.enabled: appWindow.viewOnly && appWindow.isTrustedDaemon()
button2.onClicked: {
console.log("Transfer: import key images clicked")
- importKeyImagesDialog.open();
+ if(persistentSettings.useURCode) {
+ urScannerUi.keyImages.connect(root.importKeyImages)
+ urScannerUi.canceled.connect(root.scanCanceled)
+ urScannerUi.scanKeyImages()
+ } else {
+ importKeyImagesDialog.open();
+ }
}
tooltip: {
var errorMessage = "";
@@ -946,13 +975,25 @@ Rectangle {
button2.enabled: !appWindow.viewOnly
button2.onClicked: {
console.log("Transfer: sign tx clicked")
- signTxDialog.open();
+ if(persistentSettings.useURCode) {
+ urScannerUi.canceled.connect(root.scanCanceled)
+ urScannerUi.unsignedTx.connect(root.signTx)
+ urScannerUi.scanUnsignedTx()
+ } else {
+ signTxDialog.open();
+ }
}
button3.text: qsTr("Submit") + translationManager.emptyString
button3.enabled: appWindow.viewOnly
button3.onClicked: {
console.log("Transfer: submit tx clicked")
- submitTxDialog.open();
+ if(persistentSettings.useURCode) {
+ urScannerUi.canceled.connect(root.scanCanceled)
+ urScannerUi.signedTx.connect(root.submitTx)
+ urScannerUi.scanSignedTx()
+ } else {
+ submitTxDialog.open();
+ }
}
tooltip: {
var errorMessage = "";
@@ -1029,7 +1070,7 @@ Rectangle {
}
}
- //SignTxDialog
+ //SubmitTxDialog
FileDialog {
id: submitTxDialog
title: qsTr("Please choose a file") + translationManager.emptyString
@@ -1189,4 +1230,87 @@ Rectangle {
fillPaymentDetails(address, paymentId, amount, description);
}
+
+ function txDataFromScanner(txData) {
+ disconnectCameraUi()
+ middlePanel.state = 'Transfer';
+ fillPaymentDetails(txData.address, txData.payment_id, txData.amount, txData.description);
+ }
+
+ function signTx(tx) {
+ disconnectCameraUi()
+ transaction = currentWallet.loadTxString(tx)
+ if (transaction.status !== PendingTransaction.Status_Ok) {
+ console.error("Can't load unsigned transaction: ", transaction.errorString);
+ informationPopup.title = qsTr("Error") + translationManager.emptyString;
+ informationPopup.text = qsTr("Can't load unsigned transaction: ") + transaction.errorString
+ informationPopup.icon = StandardIcon.Critical
+ informationPopup.onCloseCallback = null
+ informationPopup.open();
+ // deleting transaction object, we don't want memleaks
+ transaction.destroy();
+ } else {
+ confirmationDialog.text = qsTr("\nConfirmation message:\n ") + transaction.confirmationMessage
+ console.log(transaction.confirmationMessage);
+
+ // Show confirmation dialog
+ confirmationDialog.title = qsTr("Confirmation") + translationManager.emptyString
+ confirmationDialog.icon = StandardIcon.Question
+ confirmationDialog.onAcceptedCallback = function() {
+ var signed_tx = transaction.signAsString();
+ transaction.destroy();
+ urDisplay.showSignedTx(signed_tx)
+ };
+ confirmationDialog.onRejectedCallback = transaction.destroy;
+
+ confirmationDialog.open()
+ }
+ }
+
+ function submitTx(tx) {
+ disconnectCameraUi()
+ if(!currentWallet.submitTxString(tx)){
+ informationPopup.title = qsTr("Error") + translationManager.emptyString;
+ informationPopup.text = qsTr("Can't submit transaction: ") + currentWallet.errorString
+ informationPopup.icon = StandardIcon.Critical
+ informationPopup.onCloseCallback = null
+ informationPopup.open();
+ } else {
+ informationPopup.title = qsTr("Information") + translationManager.emptyString
+ informationPopup.text = qsTr("Monero sent successfully") + translationManager.emptyString
+ informationPopup.icon = StandardIcon.Information
+ informationPopup.onCloseCallback = null
+ informationPopup.open();
+ }
+ }
+
+ function importOutputs(outputs) {
+ disconnectCameraUi()
+ if (currentWallet.importOutputsFromString(outputs)) {
+ appWindow.showStatusMessage(qsTr("Outputs successfully imported to wallet") + translationManager.emptyString, 3);
+ } else {
+ appWindow.showStatusMessage(currentWallet.errorString, 5);
+ }
+ }
+
+ function importKeyImages(keyImages) {
+ disconnectCameraUi()
+ if (currentWallet.importKeyImagesFromString(keyImages)) {
+ appWindow.showStatusMessage(qsTr("Key images successfully imported to wallet") + translationManager.emptyString, 3);
+ } else {
+ appWindow.showStatusMessage(currentWallet.errorString, 5);
+ }
+ }
+
+ function scanCanceled() {
+ disconnectCameraUi()
+ }
+
+ function disconnectCameraUi() {
+ urScannerUi.outputs.disconnect(root.importOutputs)
+ urScannerUi.canceled.disconnect(root.scanCanceled)
+ urScannerUi.keyImages.disconnect(root.importKeyImages)
+ urScannerUi.unsignedTx.disconnect(root.signTx)
+ urScannerUi.signedTx.disconnect(root.submitTx)
+ }
}
diff --git a/pages/settings/SettingsLayout.qml b/pages/settings/SettingsLayout.qml
index c2dfc7e662..9eb859521c 100644
--- a/pages/settings/SettingsLayout.qml
+++ b/pages/settings/SettingsLayout.qml
@@ -66,6 +66,15 @@ Rectangle {
text: qsTr("Check for updates periodically") + translationManager.emptyString
}
+ MoneroComponents.CheckBox {
+ id: useUrCheckBox
+ visible: builtWithOtsUr
+ enabled: builtWithOtsUr
+ checked: persistentSettings.useURCode && builtWithOtsUr
+ onClicked: persistentSettings.useURCode = !persistentSettings.useURCode
+ text: qsTr("Use UR Code instead of files for cold wallet") + translationManager.emptyString
+ }
+
MoneroComponents.CheckBox {
checked: persistentSettings.displayWalletNameInTitleBar
onClicked: persistentSettings.displayWalletNameInTitleBar = !persistentSettings.displayWalletNameInTitleBar
diff --git a/qml.qrc b/qml.qrc
index d4bd45a618..be8bc34d3e 100644
--- a/qml.qrc
+++ b/qml.qrc
@@ -229,6 +229,8 @@
images/world-flags-globe.png
images/restore-wallet-from-hardware@2x.png
images/restore-wallet-from-hardware.png
+ images/restore-wallet-from-qr@2x.png
+ images/restore-wallet-from-qr.png
images/open-wallet-from-file@2x.png
images/open-wallet-from-file.png
images/open-wallet-from-file-mainnet@2x.png
@@ -298,5 +300,7 @@
wizard/SeedListItem.qml
wizard/SeedListGrid.qml
wizard/template.pdf
+ components/UrCode.qml
+ components/UrCodeScanner.qml
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index b5718df3a2..218930b54e 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -103,6 +103,7 @@ target_include_directories(monero-wallet-gui PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/libwalletqt
${CMAKE_CURRENT_SOURCE_DIR}/model
${CMAKE_CURRENT_SOURCE_DIR}/QR-Code-scanner
+ ${CMAKE_CURRENT_SOURCE_DIR}/ur/qtquick
${CMAKE_CURRENT_SOURCE_DIR}/zxcvbn-c
${X11_INCLUDE_DIR}
)
@@ -154,6 +155,11 @@ if(WITH_SCANNER)
endif()
endif()
+if(WITH_OTS_UR)
+ add_subdirectory(otsur)
+ target_link_libraries(monero-wallet-gui otsqtquick)
+endif()
+
add_custom_command(TARGET monero-wallet-gui POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy $ $)
include(Deploy)
diff --git a/src/libwalletqt/PendingTransaction.cpp b/src/libwalletqt/PendingTransaction.cpp
index 72c934ff68..548160ecb8 100644
--- a/src/libwalletqt/PendingTransaction.cpp
+++ b/src/libwalletqt/PendingTransaction.cpp
@@ -47,6 +47,11 @@ bool PendingTransaction::commit()
return m_pimpl->commit(m_fileName.toStdString());
}
+QByteArray PendingTransaction::commitAsString() const
+{
+ return QByteArray::fromStdString(m_pimpl->commit_string());
+}
+
quint64 PendingTransaction::amount() const
{
return m_pimpl->amount();
diff --git a/src/libwalletqt/PendingTransaction.h b/src/libwalletqt/PendingTransaction.h
index 08b6a55776..27148a3de3 100644
--- a/src/libwalletqt/PendingTransaction.h
+++ b/src/libwalletqt/PendingTransaction.h
@@ -70,6 +70,7 @@ class PendingTransaction : public QObject
Status status() const;
QString errorString() const;
Q_INVOKABLE bool commit();
+ Q_INVOKABLE QByteArray commitAsString() const;
quint64 amount() const;
quint64 dust() const;
quint64 fee() const;
diff --git a/src/libwalletqt/UnsignedTransaction.cpp b/src/libwalletqt/UnsignedTransaction.cpp
index fcd425634e..0b8936bdbc 100644
--- a/src/libwalletqt/UnsignedTransaction.cpp
+++ b/src/libwalletqt/UnsignedTransaction.cpp
@@ -103,6 +103,11 @@ bool UnsignedTransaction::sign(const QString &fileName) const
return m_walletImpl->exportKeyImages(fileName.toStdString() + "_keyImages");
}
+QByteArray UnsignedTransaction::signAsString() const
+{
+ return QByteArray::fromStdString(m_pimpl->signAsString());
+}
+
void UnsignedTransaction::setFilename(const QString &fileName)
{
m_fileName = fileName;
diff --git a/src/libwalletqt/UnsignedTransaction.h b/src/libwalletqt/UnsignedTransaction.h
index 1ce7186beb..7257638558 100644
--- a/src/libwalletqt/UnsignedTransaction.h
+++ b/src/libwalletqt/UnsignedTransaction.h
@@ -65,6 +65,7 @@ class UnsignedTransaction : public QObject
QString confirmationMessage() const;
quint64 minMixinCount() const;
Q_INVOKABLE bool sign(const QString &fileName) const;
+ Q_INVOKABLE QByteArray signAsString() const;
Q_INVOKABLE void setFilename(const QString &fileName);
private:
diff --git a/src/libwalletqt/Wallet.cpp b/src/libwalletqt/Wallet.cpp
index cec4ff63c8..1bfe637956 100644
--- a/src/libwalletqt/Wallet.cpp
+++ b/src/libwalletqt/Wallet.cpp
@@ -506,15 +506,33 @@ bool Wallet::exportKeyImages(const QString& path, bool all)
return m_walletImpl->exportKeyImages(path.toStdString(), all);
}
+QByteArray Wallet::exportKeyImagesAsString(bool all)
+{
+ return QByteArray::fromStdString(m_walletImpl->exportKeyImagesAsString(all));
+}
+
bool Wallet::importKeyImages(const QString& path)
{
return m_walletImpl->importKeyImages(path.toStdString());
}
+bool Wallet::importKeyImagesFromString(const QByteArray& data)
+{
+ return m_walletImpl->importKeyImagesFromString(data.toStdString());
+}
+
+QByteArray Wallet::exportOutputsAsString(bool all) const {
+ return QByteArray::fromStdString(m_walletImpl->exportOutputsAsString(all));
+}
+
bool Wallet::exportOutputs(const QString& path, bool all) {
return m_walletImpl->exportOutputs(path.toStdString(), all);
}
+bool Wallet::importOutputsFromString(const QByteArray& data) {
+ return m_walletImpl->importOutputsFromString(data.toStdString());
+}
+
bool Wallet::importOutputs(const QString& path) {
return m_walletImpl->importOutputs(path.toStdString());
}
@@ -650,6 +668,14 @@ UnsignedTransaction * Wallet::loadTxFile(const QString &fileName)
return result;
}
+UnsignedTransaction * Wallet::loadTxString(const QByteArray &data)
+{
+ qDebug() << "Trying to sign";
+ Monero::UnsignedTransaction * ptImpl = m_walletImpl->loadUnsignedTxFromString(data.toStdString());
+ UnsignedTransaction * result = new UnsignedTransaction(ptImpl, m_walletImpl, this);
+ return result;
+}
+
bool Wallet::submitTxFile(const QString &fileName) const
{
qDebug() << "Trying to submit " << fileName;
@@ -659,6 +685,12 @@ bool Wallet::submitTxFile(const QString &fileName) const
return m_walletImpl->importKeyImages(fileName.toStdString() + "_keyImages");
}
+bool Wallet::submitTxString(const QByteArray &data) const
+{
+ qDebug() << "Trying to submit";
+ return m_walletImpl->submitTransactionFromString(data.toStdString()); // TODO: Why importing key images? This had should be done before... (remove this comment after clarification)
+}
+
void Wallet::commitTransactionAsync(PendingTransaction *t)
{
m_scheduler.run([this, t] {
@@ -667,6 +699,14 @@ void Wallet::commitTransactionAsync(PendingTransaction *t)
});
}
+void Wallet::commitTransactionForExportAsync(PendingTransaction *t)
+{
+ m_scheduler.run([this, t] {
+ auto txIdList = t->txid(); // retrieve before commit
+ emit transactionCommittedForExport(t->commitAsString(), t, txIdList);
+ });
+}
+
void Wallet::disposeTransaction(PendingTransaction *t)
{
m_walletImpl->disposeTransaction(t->m_pimpl);
diff --git a/src/libwalletqt/Wallet.h b/src/libwalletqt/Wallet.h
index 398d19c622..f7947b8273 100644
--- a/src/libwalletqt/Wallet.h
+++ b/src/libwalletqt/Wallet.h
@@ -205,10 +205,14 @@ class Wallet : public QObject, public PassprasePrompter
Q_INVOKABLE void refreshHeightAsync();
//! export/import key images
+ Q_INVOKABLE QByteArray exportKeyImagesAsString(bool all = false);
+ Q_INVOKABLE bool importKeyImagesFromString(const QByteArray &data);
Q_INVOKABLE bool exportKeyImages(const QString& path, bool all = false);
Q_INVOKABLE bool importKeyImages(const QString& path);
//! export/import outputs
+ Q_INVOKABLE QByteArray exportOutputsAsString(bool all = false) const;
+ Q_INVOKABLE bool importOutputsFromString(const QByteArray& data);
Q_INVOKABLE bool exportOutputs(const QString& path, bool all = false);
Q_INVOKABLE bool importOutputs(const QString& path);
@@ -245,14 +249,19 @@ class Wallet : public QObject, public PassprasePrompter
Q_INVOKABLE void createSweepUnmixableTransactionAsync();
//! Sign a transfer from file
+ Q_INVOKABLE UnsignedTransaction * loadTxString(const QByteArray &data);
Q_INVOKABLE UnsignedTransaction * loadTxFile(const QString &fileName);
//! Submit a transfer from file
+ Q_INVOKABLE bool submitTxString(const QByteArray &data) const;
Q_INVOKABLE bool submitTxFile(const QString &fileName) const;
//! asynchronous transaction commit
Q_INVOKABLE void commitTransactionAsync(PendingTransaction * t);
+ //! asynchronous transaction commit for export
+ Q_INVOKABLE void commitTransactionForExportAsync(PendingTransaction * t);
+
//! deletes transaction and frees memory
Q_INVOKABLE void disposeTransaction(PendingTransaction * t);
@@ -376,6 +385,7 @@ class Wallet : public QObject, public PassprasePrompter
void deviceButtonPressed();
void walletPassphraseNeeded(bool onDevice);
void transactionCommitted(bool status, PendingTransaction *t, const QStringList& txid);
+ void transactionCommittedForExport(const QByteArray &txAsString, PendingTransaction *t, const QStringList& txid);
void heightRefreshed(quint64 walletHeight, quint64 daemonHeight, quint64 targetHeight) const;
void deviceShowAddressShowed();
diff --git a/src/main/main.cpp b/src/main/main.cpp
index 38901d3ff1..adc9c624e0 100644
--- a/src/main/main.cpp
+++ b/src/main/main.cpp
@@ -87,7 +87,10 @@
#include "qt/macoshelper.h"
#endif
-#ifdef WITH_SCANNER
+#ifdef WITH_OTS_UR
+#include "otsur/qtquick/UrRegister.h"
+// use scanner only if OTS UR is not used, how OTS UR can provide QR Scan, too.
+#elif WITH_SCANNER
#include "QR-Code-scanner/QrCodeScanner.h"
#endif
@@ -440,7 +443,9 @@ Verify update binary using 'shasum'-compatible (SHA256 algo) output signed by tw
qRegisterMetaType();
qmlRegisterType("moneroComponents.NetworkType", 1, 0, "NetworkType");
-#ifdef WITH_SCANNER
+#ifdef WITH_OTS_UR
+ OtsUr::registerTypes();
+#elif WITH_SCANNER
qmlRegisterType("moneroComponents.QRCodeScanner", 1, 0, "QRCodeScanner");
#endif
@@ -518,10 +523,15 @@ Verify update binary using 'shasum'-compatible (SHA256 algo) output signed by tw
engine.rootContext()->setContextProperty("socksProxyFlagSet", parser.isSet(socksProxyOption));
bool builtWithScanner = false;
-#ifdef WITH_SCANNER
+ bool builtWithOtsUr = false;
+#ifdef WITH_OTS_UR
+ builtWithOtsUr = true;
+ OtsUr::setupContext(engine);
+#elif WITH_SCANNER
builtWithScanner = true;
#endif
engine.rootContext()->setContextProperty("builtWithScanner", builtWithScanner);
+ engine.rootContext()->setContextProperty("builtWithOtsUr", builtWithOtsUr);
bool builtWithDesktopEntry = false;
#ifdef WITH_DESKTOP_ENTRY
@@ -549,7 +559,9 @@ Verify update binary using 'shasum'-compatible (SHA256 algo) output signed by tw
if (parser.isSet(testQmlOption))
return 0;
-#ifdef WITH_SCANNER
+#if WITH_OTS_UR
+ OtsUr::setupCamera(engine);
+#elif WITH_SCANNER
QObject *qmlCamera = rootObject->findChild("qrCameraQML");
if (qmlCamera)
{
diff --git a/src/otsur/CMakeLists.txt b/src/otsur/CMakeLists.txt
new file mode 100644
index 0000000000..012ec2d20e
--- /dev/null
+++ b/src/otsur/CMakeLists.txt
@@ -0,0 +1,16 @@
+cmake_minimum_required(VERSION 3.10)
+project(libotsur VERSION 0.1 LANGUAGES C CXX)
+
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+set(CMAKE_POSITION_INDEPENDENT_CODE ON)
+
+option(USE_EXTERNAL_QRENCODE "Use an external qrencode library" OFF)
+
+add_subdirectory(bcur)
+add_subdirectory(qrcode)
+add_subdirectory(data)
+add_subdirectory(qtquick)
+if(NOT DEFINED USE_EXTERNAL_QRENCODE OR NOT USE_EXTERNAL_QRENCODE)
+ add_subdirectory(qrencode)
+endif()
diff --git a/src/otsur/bcur/CMakeLists.txt b/src/otsur/bcur/CMakeLists.txt
new file mode 100644
index 0000000000..344af14cc2
--- /dev/null
+++ b/src/otsur/bcur/CMakeLists.txt
@@ -0,0 +1,34 @@
+set(BCUR_SOURCE
+ bytewords.cpp
+ crc32.c
+ fountain-decoder.cpp
+ fountain-encoder.cpp
+ fountain-utils.cpp
+ memzero.c
+ random-sampler.cpp
+ sha2.c
+ ur.cpp
+ ur-decoder.cpp
+ ur-encoder.cpp
+ utils.cpp
+ xoshiro256.cpp)
+set(BCUR_HEADERS
+ bc-ur.hpp
+ bytewords.hpp
+ cbor-lite.hpp
+ crc32.h
+ fountain-decoder.hpp
+ fountain-encoder.hpp
+ fountain-utils.hpp
+ memzero.h
+ random-sampler.hpp
+ sha2.h
+ ur-decoder.hpp
+ ur-encoder.hpp
+ ur.hpp
+ utils.hpp
+ xoshiro256.hpp
+)
+add_library(bcur STATIC ${BCUR_SOURCE} ${BCUR_HEADERS})
+target_include_directories(bcur PUBLIC
+ ${CMAKE_CURRENT_SOURCE_DIR})
diff --git a/src/otsur/bcur/README.md b/src/otsur/bcur/README.md
new file mode 100644
index 0000000000..478fce3667
--- /dev/null
+++ b/src/otsur/bcur/README.md
@@ -0,0 +1,2 @@
+vendored from https://github.com/BlockchainCommons/bc-ur
+2bfc3fd396498c2519273aeaa732abf7ea7d24b8
\ No newline at end of file
diff --git a/src/otsur/bcur/bc-ur.hpp b/src/otsur/bcur/bc-ur.hpp
new file mode 100644
index 0000000000..2f695ee77f
--- /dev/null
+++ b/src/otsur/bcur/bc-ur.hpp
@@ -0,0 +1,28 @@
+//
+// bc-ur.hpp
+//
+// Copyright © 2020 by Blockchain Commons, LLC
+// Licensed under the "BSD-2-Clause Plus Patent License"
+//
+
+#ifndef BC_UR_HPP
+#define BC_UR_HPP
+
+#include "ur.hpp"
+#include "ur-encoder.hpp"
+#include "ur-decoder.hpp"
+#include "fountain-encoder.hpp"
+#include "fountain-decoder.hpp"
+#include "fountain-utils.hpp"
+#include "utils.hpp"
+#include "bytewords.hpp"
+#include "xoshiro256.hpp"
+#include "random-sampler.hpp"
+
+namespace ur {
+
+#include "cbor-lite.hpp"
+
+}
+
+#endif // BC_UR_HPP
diff --git a/src/otsur/bcur/bytewords.cpp b/src/otsur/bcur/bytewords.cpp
new file mode 100644
index 0000000000..90ca65ad21
--- /dev/null
+++ b/src/otsur/bcur/bytewords.cpp
@@ -0,0 +1,170 @@
+//
+// bytewords.cpp
+//
+// Copyright © 2020 by Blockchain Commons, LLC
+// Licensed under the "BSD-2-Clause Plus Patent License"
+//
+
+#include "bytewords.hpp"
+#include "utils.hpp"
+#include
+#include
+#include
+
+namespace ur {
+
+using namespace std;
+
+static const char* bytewords = "ableacidalsoapexaquaarchatomauntawayaxisbackbaldbarnbeltbetabiasbluebodybragbrewbulbbuzzcalmcashcatschefcityclawcodecolacookcostcruxcurlcuspcyandarkdatadaysdelidicedietdoordowndrawdropdrumdulldutyeacheasyechoedgeepicevenexamexiteyesfactfairfernfigsfilmfishfizzflapflewfluxfoxyfreefrogfuelfundgalagamegeargemsgiftgirlglowgoodgraygrimgurugushgyrohalfhanghardhawkheathelphighhillholyhopehornhutsicedideaidleinchinkyintoirisironitemjadejazzjoinjoltjowljudojugsjumpjunkjurykeepkenokeptkeyskickkilnkingkitekiwiknoblamblavalazyleaflegsliarlimplionlistlogoloudloveluaulucklungmainmanymathmazememomenumeowmildmintmissmonknailnavyneednewsnextnoonnotenumbobeyoboeomitonyxopenovalowlspaidpartpeckplaypluspoempoolposepuffpumapurrquadquizraceramprealredorichroadrockroofrubyruinrunsrustsafesagascarsetssilkskewslotsoapsolosongstubsurfswantacotasktaxitenttiedtimetinytoiltombtoystriptunatwinuglyundouniturgeuservastveryvetovialvibeviewvisavoidvowswallwandwarmwaspwavewaxywebswhatwhenwhizwolfworkyankyawnyellyogayurtzapszerozestzinczonezoom";
+
+uint8_t decode_word(const string& word, size_t word_len) {
+ if(word.length() != word_len) {
+ throw runtime_error("Invalid Bytewords.");
+ }
+
+ static int16_t* array = NULL;
+ const size_t dim = 26;
+
+ // Since the first and last letters of each Byteword are unique,
+ // we can use them as indexes into a two-dimensional lookup table.
+ // This table is generated lazily.
+ if(array == NULL) {
+ const size_t array_len = dim * dim;
+ array = (int16_t*)malloc(array_len * sizeof(int16_t));
+ for(size_t i = 0; i < array_len; i++) {
+ array[i] = -1;
+ }
+ for(size_t i = 0; i < 256; i++) {
+ const char* byteword = bytewords + i * 4;
+ size_t x = byteword[0] - 'a';
+ size_t y = byteword[3] - 'a';
+ size_t offset = y * dim + x;
+ array[offset] = i;
+ }
+ }
+
+ // If the coordinates generated by the first and last letters are out of bounds,
+ // or the lookup table contains -1 at the coordinates, then the word is not valid.
+ int x = tolower(word[0]) - 'a';
+ int y = tolower(word[word_len == 4 ? 3 : 1]) - 'a';
+ if(!(0 <= x && x < dim && 0 <= y && y < dim)) {
+ throw runtime_error("Invalid Bytewords.");
+ }
+ size_t offset = y * dim + x;
+ int16_t value = array[offset];
+ if(value == -1) {
+ throw runtime_error("Invalid Bytewords.");
+ }
+
+ // If we're decoding a full four-letter word, verify that the two middle letters are correct.
+ if(word_len == 4) {
+ const char* byteword = bytewords + value * 4;
+ int c1 = tolower(word[1]);
+ int c2 = tolower(word[2]);
+ if(c1 != byteword[1] || c2 != byteword[2]) {
+ throw runtime_error("Invalid Bytewords.");
+ }
+ }
+
+ // Successful decode.
+ return value;
+}
+
+static const string get_word(uint8_t index) {
+ auto p = &bytewords[index * 4];
+ return string(p, p + 4);
+}
+
+static const string get_minimal_word(uint8_t index) {
+ string word;
+ word.reserve(2);
+ auto p = &bytewords[index * 4];
+ word.push_back(*p);
+ word.push_back(*(p + 3));
+ return word;
+}
+
+static const string encode(const ByteVector& buf, const string& separator) {
+ auto len = buf.size();
+ StringVector words;
+ words.reserve(len);
+ for(int i = 0; i < len; i++) {
+ auto byte = buf[i];
+ words.push_back(get_word(byte));
+ }
+ return join(words, separator);
+}
+
+static const ByteVector add_crc(const ByteVector& buf) {
+ auto crc_buf = crc32_bytes(buf);
+ auto result = buf;
+ append(result, crc_buf);
+ return result;
+}
+
+static const string encode_with_separator(const ByteVector& buf, const string& separator) {
+ auto crc_buf = add_crc(buf);
+ return encode(crc_buf, separator);
+}
+
+static const string encode_minimal(const ByteVector& buf) {
+ string result;
+ auto crc_buf = add_crc(buf);
+ auto len = crc_buf.size();
+ for(int i = 0; i < len; i++) {
+ auto byte = crc_buf[i];
+ result.append(get_minimal_word(byte));
+ }
+ return result;
+}
+
+static const ByteVector _decode(const string& s, char separator, size_t word_len) {
+ StringVector words;
+ if(word_len == 4) {
+ words = split(s, separator);
+ } else {
+ words = partition(s, 2);
+ }
+ ByteVector buf;
+ transform(words.begin(), words.end(), back_inserter(buf), [&](auto word) { return decode_word(word, word_len); });
+ if(buf.size() < 5) {
+ throw runtime_error("Invalid Bytewords.");
+ }
+ auto p = split(buf, buf.size() - 4);
+ auto body = p.first;
+ auto body_checksum = p.second;
+ auto checksum = crc32_bytes(body);
+ if(checksum != body_checksum) {
+ throw runtime_error("Invalid Bytewords.");
+ }
+
+ return body;
+}
+
+string Bytewords::encode(style style, const ByteVector& bytes) {
+ switch(style) {
+ case standard:
+ return encode_with_separator(bytes, " ");
+ case uri:
+ return encode_with_separator(bytes, "-");
+ case minimal:
+ return encode_minimal(bytes);
+ default:
+ assert(false);
+ }
+}
+
+ByteVector Bytewords::decode(style style, const string& string) {
+ switch(style) {
+ case standard:
+ return _decode(string, ' ', 4);
+ case uri:
+ return _decode(string, '-', 4);
+ case minimal:
+ return _decode(string, 0, 2);
+ default:
+ assert(false);
+ }
+}
+
+}
diff --git a/src/otsur/bcur/bytewords.hpp b/src/otsur/bcur/bytewords.hpp
new file mode 100644
index 0000000000..427f2c32f0
--- /dev/null
+++ b/src/otsur/bcur/bytewords.hpp
@@ -0,0 +1,30 @@
+//
+// bytewords.hpp
+//
+// Copyright © 2020 by Blockchain Commons, LLC
+// Licensed under the "BSD-2-Clause Plus Patent License"
+//
+
+#ifndef BC_UR_BYTEWORDS_HPP
+#define BC_UR_BYTEWORDS_HPP
+
+#include
+#include "utils.hpp"
+
+namespace ur {
+
+class Bytewords final {
+public:
+ enum style {
+ standard,
+ uri,
+ minimal
+ };
+
+ static std::string encode(style style, const ByteVector& bytes);
+ static ByteVector decode(style style, const std::string& string);
+};
+
+}
+
+#endif // BC_UR_BYTEWORDS_HPP
diff --git a/src/otsur/bcur/cbor-lite.hpp b/src/otsur/bcur/cbor-lite.hpp
new file mode 100644
index 0000000000..267474a9b4
--- /dev/null
+++ b/src/otsur/bcur/cbor-lite.hpp
@@ -0,0 +1,558 @@
+#ifndef BC_UR_CBOR_LITE_HPP
+#define BC_UR_CBOR_LITE_HPP
+
+// From: https://bitbucket.org/isode/cbor-lite/raw/6c770624a97e3229e3f200be092c1b9c70a60ef1/include/cbor-lite/codec.h
+
+// This file is part of CBOR-lite which is copyright Isode Limited
+// and others and released under a MIT license. For details, see the
+// COPYRIGHT.md file in the top-level folder of the CBOR-lite software
+// distribution.
+
+#include
+#include
+#include
+#include
+#include
+
+#ifndef __BYTE_ORDER__
+#error __BYTE_ORDER__ not defined
+#elif (__BYTE_ORDER__ != __ORDER_LITTLE_ENDIAN__) && (__BYTE_ORDER__ != __ORDER_BIG_ENDIAN__)
+#error __BYTE_ORDER__ neither __ORDER_BIG_ENDIAN__ nor __ORDER_LITTLE_ENDIAN__
+#endif
+
+namespace CborLite {
+
+class Exception : public std::exception {
+public:
+ Exception() noexcept {
+ }
+ virtual ~Exception() noexcept = default;
+
+ explicit Exception(const char* d) noexcept {
+ what_ += std::string(": ") + d;
+ }
+
+ explicit Exception(const std::string& d) noexcept {
+ what_ += ": " + d;
+ }
+
+ Exception(const Exception& e) noexcept : what_(e.what_) {
+ }
+
+ Exception(Exception&& e) noexcept : what_(std::move(e.what_)) {
+ // Note that e.what_ is not re-initialized to "CBOR Exception" as
+ // the moved-from object is not expected to ever be reused.
+ }
+
+ Exception& operator=(const Exception&) = delete;
+ Exception& operator=(Exception&&) = delete;
+
+ virtual const char* what() const noexcept {
+ return what_.c_str();
+ }
+
+private:
+ std::string what_ = "CBOR Exception";
+};
+
+using Tag = std::uint_fast64_t;
+
+namespace Major {
+constexpr Tag unsignedInteger = 0u;
+constexpr Tag negativeInteger = 1u << 5;
+constexpr Tag byteString = 2u << 5;
+constexpr Tag textString = 3u << 5;
+constexpr Tag array = 4u << 5;
+constexpr Tag map = 5u << 5;
+constexpr Tag semantic = 6u << 5;
+constexpr Tag floatingPoint = 7u << 5;
+constexpr Tag simple = 7u << 5;
+constexpr Tag mask = 0xe0u;
+} // namespace Major
+
+namespace Minor {
+constexpr Tag length1 = 24u;
+constexpr Tag length2 = 25u;
+constexpr Tag length4 = 26u;
+constexpr Tag length8 = 27u;
+
+constexpr Tag False = 20u;
+constexpr Tag True = 21u;
+constexpr Tag null = 22u;
+constexpr Tag undefined = 23u;
+constexpr Tag halfFloat = 25u; // not implemented
+constexpr Tag singleFloat = 26u;
+constexpr Tag doubleFloat = 27u;
+
+constexpr Tag dataTime = 0u;
+constexpr Tag epochDataTime = 1u;
+constexpr Tag positiveBignum = 2u;
+constexpr Tag negativeBignum = 3u;
+constexpr Tag decimalFraction = 4u;
+constexpr Tag bigfloat = 5u;
+constexpr Tag convertBase64Url = 21u;
+constexpr Tag convertBase64 = 22u;
+constexpr Tag convertBase16 = 23u;
+constexpr Tag cborEncodedData = 24u;
+constexpr Tag uri = 32u;
+constexpr Tag base64Url = 33u;
+constexpr Tag base64 = 34u;
+constexpr Tag regex = 35u;
+constexpr Tag mimeMessage = 36u;
+constexpr Tag selfDescribeCbor = 55799u;
+
+constexpr Tag mask = 0x1fu;
+} // namespace Minor
+
+constexpr Tag undefined = Major::semantic + Minor::undefined;
+
+using Flags = unsigned;
+namespace Flag {
+constexpr Flags none = 0;
+constexpr Flags requireMinimalEncoding = 1 << 0;
+} // namespace Flag
+
+template
+typename std::enable_if::value, std::size_t>::type length(Type val) {
+ if (val < 24) return 0;
+ for (std::size_t i = 1; i <= ((sizeof val) >> 1); i <<= 1) {
+ if (!(val >> (i << 3))) return i;
+ }
+ return sizeof val;
+}
+
+template
+typename std::enable_if::value, std::size_t>::type encodeTagAndAdditional(
+ Buffer& buffer, Tag tag, Tag additional) {
+ buffer.push_back(static_cast(tag + additional));
+ return 1;
+}
+
+template
+typename std::enable_if::value, std::size_t>::type decodeTagAndAdditional(
+ InputIterator& pos, InputIterator end, Tag& tag, Tag& additional, Flags = Flag::none) {
+ if (pos == end) throw Exception("not enough input");
+ auto octet = *(pos++);
+ tag = octet & Major::mask;
+ additional = octet & Minor::mask;
+ return 1;
+}
+
+template
+typename std::enable_if::value && std::is_unsigned::value, std::size_t>::type encodeTagAndValue(
+ Buffer& buffer, Tag tag, const Type t) {
+ auto len = length(t);
+ buffer.reserve(buffer.size() + len + 1);
+
+ switch (len) {
+ case 8:
+ encodeTagAndAdditional(buffer, tag, Minor::length8);
+ break;
+ case 4:
+ encodeTagAndAdditional(buffer, tag, Minor::length4);
+ break;
+ case 2:
+ encodeTagAndAdditional(buffer, tag, Minor::length2);
+ break;
+ case 1:
+ encodeTagAndAdditional(buffer, tag, Minor::length1);
+ break;
+ case 0:
+ return encodeTagAndAdditional(buffer, tag, t);
+ default:
+ throw Exception("too long");
+ }
+
+ switch (len) {
+ case 8:
+ buffer.push_back((t >> 56) & 0xffU);
+ buffer.push_back((t >> 48) & 0xffU);
+ buffer.push_back((t >> 40) & 0xffU);
+ buffer.push_back((t >> 32) & 0xffU);
+ case 4:
+ buffer.push_back((t >> 24) & 0xffU);
+ buffer.push_back((t >> 16) & 0xffU);
+ case 2:
+ buffer.push_back((t >> 8) & 0xffU);
+ case 1:
+ buffer.push_back(t & 0xffU);
+ }
+
+ return 1 + len;
+}
+
+template
+typename std::enable_if::value && std::is_unsigned::value, std::size_t>::type decodeTagAndValue(
+ InputIterator& pos, InputIterator end, Tag& tag, Type& t, Flags flags = Flag::none) {
+ if (pos == end) throw Exception("not enough input");
+ auto additional = Minor::undefined;
+ auto len = decodeTagAndAdditional(pos, end, tag, additional, flags);
+ if (additional < Minor::length1) {
+ t = additional;
+ return len;
+ }
+ t = 0u;
+ switch (additional) {
+ case Minor::length8:
+ if (std::distance(pos, end) < 8) throw Exception("not enough input");
+ t |= static_cast(reinterpret_cast(*(pos++))) << 56;
+ t |= static_cast(reinterpret_cast(*(pos++))) << 48;
+ t |= static_cast(reinterpret_cast(*(pos++))) << 40;
+ t |= static_cast(reinterpret_cast(*(pos++))) << 32;
+ len += 4;
+ if ((flags & Flag::requireMinimalEncoding) && !t) throw Exception("encoding not minimal");
+ case Minor::length4:
+ if (std::distance(pos, end) < 4) throw Exception("not enough input");
+ t |= static_cast(reinterpret_cast(*(pos++))) << 24;
+ t |= static_cast(reinterpret_cast(*(pos++))) << 16;
+ len += 2;
+ if ((flags & Flag::requireMinimalEncoding) && !t) throw Exception("encoding not minimal");
+ case Minor::length2:
+ if (std::distance(pos, end) < 2) throw Exception("not enough input");
+ t |= static_cast(reinterpret_cast(*(pos++))) << 8;
+ len++;
+ if ((flags & Flag::requireMinimalEncoding) && !t) throw Exception("encoding not minimal");
+ case Minor::length1:
+ if (std::distance(pos, end) < 1) throw Exception("not enough input");
+ t |= static_cast(reinterpret_cast(*(pos++)));
+ len++;
+ if ((flags & Flag::requireMinimalEncoding) && t < 24) throw Exception("encoding not minimal");
+ return len;
+ }
+ throw Exception("bad additional value");
+}
+
+template
+typename std::enable_if::value, std::size_t>::type encodeUnsigned(Buffer& buffer, const Type& t) {
+ return encodeTagAndValue(buffer, Major::unsignedInteger, t);
+}
+
+template
+typename std::enable_if::value && std::is_unsigned::value && !std::is_const::value,
+ std::size_t>::type
+decodeUnsigned(InputIterator& pos, InputIterator end, Type& t, Flags flags = Flag::none) {
+ auto tag = undefined;
+ auto len = decodeTagAndValue(pos, end, tag, t, flags);
+ if (tag != Major::unsignedInteger) throw Exception("not Unsigned");
+ return len;
+}
+
+template
+typename std::enable_if::value, std::size_t>::type encodeNegative(Buffer& buffer, const Type& t) {
+ return encodeTagAndValue(buffer, Major::negativeInteger, t);
+}
+
+template
+typename std::enable_if::value && std::is_unsigned::value && !std::is_const::value,
+ std::size_t>::type
+decodeNegative(InputIterator& pos, InputIterator end, Type& t, Flags flags = Flag::none) {
+ auto tag = undefined;
+ auto len = decodeTagAndValue(pos, end, tag, t, flags);
+ if (tag != Major::negativeInteger) throw Exception("not Unsigned");
+ return len;
+}
+
+template
+typename std::enable_if::value, std::size_t>::type encodeInteger(Buffer& buffer, const Type& t) {
+ if (t >= 0) {
+ unsigned long long val = t;
+ return encodeUnsigned(buffer, val);
+ } else {
+ unsigned long long val = -t - 1;
+ return encodeNegative(buffer, val);
+ }
+}
+
+template
+typename std::enable_if::value && std::is_signed::value && !std::is_const::value,
+ std::size_t>::type
+decodeInteger(InputIterator& pos, InputIterator end, Type& t, Flags flags = Flag::none) {
+ auto tag = undefined;
+ unsigned long long val;
+ auto len = decodeTagAndValue(pos, end, tag, val, flags);
+ switch (tag) {
+ case Major::unsignedInteger:
+ t = val;
+ break;
+ case Major::negativeInteger:
+ t = -1 - static_cast(val);
+ break;
+ default:
+ throw Exception("not integer");
+ }
+ return len;
+}
+
+template
+typename std::enable_if::value && std::is_same::value, std::size_t>::type encodeBool(
+ Buffer& buffer, const Type& t) {
+ return encodeTagAndAdditional(buffer, Major::simple, t ? Minor::True : Minor::False);
+}
+
+template
+typename std::enable_if::value && std::is_same::value && !std::is_const::value,
+ std::size_t>::type
+decodeBool(InputIterator& pos, InputIterator end, Type& t, Flags flags = Flag::none) {
+ auto tag = undefined;
+ auto value = undefined;
+ auto len = decodeTagAndValue(pos, end, tag, value, flags);
+ if (tag == Major::simple) {
+ if (value == Minor::True) {
+ t = true;
+ return len;
+ } else if (value == Minor::False) {
+ t = false;
+ return len;
+ }
+ throw Exception("not Boolean");
+ }
+ throw Exception("not Simple");
+}
+
+template
+typename std::enable_if::value, std::size_t>::type encodeBytes(Buffer& buffer, const Type& t) {
+ auto len = encodeTagAndValue(buffer, Major::byteString, t.size());
+ buffer.insert(std::end(buffer), std::begin(t), std::end(t));
+ return len + t.size();
+}
+
+template
+typename std::enable_if::value && !std::is_const::value, std::size_t>::type decodeBytes(
+ InputIterator& pos, InputIterator end, Type& t, Flags flags = Flag::none) {
+ auto tag = undefined;
+ auto value = undefined;
+ auto len = decodeTagAndValue(pos, end, tag, value, flags);
+ if (tag != Major::byteString) throw Exception("not ByteString");
+
+ auto dist = std::distance(pos, end);
+ if (dist < static_cast(value)) throw Exception("not enough input");
+ t.insert(std::end(t), pos, pos + value);
+ std::advance(pos, value);
+ return len + value;
+}
+
+template
+typename std::enable_if::value && std::is_unsigned::value, std::size_t>::type encodeEncodedBytesPrefix(
+ Buffer& buffer, const Type& t) {
+ auto len = encodeTagAndValue(buffer, Major::semantic, Minor::cborEncodedData);
+ return len + encodeTagAndValue(buffer, Major::byteString, t);
+}
+
+template
+typename std::enable_if::value && !std::is_const::value, std::size_t>::type
+decodeEncodedBytesPrefix(InputIterator& pos, InputIterator end, Type& t, Flags flags = Flag::none) {
+ auto tag = undefined;
+ auto value = undefined;
+ auto len = decodeTagAndValue(pos, end, tag, value, flags);
+ if (tag != Major::semantic || value != Minor::cborEncodedData) {
+ throw Exception("not CBOR Encoded Data");
+ }
+ tag = undefined;
+ len += decodeTagAndValue(pos, end, tag, value, flags);
+ if (tag != Major::byteString) throw Exception("not ByteString");
+ t = value;
+ return len;
+}
+
+template
+typename std::enable_if::value, std::size_t>::type encodeEncodedBytes(Buffer& buffer, const Type& t) {
+ auto len = encodeTagAndValue(buffer, Major::semantic, Minor::cborEncodedData);
+ return len + encodeBytes(buffer, t);
+}
+
+template
+typename std::enable_if::value && !std::is_const::value, std::size_t>::type decodeEncodedBytes(
+ InputIterator& pos, InputIterator end, Type& t, Flags flags = Flag::none) {
+ auto tag = undefined;
+ auto value = undefined;
+ auto len = decodeTagAndValue(pos, end, tag, value, flags);
+ if (tag != Major::semantic || value != Minor::cborEncodedData) {
+ throw Exception("not CBOR Encoded Data");
+ }
+ return len + decodeBytes(pos, end, t, flags);
+}
+
+template
+typename std::enable_if::value, std::size_t>::type encodeText(Buffer& buffer, const Type& t) {
+ auto len = encodeTagAndValue(buffer, Major::textString, t.size());
+ buffer.insert(std::end(buffer), std::begin(t), std::end(t));
+ return len + t.size();
+}
+
+template
+typename std::enable_if::value && !std::is_const::value, std::size_t>::type decodeText(
+ InputIterator& pos, InputIterator end, Type& t, Flags flags = Flag::none) {
+ auto tag = undefined;
+ auto value = undefined;
+ auto len = decodeTagAndValue(pos, end, tag, value, flags);
+ if (tag != Major::textString) throw Exception("not TextString");
+
+ auto dist = std::distance(pos, end);
+ if (dist < static_cast(value)) throw Exception("not enough input");
+ t.insert(std::end(t), pos, pos + value);
+ std::advance(pos, value);
+ return len + value;
+}
+
+template
+typename std::enable_if::value && std::is_unsigned::value, std::size_t>::type encodeArraySize(
+ Buffer& buffer, const Type& t) {
+ return encodeTagAndValue(buffer, Major::array, t);
+}
+
+template
+typename std::enable_if::value && !std::is_const::value && std::is_unsigned::value,
+ std::size_t>::type
+decodeArraySize(InputIterator& pos, InputIterator end, Type& t, Flags flags = Flag::none) {
+ auto tag = undefined;
+ auto value = undefined;
+ auto len = decodeTagAndValue(pos, end, tag, value, flags);
+ if (tag != Major::array) throw Exception("not Array");
+ t = value;
+ return len;
+}
+
+template
+typename std::enable_if::value && std::is_unsigned::value, std::size_t>::type encodeMapSize(
+ Buffer& buffer, const Type& t) {
+ return encodeTagAndValue(buffer, Major::map, t);
+}
+
+template
+typename std::enable_if::value && !std::is_const::value && std::is_unsigned::value,
+ std::size_t>::type
+decodeMapSize(InputIterator& pos, InputIterator end, Type& t, Flags flags = Flag::none) {
+ auto tag = undefined;
+ auto value = undefined;
+ auto len = decodeTagAndValue(pos, end, tag, value, flags);
+ if (tag != Major::map) throw Exception("not Map");
+ t = value;
+ return len;
+}
+
+//
+// codec-fp.h
+//
+
+template
+typename std::enable_if::value && std::is_floating_point::value, std::size_t>::type encodeSingleFloat(
+ Buffer& buffer, const Type& t) {
+ static_assert(sizeof(float) == 4, "sizeof(float) expected to be 4");
+ auto len = encodeTagAndAdditional(buffer, Major::floatingPoint, Minor::singleFloat);
+ const char* p;
+ float ft;
+ if (sizeof(t) == sizeof(ft)) {
+ p = reinterpret_cast(&t);
+ } else {
+ ft = static_cast(t);
+ p = reinterpret_cast(&ft);
+ }
+#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
+ for (auto i = 0u; i < sizeof(ft); ++i) {
+ buffer.push_back(p[i]);
+ }
+#else
+ for (auto i = 1u; i <= sizeof(ft); ++i) {
+ buffer.push_back(p[sizeof(ft) - i]);
+ }
+#endif
+ return len + sizeof(ft);
+}
+
+template
+typename std::enable_if::value && std::is_floating_point::value, std::size_t>::type encodeDoubleFloat(
+ Buffer& buffer, const Type& t) {
+ static_assert(sizeof(double) == 8, "sizeof(double) expected to be 8");
+ auto len = encodeTagAndAdditional(buffer, Major::floatingPoint, Minor::doubleFloat);
+ const char* p;
+ double ft;
+ if (sizeof(t) == sizeof(ft)) {
+ p = reinterpret_cast(&t);
+ } else {
+ ft = t;
+ p = reinterpret_cast(&ft);
+ }
+#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
+ for (auto i = 0u; i < sizeof(ft); ++i) {
+ buffer.push_back(p[i]);
+ }
+#else
+ for (auto i = 1u; i <= sizeof(ft); ++i) {
+ buffer.push_back(p[sizeof(ft) - i]);
+ }
+#endif
+ return len + sizeof(ft);
+}
+
+template
+typename std::enable_if::value && std::is_floating_point::value && !std::is_const::value,
+ std::size_t>::type
+decodeSingleFloat(InputIterator& pos, InputIterator end, Type& t, Flags flags = Flag::none) {
+ static_assert(sizeof(float) == 4, "sizeof(float) expected to be 4");
+ auto tag = undefined;
+ auto value = undefined;
+ auto len = decodeTagAndAdditional(pos, end, tag, value, flags);
+ if (tag != Major::floatingPoint) throw Exception("not floating-point");
+ if (value != Minor::singleFloat) throw Exception("not single-precision floating-point");
+ if (std::distance(pos, end) < static_cast(sizeof(float))) throw Exception("not enough input");
+
+ char* p;
+ float ft;
+ if (sizeof(t) == sizeof(ft)) {
+ p = reinterpret_cast(&t);
+ } else {
+ ft = static_cast(t);
+ p = reinterpret_cast(&ft);
+ }
+
+#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
+ for (auto i = 0u; i < sizeof(ft); ++i) {
+ p[i] = *(pos++);
+ }
+#else
+ for (auto i = 1u; i <= sizeof(ft); ++i) {
+ p[sizeof(ft) - i] = *(pos++);
+ }
+#endif
+ if (sizeof(t) != sizeof(ft)) t = ft;
+ return len + sizeof(ft);
+}
+
+template
+typename std::enable_if::value && std::is_floating_point::value && !std::is_const::value,
+ std::size_t>::type
+decodeDoubleFloat(InputIterator& pos, InputIterator end, Type& t, Flags flags = Flag::none) {
+ static_assert(sizeof(double) == 8, "sizeof(double) expected to be 8");
+ auto tag = undefined;
+ auto value = undefined;
+ auto len = decodeTagAndAdditional(pos, end, tag, value, flags);
+ if (tag != Major::floatingPoint) throw Exception("not floating-point");
+ if (value != Minor::doubleFloat) throw Exception("not double-precision floating-point");
+ if (std::distance(pos, end) < static_cast(sizeof(double))) throw Exception("not enough input");
+
+ char* p;
+ double ft;
+ if (sizeof(t) == sizeof(ft)) {
+ p = reinterpret_cast(&t);
+ } else {
+ ft = t;
+ p = reinterpret_cast(&ft);
+ }
+
+#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
+ for (auto i = 0u; i < sizeof(ft); ++i) {
+ p[i] = *(pos++);
+ }
+#else
+ for (auto i = 1u; i <= sizeof(ft); ++i) {
+ p[sizeof(ft) - i] = *(pos++);
+ }
+#endif
+
+ if (sizeof(t) != sizeof(ft)) t = ft;
+ return len + sizeof(ft);
+}
+
+} // namespace CborLite
+
+#endif // BC_UR_CBOR_LITE_HPP
diff --git a/src/otsur/bcur/crc32.c b/src/otsur/bcur/crc32.c
new file mode 100644
index 0000000000..38e739a7ee
--- /dev/null
+++ b/src/otsur/bcur/crc32.c
@@ -0,0 +1,43 @@
+//
+// crc32.c
+//
+// Copyright © 2020 by Blockchain Commons, LLC
+// Licensed under the "BSD-2-Clause Plus Patent License"
+//
+
+#include "crc32.h"
+#include
+
+#ifdef ARDUINO
+#define htonl(x) __builtin_bswap32((uint32_t) (x))
+#elif _WIN32
+#include
+#else
+#include
+#endif
+
+uint32_t ur_crc32(const uint8_t* bytes, size_t len) {
+ static uint32_t* table = NULL;
+
+ if(table == NULL) {
+ table = malloc(256 * sizeof(uint32_t));
+ for(int i = 0; i < 256; i++) {
+ uint32_t c = i;
+ for(int j = 0; j < 8; j++) {
+ c = (c % 2 == 0) ? (c >> 1) : (0xEDB88320 ^ (c >> 1));
+ }
+ table[i] = c;
+ }
+ }
+
+ uint32_t crc = ~0;
+ for(int i = 0; i < len; i++) {
+ uint32_t byte = bytes[i];
+ crc = (crc >> 8) ^ table[(crc ^ byte) & 0xFF];
+ }
+ return ~crc;
+}
+
+uint32_t ur_crc32n(const uint8_t* bytes, size_t len) {
+ return htonl(ur_crc32(bytes, len));
+}
diff --git a/src/otsur/bcur/crc32.h b/src/otsur/bcur/crc32.h
new file mode 100644
index 0000000000..cfa38ef75f
--- /dev/null
+++ b/src/otsur/bcur/crc32.h
@@ -0,0 +1,28 @@
+//
+// crc32.h
+//
+// Copyright © 2020 by Blockchain Commons, LLC
+// Licensed under the "BSD-2-Clause Plus Patent License"
+//
+
+#ifndef BC_UR_CRC32_H
+#define BC_UR_CRC32_H
+
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// Returns the CRC-32 checksum of the input buffer.
+uint32_t ur_crc32(const uint8_t* bytes, size_t len);
+
+// Returns the CRC-32 checksum of the input buffer in network byte order (big endian).
+uint32_t ur_crc32n(const uint8_t* bytes, size_t len);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // BC_UR_CRC32_H
diff --git a/src/otsur/bcur/fountain-decoder.cpp b/src/otsur/bcur/fountain-decoder.cpp
new file mode 100644
index 0000000000..0bd0ec78ca
--- /dev/null
+++ b/src/otsur/bcur/fountain-decoder.cpp
@@ -0,0 +1,254 @@
+//
+// fountain-decoder.cpp
+//
+// Copyright © 2020 by Blockchain Commons, LLC
+// Licensed under the "BSD-2-Clause Plus Patent License"
+//
+
+#include "fountain-decoder.hpp"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+using namespace std;
+
+namespace ur {
+
+FountainDecoder::FountainDecoder() { }
+
+FountainDecoder::Part::Part(const FountainEncoder::Part& p)
+ : indexes_(choose_fragments(p.seq_num(), p.seq_len(), p.checksum()))
+ , data_(p.data())
+{
+}
+
+FountainDecoder::Part::Part(PartIndexes& indexes, ByteVector& data)
+ : indexes_(indexes)
+ , data_(data)
+{
+}
+
+const ByteVector FountainDecoder::join_fragments(const vector& fragments, size_t message_len) {
+ auto message = join(fragments);
+ return take_first(message, message_len);
+}
+
+double FountainDecoder::estimated_percent_complete() const {
+ if(is_complete()) return 1;
+ if(!_expected_part_indexes.has_value()) return 0;
+ auto estimated_input_parts = expected_part_count() * 1.75;
+ return min(0.99, processed_parts_count_ / estimated_input_parts);
+}
+
+bool FountainDecoder::receive_part(FountainEncoder::Part& encoder_part) {
+ // Don't process the part if we're already done
+ if(is_complete()) return false;
+
+ // Don't continue if this part doesn't validate
+ if(!validate_part(encoder_part)) return false;
+
+ // Add this part to the queue
+ auto p = Part(encoder_part);
+ last_part_indexes_ = p.indexes();
+ enqueue(p);
+
+ // Process the queue until we're done or the queue is empty
+ while(!is_complete() && !_queued_parts.empty()) {
+ process_queue_item();
+ }
+
+ // Keep track of how many parts we've processed
+ processed_parts_count_ += 1;
+
+ //print_part_end();
+
+ return true;
+}
+
+void FountainDecoder::enqueue(Part &&p) {
+ _queued_parts.push_back(p);
+}
+
+void FountainDecoder::enqueue(const Part &p) {
+ _queued_parts.push_back(p);
+}
+
+void FountainDecoder::process_queue_item() {
+ auto part = _queued_parts.front();
+ //print_part(part);
+ _queued_parts.pop_front();
+ if(part.is_simple()) {
+ process_simple_part(part);
+ } else {
+ process_mixed_part(part);
+ }
+ //print_state();
+}
+
+void FountainDecoder::reduce_mixed_by(const Part& p) {
+ // Reduce all the current mixed parts by the given part
+ vector reduced_parts;
+ for(auto i = _mixed_parts.begin(); i != _mixed_parts.end(); i++) {
+ reduced_parts.push_back(reduce_part_by_part(i->second, p));
+ }
+
+ // Collect all the remaining mixed parts
+ PartDict new_mixed;
+ for(auto reduced_part: reduced_parts) {
+ // If this reduced part is now simple
+ if(reduced_part.is_simple()) {
+ // Add it to the queue
+ enqueue(reduced_part);
+ } else {
+ // Otherwise, add it to the list of current mixed parts
+ new_mixed.insert(pair(reduced_part.indexes(), reduced_part));
+ }
+ }
+ _mixed_parts = new_mixed;
+}
+
+FountainDecoder::Part FountainDecoder::reduce_part_by_part(const Part& a, const Part& b) const {
+ // If the fragments mixed into `b` are a strict (proper) subset of those in `a`...
+ if(is_strict_subset(b.indexes(), a.indexes())) {
+ // The new fragments in the revised part are `a` - `b`.
+ auto new_indexes = set_difference(a.indexes(), b.indexes());
+ // The new data in the revised part are `a` XOR `b`
+ auto new_data = xor_with(a.data(), b.data());
+ return Part(new_indexes, new_data);
+ } else {
+ // `a` is not reducable by `b`, so return a
+ return a;
+ }
+}
+
+void FountainDecoder::process_simple_part(Part& p) {
+ // Don't process duplicate parts
+ auto fragment_index = p.index();
+ if(contains(received_part_indexes_, fragment_index)) return;
+
+ // Record this part
+ _simple_parts.insert(pair(p.indexes(), p));
+ received_part_indexes_.insert(fragment_index);
+
+ // If we've received all the parts
+ if(received_part_indexes_ == _expected_part_indexes) {
+ // Reassemble the message from its fragments
+ vector sorted_parts;
+ transform(_simple_parts.begin(), _simple_parts.end(), back_inserter(sorted_parts), [&](auto elem) { return elem.second; });
+ sort(sorted_parts.begin(), sorted_parts.end(),
+ [](const Part& a, const Part& b) -> bool {
+ return a.index() < b.index();
+ }
+ );
+ vector fragments;
+ transform(sorted_parts.begin(), sorted_parts.end(), back_inserter(fragments), [&](auto part) { return part.data(); });
+ auto message = join_fragments(fragments, *_expected_message_len);
+
+ // Verify the message checksum and note success or failure
+ auto checksum = crc32_int(message);
+ if(checksum == _expected_checksum) {
+ result_ = message;
+ } else {
+ result_ = InvalidChecksum();
+ }
+ } else {
+ // Reduce all the mixed parts by this part
+ reduce_mixed_by(p);
+ }
+}
+
+void FountainDecoder::process_mixed_part(const Part& p) {
+ // Don't process duplicate parts
+ if(any_of(_mixed_parts.begin(), _mixed_parts.end(), [&](auto r) { return r.first == p.indexes(); })) {
+ return;
+ }
+
+ // Reduce this part by all the others
+ auto p2 = accumulate(_simple_parts.begin(), _simple_parts.end(), p, [&](auto p, auto r) { return reduce_part_by_part(p, r.second); });
+ p2 = accumulate(_mixed_parts.begin(), _mixed_parts.end(), p2, [&](auto p, auto r) { return reduce_part_by_part(p, r.second); });
+
+ // If the part is now simple
+ if(p2.is_simple()) {
+ // Add it to the queue
+ enqueue(p2);
+ } else {
+ // Reduce all the mixed parts by this one
+ reduce_mixed_by(p2);
+ // Record this new mixed part
+ _mixed_parts.insert(pair(p2.indexes(), p2));
+ }
+}
+
+bool FountainDecoder::validate_part(const FountainEncoder::Part& p) {
+ // If this is the first part we've seen
+ if(!_expected_part_indexes.has_value()) {
+ // Record the things that all the other parts we see will have to match to be valid.
+ _expected_part_indexes = PartIndexes();
+ for(size_t i = 0; i < p.seq_len(); i++) { _expected_part_indexes->insert(i); }
+ _expected_message_len = p.message_len();
+ _expected_checksum = p.checksum();
+ _expected_fragment_len = p.data().size();
+ } else {
+ // If this part's values don't match the first part's values, throw away the part
+ if(expected_part_count() != p.seq_len()) return false;
+ if(_expected_message_len != p.message_len()) return false;
+ if(_expected_checksum != p.checksum()) return false;
+ if(_expected_fragment_len != p.data().size()) return false;
+ }
+ // This part should be processed
+ return true;
+}
+
+string FountainDecoder::indexes_to_string(const PartIndexes& indexes) {
+ auto i = vector(indexes.begin(), indexes.end());
+ sort(i.begin(), i.end());
+ StringVector s;
+ transform(i.begin(), i.end(), back_inserter(s), [](size_t a) { return to_string(a); });
+ return "[" + join(s, ", ") + "]";
+}
+
+void FountainDecoder::print_part(const Part& p) const {
+ cout << "part indexes: " << indexes_to_string(p.indexes()) << endl;
+}
+
+void FountainDecoder::print_part_end() const {
+ auto expected = _expected_part_indexes.has_value() ? to_string(expected_part_count()) : "nil";
+ auto percent = int(round(estimated_percent_complete() * 100));
+ cout << "processed: " << processed_parts_count_ << ", expected: " << expected << ", received: " << received_part_indexes_.size() << ", percent: " << percent << "%" << endl;
+}
+
+string FountainDecoder::result_description() const {
+ string desc;
+ if(!result_.has_value()) {
+ desc = "nil";
+ } else {
+ auto r = *result_;
+ if(holds_alternative(r)) {
+ desc = to_string(get(r).size()) + " bytes";
+ } else if(holds_alternative(r)) {
+ desc = get(r).what();
+ } else {
+ assert(false);
+ }
+ }
+ return desc;
+}
+
+void FountainDecoder::print_state() const {
+ auto parts = _expected_part_indexes.has_value() ? to_string(expected_part_count()) : "nil";
+ auto received = indexes_to_string(received_part_indexes_);
+ StringVector mixed;
+ transform(_mixed_parts.begin(), _mixed_parts.end(), back_inserter(mixed), [](const pair& p) {
+ return indexes_to_string(p.first);
+ });
+ auto mixed_s = "[" + join(mixed, ", ") + "]";
+ auto queued = _queued_parts.size();
+ auto res = result_description();
+ cout << "parts: " << parts << ", received: " << received << ", mixed: " << mixed_s << ", queued: " << queued << ", result: " << res << endl;
+}
+
+}
diff --git a/src/otsur/bcur/fountain-decoder.hpp b/src/otsur/bcur/fountain-decoder.hpp
new file mode 100644
index 0000000000..6dc92d70dc
--- /dev/null
+++ b/src/otsur/bcur/fountain-decoder.hpp
@@ -0,0 +1,103 @@
+//
+// fountain-decoder.hpp
+//
+// Copyright © 2020 by Blockchain Commons, LLC
+// Licensed under the "BSD-2-Clause Plus Patent License"
+//
+
+#ifndef BC_UR_FOUNTAIN_DECODER_HPP
+#define BC_UR_FOUNTAIN_DECODER_HPP
+
+#include "utils.hpp"
+#include "fountain-encoder.hpp"
+#include