diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 201c943..080829f 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -1,4 +1,4 @@ -name: Java CI +name: Gradle Build on: [push, pull_request] @@ -8,7 +8,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macOS-latest, windows-latest] - java: [ '11', '12' ] + java: [ '11', '14' ] fail-fast: false name: ${{ matrix.os }} JDK ${{ matrix.java }} steps: @@ -17,8 +17,10 @@ jobs: uses: actions/setup-java@v1 with: java-version: ${{ matrix.java }} + - name: Verify Gradle Wrapper + uses: gradle/wrapper-validation-action@v1 - name: Build with Gradle - run: ./gradlew -Ddownload.jpackage=true buildCI consensusj-netwallet-fx:jpackage --info --stacktrace + run: ./gradlew -D"download.jpackage"=true buildCI consensusj-netwallet-fx:jpackage --info --stacktrace - name: Upload jpackage directory as artifact uses: actions/upload-artifact@master with: diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9b4c426..1e2b5ff 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,11 +6,11 @@ variables: before_script: - apt-get update - apt-get -y upgrade - - apt-get -y install openjdk-11-jdk curl rpm fakeroot wget gnupg2 software-properties-common - # Install AdoptOpenJDK 12 following instructions from here: https://adoptopenjdk.net/installation.html + - apt-get -y install openjdk-11-jdk binutils curl rpm fakeroot wget gnupg2 software-properties-common + # Install AdoptOpenJDK 14 - wget -qO - https://adoptopenjdk.jfrog.io/adoptopenjdk/api/gpg/key/public | apt-key add - - add-apt-repository --yes https://adoptopenjdk.jfrog.io/adoptopenjdk/deb/ - - apt-get -y install adoptopenjdk-12-hotspot + - apt-get -y install adoptopenjdk-14-hotspot build: script: @@ -19,8 +19,8 @@ build: - ./gradlew -Ddownload.jpackage=true buildCI consensusj-netwallet-fx:jpackage --stacktrace artifacts: paths: - - airgapfx-wallet/build/jpackage - - airgapfx-wallet/build/libs + - consensusj-netwallet-fx/build/jpackage + - consensusj-signwallet-fx/build/jpackage # Note: You can test changes to this file locally with: # gitlab-runner exec docker --docker-privileged build diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 0a267fb..aa4a613 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -3,6 +3,37 @@ A high-level view of the changes in each wallet-framework binary release. +== v0.1.8-SNAPSHOT + +Released: Not yet + +* Upgrade to Gradle 6.3 + +== v0.1.7 + +Released: 2019.10.28 + +* `airgaplib` now is JDK 8+ with automatic module name + +== v0.1.6 + +Released: 2019.10.28 + +* Libraries published to Bintray +* Other changes, to be documented + +== v0.1.5 + +Released: 2019.10.23 + +* Put Unsigned Transaction QR and QR Scanner window in same pop-up view +* Transaction list view is now populated with (minimal) transaction information +* `airgapfx-wallet` subproject is now `consensusj-netwallet-fx` +* Airgap Wallet rebranded with ConsensusJ name and logo +* Update Gradle to 6.0-rc-1 (so we can build with JDK 13) +* Several dependency updates +* Support for CI builds with Github Actions + == v0.1.4 Released: 2019.08.20 diff --git a/airgapfx/build.gradle b/airgapfx/build.gradle index edab0b9..b043ca3 100644 --- a/airgapfx/build.gradle +++ b/airgapfx/build.gradle @@ -3,10 +3,12 @@ plugins { id 'java-library' id 'maven' id 'application' - id 'org.openjfx.javafxplugin' version '0.0.8' - id 'org.javamodularity.moduleplugin' version "1.5.0" + id 'org.openjfx.javafxplugin' version '0.0.9' } +sourceCompatibility = 9 +targetCompatibility = 9 + dependencies { implementation project(':airgaplib') @@ -26,6 +28,7 @@ dependencies { } javafx { + version = javaFxVersion modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing' ] } diff --git a/airgapfx/src/main/java/org/consensusj/airgap/fx/camera/CameraService.java b/airgapfx/src/main/java/org/consensusj/airgap/fx/camera/CameraService.java index 799346d..227d60c 100644 --- a/airgapfx/src/main/java/org/consensusj/airgap/fx/camera/CameraService.java +++ b/airgapfx/src/main/java/org/consensusj/airgap/fx/camera/CameraService.java @@ -34,6 +34,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.awt.Dimension; import java.awt.image.BufferedImage; import java.util.function.Consumer; @@ -57,7 +58,7 @@ public CameraService(Webcam camera, WebcamResolution resolution) { camera.setViewSize(resolution.getSize()); if (logViewSizes) { log.info("Camera supports the following sizes:"); - for (var size : camera.getViewSizes()) { + for (Dimension size : camera.getViewSizes()) { log.info(size.toString()); } } diff --git a/airgapfx/src/main/java/org/consensusj/airgap/fx/components/QrCaptureView.java b/airgapfx/src/main/java/org/consensusj/airgap/fx/components/QrCaptureView.java index c9168c3..bddb77b 100644 --- a/airgapfx/src/main/java/org/consensusj/airgap/fx/components/QrCaptureView.java +++ b/airgapfx/src/main/java/org/consensusj/airgap/fx/components/QrCaptureView.java @@ -26,7 +26,6 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.VBox; -import javafx.stage.Window; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,7 +36,8 @@ */ public class QrCaptureView extends BorderPane { private static final Logger log = LoggerFactory.getLogger(QrCaptureView.class); - private final Consumer listener; + private final Consumer scanListener; + private final Consumer closeListener; private final CameraView cameraView; private final CameraService service; private TextArea previewText; @@ -46,11 +46,19 @@ public class QrCaptureView extends BorderPane { private Button acceptButton; private String previewResult; - - public QrCaptureView(CameraService cameraService, Consumer listener) { + + /** + * JavaFX View with UI for a WebCam-based QR Code Scanner + * + * @param cameraService QR Code Capturing Camera Service + * @param scanListener Listener called when QR code is accepted (accept button) + * @param closeListener Listener called when view is closed (cancel or accept button) + */ + public QrCaptureView(CameraService cameraService, Consumer scanListener, Consumer closeListener) { service = cameraService; cameraView = new CameraView(cameraService); - this.listener = listener; + this.scanListener = scanListener; + this.closeListener = closeListener; setCenter(cameraView); @@ -94,15 +102,12 @@ private HBox buttonBox() { } private void acceptAction(ActionEvent actionEvent) { - listener.accept(previewResult); - closeParentWindow(); + scanListener.accept(previewResult); + closeParent(); } private void cancelAction(ActionEvent actionEvent) { - if (service.isRunning()) { - service.cancel(); - } - closeParentWindow(); + closeParent(); } private void rescanAction(ActionEvent e) { @@ -131,11 +136,15 @@ private void scan() { rescanButton.setDisable(true); } - private void closeParentWindow() { - Window stage = this.getScene().getWindow(); - if (stage != null) { - stage.hide(); + private void stopCameraService() { + if (service.isRunning()) { + service.cancel(); } } + private void closeParent() { + stopCameraService(); + closeListener.accept(null); + } + } diff --git a/airgapfx/src/main/java/org/consensusj/airgap/fx/demoapp/DemoQRScannerApplication.java b/airgapfx/src/main/java/org/consensusj/airgap/fx/demoapp/DemoQRScannerApplication.java index ef9ba9e..620f229 100644 --- a/airgapfx/src/main/java/org/consensusj/airgap/fx/demoapp/DemoQRScannerApplication.java +++ b/airgapfx/src/main/java/org/consensusj/airgap/fx/demoapp/DemoQRScannerApplication.java @@ -32,6 +32,7 @@ public class DemoQRScannerApplication extends Application { private static final Logger log = LoggerFactory.getLogger(DemoQRScannerApplication.class); private CameraService cameraService; + private Stage primaryStage; @Override public void init() { @@ -42,7 +43,8 @@ public void init() { @Override public void start(Stage primaryStage) { - QrCaptureView captureView = new QrCaptureView(cameraService, this::scanListener); + this.primaryStage = primaryStage; + QrCaptureView captureView = new QrCaptureView(cameraService, this::scanListener, this::closeListener); Scene scene = new Scene(captureView); primaryStage.setScene(scene); @@ -53,6 +55,11 @@ private void scanListener(String result) { System.out.println("Result: " + result); } + private void closeListener(Object result) { + if (primaryStage != null) { + primaryStage.hide(); + } + } public static void main(String[] args) { launch(args); diff --git a/airgapfx/src/main/resources/org/consensusj/airgap/fx/AirgapSigningDialog.fxml b/airgapfx/src/main/resources/org/consensusj/airgap/fx/AirgapSigningDialog.fxml deleted file mode 100644 index 05f3a9b..0000000 --- a/airgapfx/src/main/resources/org/consensusj/airgap/fx/AirgapSigningDialog.fxml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/airgaplib/build.gradle b/airgaplib/build.gradle index 3ceea96..2b78793 100644 --- a/airgaplib/build.gradle +++ b/airgaplib/build.gradle @@ -1,31 +1,35 @@ plugins { - id 'java' - id 'groovy' id 'java-library' + id 'groovy' id 'maven' id 'eclipse' - id 'org.javamodularity.moduleplugin' version "1.5.0" + id 'org.javamodularity.moduleplugin' version "1.7.0" } -def jacksonVersion = '2.9.8' - -/* - * TODO: This module should really be JDK 8 compatible, perhaps a multi-release jar - * Currently the only feature we're using that requires 9 is the module header +/** + * Java 9 compatibility (We may switch back to Java 8 for use on Android) */ sourceCompatibility = 9 targetCompatibility = 9 dependencies { api "${bitcoinjGroup}:${bitcoinjArtifact}:${bitcoinjVersion}" - - implementation 'com.google.guava:guava:27.1-android' - + implementation "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}" implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" implementation "org.slf4j:slf4j-api:${slf4jVersion}" testCompile group: 'junit', name: 'junit', version: '4.12' - testCompile 'com.msgilligan:bitcoinj-dsl:0.4.0' + testCompile 'com.msgilligan:bitcoinj-dsl:0.5.0' +} + +ext.moduleName = 'org.consensusj.airgap' + +jar { + inputs.property('moduleName', moduleName) + manifest { + attributes 'Automatic-Module-Name': moduleName, + 'Implementation-Version': archiveVersion + } } diff --git a/airgaplib/src/main/java/module-info.java b/airgaplib/src/main/java/module-info.java index ffece39..697e6f1 100644 --- a/airgaplib/src/main/java/module-info.java +++ b/airgaplib/src/main/java/module-info.java @@ -1,15 +1,13 @@ /** - * + * Java Platform Module System dependencies and exports for airgaplib. + * Note: We may backport this library to Java 8 and eliminate this file. */ module org.consensusj.airgap { requires org.bitcoinj.core; requires com.fasterxml.jackson.databind; requires com.fasterxml.jackson.core; - requires jackson.annotations; requires org.slf4j; - - requires com.google.common; - + exports org.consensusj.airgap; exports org.consensusj.airgap.json; exports org.consensusj.airgap.keychain; diff --git a/airgaplib/src/main/java/org/consensusj/airgap/AirGapTransactionSigner.java b/airgaplib/src/main/java/org/consensusj/airgap/AirGapTransactionSigner.java index c5b2ee0..a6f63a3 100644 --- a/airgaplib/src/main/java/org/consensusj/airgap/AirGapTransactionSigner.java +++ b/airgaplib/src/main/java/org/consensusj/airgap/AirGapTransactionSigner.java @@ -11,7 +11,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.bitcoinj.core.Address; import org.bitcoinj.core.Coin; -import org.bitcoinj.core.LegacyAddress; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.Sha256Hash; import org.bitcoinj.core.Transaction; @@ -98,6 +97,18 @@ public TransactionSignatureResponse signatureResponseFromSigningRequest(Transact return response; } + /** + * Create a signature response Json string (without display or confirmation) for a signing request Json string + * + * @param requestJsonString Request Json string + * @return Signed response Json string + */ + public String signatureResponseFromSigningRequestJson(String requestJsonString) { + TransactionSigningRequest request = parseSigningRequestJson(requestJsonString); + TransactionSignatureResponse response = signatureResponseFromSigningRequest(request); + return serializeResponse(response); + } + /** * Create an {@code InputSignature} for transaction input * @@ -139,10 +150,12 @@ public Transaction signTransaction(TransactionSigningRequest request) { TransactionOutPoint outPoint = new TransactionOutPoint(netParams, input.getIndex(), Sha256Hash.wrap(input.getTxHash())); - Address fromAddr = LegacyAddress.fromBase58(netParams, input.getSender()); + Address fromAddr = Address.fromString(netParams, input.getSender()); DeterministicKey fromKey = keyChain.findKeyFromPubHash(fromAddr.getHash()); tx.addSignedInput(outPoint, ScriptBuilder.createOutputScript(fromAddr), fromKey); } return tx; } + + } diff --git a/airgaplib/src/main/java/org/consensusj/airgap/BipStandardKeyChainGroupStructure.java b/airgaplib/src/main/java/org/consensusj/airgap/BipStandardKeyChainGroupStructure.java index b29f7a8..d6a209f 100644 --- a/airgaplib/src/main/java/org/consensusj/airgap/BipStandardKeyChainGroupStructure.java +++ b/airgaplib/src/main/java/org/consensusj/airgap/BipStandardKeyChainGroupStructure.java @@ -6,8 +6,6 @@ import org.bitcoinj.script.Script; import org.bitcoinj.wallet.KeyChainGroupStructure; -import java.util.Collections; - /** * KeyChainGroupStructure that supports BIP44, etc. This should be part of bitcoinj. */ @@ -25,8 +23,8 @@ public class BipStandardKeyChainGroupStructure implements KeyChainGroupStructure public static ChildNumber CHANGE_RECEIVING = new ChildNumber(0, false); public static ChildNumber CHANGE_CHANGE = new ChildNumber(1, false); - private static final HDPath BIP44_PARENT = new HDPath(true, Collections.singletonList(PURPOSE_BIP44)); - private static final HDPath BIP84_PARENT = new HDPath(true, Collections.singletonList(PURPOSE_BIP84)); + private static final HDPath BIP44_PARENT = HDPath.m(PURPOSE_BIP44); + private static final HDPath BIP84_PARENT = HDPath.m(PURPOSE_BIP84); public BipStandardKeyChainGroupStructure(NetworkParameters networkParameters) { if (networkParameters.getId().equals(NetworkParameters.ID_MAINNET)) { diff --git a/airgaplib/src/main/java/org/consensusj/airgap/SignedResponseHandler.java b/airgaplib/src/main/java/org/consensusj/airgap/SignedResponseHandler.java index fc7a81f..7b3ca85 100644 --- a/airgaplib/src/main/java/org/consensusj/airgap/SignedResponseHandler.java +++ b/airgaplib/src/main/java/org/consensusj/airgap/SignedResponseHandler.java @@ -4,13 +4,11 @@ import org.consensusj.airgap.json.TransactionSignatureResponse; import org.bitcoinj.core.Address; import org.bitcoinj.core.ECKey; -import org.bitcoinj.core.LegacyAddress; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.SignatureDecodeException; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionInput; import org.bitcoinj.crypto.TransactionSignature; -import org.bitcoinj.params.TestNet3Params; import org.bitcoinj.script.Script; import org.bitcoinj.script.ScriptBuilder; import org.bitcoinj.script.ScriptException; @@ -25,9 +23,15 @@ */ public class SignedResponseHandler { private static final Logger log = LoggerFactory.getLogger(SignedResponseHandler.class); - private final NetworkParameters netParams = TestNet3Params.get(); + private final Script.ScriptType scriptType; + private final NetworkParameters netParams; private final boolean verify = true; + public SignedResponseHandler(NetworkParameters netParams, Script.ScriptType scriptType) { + this.netParams = netParams; + this.scriptType = scriptType; + } + public void signWithResponse(Transaction transaction, TransactionSignatureResponse response) throws SignatureDecodeException { List inputSignatures = response.getTransaction().getInputSignatures(); int inputIndex = 0; @@ -36,7 +40,7 @@ public void signWithResponse(Transaction transaction, TransactionSignatureRespon ECKey pubKey = pubKeyFromString(inputSig.getEcPublicKey()); this.signInput(transaction.getInput(inputIndex), signature, pubKey); if (verify) { - correctlySpendsInput(transaction, inputIndex, LegacyAddress.fromKey(netParams, pubKey)); + correctlySpendsInput(transaction, inputIndex, Address.fromKey(netParams, pubKey, scriptType)); } inputIndex++; } @@ -79,7 +83,7 @@ public void signInput(TransactionInput input, TransactionSignature signature, EC * @param fromAddr The address we are trying to spend funds from * @throws ScriptException If {@code scriptSig#correctlySpends} fails with exception */ - protected static void correctlySpendsInput(Transaction tx, int inputIndex, Address fromAddr) throws ScriptException { + static void correctlySpendsInput(Transaction tx, int inputIndex, Address fromAddr) throws ScriptException { log.info("About to validate signed input {} of transaction {}", inputIndex, tx.getTxId().toString()); Script scriptSig = tx.getInputs().get(inputIndex).getScriptSig(); Script scriptPubKey = ScriptBuilder.createOutputScript(fromAddr); diff --git a/airgaplib/src/main/java/org/consensusj/airgap/SignedResponseParser.java b/airgaplib/src/main/java/org/consensusj/airgap/SignedResponseParser.java index 3f7a312..3c2ad1f 100644 --- a/airgaplib/src/main/java/org/consensusj/airgap/SignedResponseParser.java +++ b/airgaplib/src/main/java/org/consensusj/airgap/SignedResponseParser.java @@ -25,7 +25,7 @@ * */ public class SignedResponseParser { - private final ObjectMapper mapper = new ObjectMapper();; + private final ObjectMapper mapper = new ObjectMapper(); public TransactionSignatureResponse parse(String json) throws IOException { TransactionSignatureResponse response = mapper.readValue(json, TransactionSignatureResponse.class); diff --git a/airgaplib/src/main/java/org/consensusj/airgap/keychain/BipStandardDeterministicKeyChain.java b/airgaplib/src/main/java/org/consensusj/airgap/keychain/BipStandardDeterministicKeyChain.java index 5070ddc..00aa2bc 100644 --- a/airgaplib/src/main/java/org/consensusj/airgap/keychain/BipStandardDeterministicKeyChain.java +++ b/airgaplib/src/main/java/org/consensusj/airgap/keychain/BipStandardDeterministicKeyChain.java @@ -1,7 +1,7 @@ package org.consensusj.airgap.keychain; +import org.bitcoinj.core.Address; import org.consensusj.airgap.BipStandardKeyChainGroupStructure; -import org.bitcoinj.core.LegacyAddress; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.crypto.ChildNumber; import org.bitcoinj.crypto.DeterministicKey; @@ -22,6 +22,7 @@ public class BipStandardDeterministicKeyChain extends DeterministicKeyChain { private static final BipStandardKeyChainGroupStructure bip44KeyChainGroupStructure = new BipStandardKeyChainGroupStructure(TestNet3Params.get()); private final NetworkParameters netParams = TestNet3Params.get(); + private final Script.ScriptType outputScriptType; private final HDPath pathAccount; private final HDPath pathReceiving; private final HDPath pathChange; @@ -36,12 +37,13 @@ public class BipStandardDeterministicKeyChain extends DeterministicKeyChain { */ public BipStandardDeterministicKeyChain(DeterministicSeed seed, Script.ScriptType outputScriptType, int accountIndex) { super(seed, null, outputScriptType, bip44KeyChainGroupStructure.accountHDPathFor(outputScriptType, accountIndex)); + this.outputScriptType = outputScriptType; pathAccount = bip44KeyChainGroupStructure.accountHDPathFor(outputScriptType, accountIndex); pathReceiving = pathAccount.extend(BipStandardKeyChainGroupStructure.CHANGE_RECEIVING); pathChange = pathAccount.extend(BipStandardKeyChainGroupStructure.CHANGE_CHANGE); } - public LegacyAddress receivingAddr(int index) { + public Address receivingAddr(int index) { HDPath indexPath = pathReceiving.extend(new ChildNumber(index)); return address(indexPath); } @@ -50,7 +52,7 @@ public DeterministicKey receivingKey(int index) { return key(pathReceiving.extend(new ChildNumber(index))); } - public LegacyAddress changeAddr(int index) { + public Address changeAddr(int index) { HDPath indexPath = pathChange.extend(new ChildNumber(index)); return address(indexPath); } @@ -63,8 +65,8 @@ public DeterministicKey key(HDPath indexPath) { return getKeyByPath(indexPath,true); } - public LegacyAddress address(HDPath indexPath) { + public Address address(HDPath indexPath) { DeterministicKey key = key(indexPath); - return LegacyAddress.fromKey(netParams, key); + return Address.fromKey(netParams, key, outputScriptType); } } diff --git a/airgaplib/src/test/groovy/org/consensusj/airgap/DeterministicKeychainBaseSpec.groovy b/airgaplib/src/test/groovy/org/consensusj/airgap/DeterministicKeychainBaseSpec.groovy index eadb501..d38bf97 100644 --- a/airgaplib/src/test/groovy/org/consensusj/airgap/DeterministicKeychainBaseSpec.groovy +++ b/airgaplib/src/test/groovy/org/consensusj/airgap/DeterministicKeychainBaseSpec.groovy @@ -4,7 +4,6 @@ import org.consensusj.airgap.keychain.BipStandardDeterministicKeyChain import org.bitcoinj.core.Address import org.bitcoinj.core.Coin import org.bitcoinj.core.ECKey -import org.bitcoinj.core.LegacyAddress import org.bitcoinj.core.NetworkParameters import org.bitcoinj.core.Transaction import org.bitcoinj.core.TransactionOutPoint @@ -33,10 +32,13 @@ abstract class DeterministicKeychainBaseSpec extends Specification { public static final Instant creationInstant = LocalDate.of(2019, 4, 10).atStartOfDay().toInstant(ZoneOffset.UTC) public static final BipStandardKeyChainGroupStructure bip44KeyChainGroupStructure = new BipStandardKeyChainGroupStructure(TestNet3Params.get()) public static final int signingAccountIndex = 0 - public static final Script.ScriptType outputScriptType = Script.ScriptType.P2PKH - public static final HDPath signingAccountPath = bip44KeyChainGroupStructure.accountHDPathFor(outputScriptType, signingAccountIndex) + //public static final Script.ScriptType outputScriptType = Script.ScriptType.P2PKH + public static final HDPath signingAccountPath = bip44KeyChainGroupStructure.accountHDPathFor(Script.ScriptType.P2PKH, signingAccountIndex) protected static final NetworkParameters netParams = TestNet3Params.get() - + + @Shared + Script.ScriptType outputScriptType + @Shared BipStandardDeterministicKeyChain signingKeychain @@ -44,6 +46,7 @@ abstract class DeterministicKeychainBaseSpec extends Specification { KeyChainGroup keyChainGroup def setupSpec() { + outputScriptType = Script.ScriptType.P2PKH; DeterministicSeed seed = new DeterministicSeed(mnemonicString, null, "", creationInstant.getEpochSecond()) signingKeychain = new BipStandardDeterministicKeyChain(seed, outputScriptType, signingAccountIndex); // We need to create some leaf keys in the HD keychain so that they can be found for verifying transactions @@ -81,7 +84,7 @@ abstract class DeterministicKeychainBaseSpec extends Specification { * @param changeAmount amount to send to change address * @return A ready-to-sign transaction with OP_0 in the signature slot */ - static Transaction buildTestTransaction(ECKey fromKey, TransactionOutput fromOutput, LegacyAddress toAddress, LegacyAddress changeAddress, Coin toAmount, Coin changeAmount) { + static Transaction buildTestTransaction(ECKey fromKey, TransactionOutput fromOutput, Address toAddress, Address changeAddress, Coin toAmount, Coin changeAmount) { Transaction tx = new Transaction(toAddress.getParameters()) tx.addInput(fromOutput) tx.addOutput(toAmount, toAddress) @@ -91,8 +94,9 @@ abstract class DeterministicKeychainBaseSpec extends Specification { return tx } - static Transaction buildSignedTestTransaction(ECKey fromKey, TransactionOutput fromOutput, LegacyAddress toAddress, LegacyAddress changeAddress, Coin toAmount, Coin changeAmount) { - LegacyAddress fromAddress = LegacyAddress.fromKey(toAddress.getParameters(), fromKey) + static Transaction buildSignedTestTransaction(ECKey fromKey, TransactionOutput fromOutput, Address toAddress, Address changeAddress, Coin toAmount, Coin changeAmount) { + Script.ScriptType scriptType = changeAddress.outputScriptType; // Use the change address to determine the script type + Address fromAddress = Address.fromKey(toAddress.getParameters(), fromKey, scriptType) Transaction tx = new Transaction(toAddress.getParameters()) tx.addOutput(toAmount, toAddress) tx.addOutput(changeAmount, changeAddress) @@ -104,31 +108,19 @@ abstract class DeterministicKeychainBaseSpec extends Specification { protected Transaction originalFundingTransaction() { ECKey fromKey = signingKeychain.receivingKey(0) TransactionOutput utxo = RoundtripTest.initial_tx.getOutput(0) - LegacyAddress toAddr = signingKeychain.receivingAddr(1) + Address toAddr = signingKeychain.receivingAddr(1) Coin toAmount = 0.01.btc Coin changeAmount = RoundtripTest.changeAmount - LegacyAddress changeAddr = signingKeychain.changeAddr(0) + Address changeAddr = signingKeychain.changeAddr(0) def tx = buildTestTransaction(fromKey, utxo, toAddr, changeAddr, toAmount, changeAmount) return tx } - -// protected Transaction firstChangeTransaction() { -// ECKey fromKey = keyChain.changeKey(0) -// TransactionOutput utxo = RoundtripTest.change_tx.getOutput(1) -// LegacyAddress toAddr = keyChain.receivingAddr(0) -// Coin toAmount = 0.01.btc -// Coin changeAmount = RoundtripTest.changeAmount2 -// LegacyAddress changeAddr = keyChain.changeAddr(1) -// def tx = buildTestTransaction(fromKey, utxo, toAddr, changeAddr, toAmount, changeAmount) -// return tx -// } - // change_tx protected Transaction firstChangeTransaction() { return RoundtripTest.change_tx } - static LegacyAddress addressFromKey(DeterministicKey key) { - return LegacyAddress.fromKey(netParams, key); + Address addressFromKey(DeterministicKey key) { + return Address.fromKey(netParams, key, outputScriptType); } } diff --git a/airgaplib/src/test/groovy/org/consensusj/airgap/KeychainRoundtripStepwiseTest.groovy b/airgaplib/src/test/groovy/org/consensusj/airgap/KeychainRoundtripStepwiseTest.groovy index 568e6a8..b1481e8 100644 --- a/airgaplib/src/test/groovy/org/consensusj/airgap/KeychainRoundtripStepwiseTest.groovy +++ b/airgaplib/src/test/groovy/org/consensusj/airgap/KeychainRoundtripStepwiseTest.groovy @@ -1,8 +1,7 @@ package org.consensusj.airgap - +import org.bitcoinj.core.Address import org.bitcoinj.core.Coin -import org.bitcoinj.core.LegacyAddress import org.bitcoinj.core.Transaction import org.bitcoinj.core.TransactionOutput import org.bitcoinj.crypto.ChildNumber @@ -42,9 +41,9 @@ class KeychainRoundtripStepwiseTest extends DeterministicKeychainBaseSpec { @Shared TransactionSignatureResponse response @Shared Transaction transaction @Shared DeterministicKey fromKey - @Shared LegacyAddress fromAddr + @Shared Address fromAddr - def "Can create an xpub string from signing keychain"() { + def "SIGNING wallet can create an xpub string from signing keychain"() { expect: "Test setup provides a DeterministicKeyChain initialized with the Panda Diary seed" signingKeychain != null @@ -62,7 +61,7 @@ class KeychainRoundtripStepwiseTest extends DeterministicKeychainBaseSpec { xpub == "tpubDDpSwdfCsfnYP8SH7YZvu1LK3BUMr3RQruCKTkKdtnHy2iBNJWn1CYvLwgskZxVNBV4KhicZ4FfgFCGjTwo4ATqdwoQcb5UjJ6ejaey5Ff8" } - def "Can create a network keychain from the xpub"() { + def "NETWORK wallet can create a network keychain from the xpub"() { when: "we create a network keychain from the xpub" DeterministicKey key = DeterministicKey.deserializeB58(xpub, netParams) key.creationTimeSeconds = xpubCreationInstant.epochSecond @@ -73,30 +72,30 @@ class KeychainRoundtripStepwiseTest extends DeterministicKeychainBaseSpec { networkKeyChain = DeterministicKeyChain.builder().watch(key).outputScriptType(outputScriptType).build() and: "we fetch the keys that are used in later steps" - DeterministicKey fromKey = networkKeyChain.getKeyByPath(HDPath.of(networkAccountPath).extend(fromKeyPath), true) - DeterministicKey toKey = networkKeyChain.getKeyByPath(HDPath.of(networkAccountPath).extend(toKeyPath), true) - DeterministicKey changeKey = networkKeyChain.getKeyByPath(HDPath.of(networkAccountPath).extend(changeKeyPath), true) + DeterministicKey fromKey = networkKeyChain.getKeyByPath(HDPath.M(networkAccountPath).extend(fromKeyPath), true) + DeterministicKey toKey = networkKeyChain.getKeyByPath(HDPath.M(networkAccountPath).extend(toKeyPath), true) + DeterministicKey changeKey = networkKeyChain.getKeyByPath(HDPath.M(networkAccountPath).extend(changeKeyPath), true) then: "the pubkeys in the network keychain match the pubkeys in the signing keychain" networkKeyChain != null networkKeyChain.isWatching() - fromKey.getPubKey() == signingKeychain.getKeyByPath(HDPath.of(signingAccountPath).extend(fromKeyPath), false).getPubKey() - toKey.getPubKey() == signingKeychain.getKeyByPath(HDPath.of(signingAccountPath).extend(toKeyPath), false).getPubKey() - changeKey.getPubKey() == signingKeychain.getKeyByPath(HDPath.of(signingAccountPath).extend(changeKeyPath), false).getPubKey() + fromKey.getPubKey() == signingKeychain.getKeyByPath(HDPath.m(signingAccountPath).extend(fromKeyPath), false).getPubKey() + toKey.getPubKey() == signingKeychain.getKeyByPath(HDPath.m(signingAccountPath).extend(toKeyPath), false).getPubKey() + changeKey.getPubKey() == signingKeychain.getKeyByPath(HDPath.m(signingAccountPath).extend(changeKeyPath), false).getPubKey() } def "NETWORK wallet can create a transaction and serialize a JSON signing request "() { given: "a transaction with a UTXO in output 1" // This is actually the first transaction received by the // 0'th change address in our "panda diary" keychain. - fromKey = networkKeyChain.getKeyByPath(HDPath.of(networkAccountPath).extend(fromKeyPath), false) + fromKey = networkKeyChain.getKeyByPath(HDPath.M(networkAccountPath).extend(fromKeyPath), false) fromAddr = addressFromKey(fromKey) Transaction utxo_tx = firstChangeTransaction() TransactionOutput utxo = utxo_tx.getOutput(1) when: "we build a 1-input, 2-output (unsigned) transaction to spend the UTXO" - LegacyAddress toAddr = addressFromKey(networkKeyChain.getKeyByPath(HDPath.of(networkAccountPath).extend(toKeyPath), false)) - LegacyAddress changeAddr = addressFromKey(networkKeyChain.getKeyByPath(HDPath.of(networkAccountPath).extend(changeKeyPath), false)) + Address toAddr = addressFromKey(networkKeyChain.getKeyByPath(HDPath.M(networkAccountPath).extend(toKeyPath), false)) + Address changeAddr = addressFromKey(networkKeyChain.getKeyByPath(HDPath.M(networkAccountPath).extend(changeKeyPath), false)) Coin txAmount = 0.01.btc Coin changeAmount = 0.20990147.btc transaction = buildTestTransaction(fromKey, utxo, toAddr, changeAddr, txAmount, changeAmount) @@ -111,7 +110,7 @@ class KeychainRoundtripStepwiseTest extends DeterministicKeychainBaseSpec { // TODO: More checks } - def "AIRGAP wallet can use the signing request JSON to generate a signed response"() { + def "SIGNING wallet can use the signing request JSON to generate a signed response"() { given: "An Airgap transaction signer object " // (same basic functionality as an airgap device/wallet) AirGapTransactionSigner signer = new AirGapTransactionSigner(signingKeychain) @@ -146,7 +145,7 @@ class KeychainRoundtripStepwiseTest extends DeterministicKeychainBaseSpec { def "NETWORK wallet can sign and verify a transaction using the signature from the JSON"() { given: - def responseHandler = new SignedResponseHandler() + def responseHandler = new SignedResponseHandler(netParams, outputScriptType) when: "we use the signature and pubKey from the response to sign the input" responseHandler.signWithResponse(transaction, response) diff --git a/build.gradle b/build.gradle index 1dedb76..89fe279 100644 --- a/build.gradle +++ b/build.gradle @@ -9,9 +9,9 @@ buildscript { } plugins { - id 'com.gradle.build-scan' version '2.3' id 'org.asciidoctor.jvm.convert' version '2.2.0' id 'com.jfrog.bintray' version '1.8.4' + id 'de.undercouch.download' version '4.0.1' } buildScan { @@ -23,6 +23,9 @@ buildScan { termsOfServiceAgree = 'yes' } +rootProject.ext.signJPackageImages = signJPackageImages == "true" // create boolean in ext from property string +logger.warn "rootProject.ext.signJPackageImages = ${rootProject.ext.signJPackageImages}" + allprojects { apply plugin: 'java' @@ -31,6 +34,7 @@ allprojects { repositories { jcenter() + //mavenLocal() // Uncomment to work with local snapshots maven { url 'https://dl.bintray.com/consensusj/maven' } maven { url 'https://dl.bintray.com/msgilligan/maven' } // ConsensusJ (bitcoinj DSL) maven { url 'https://dl.bintray.com/omni/maven' } // OmniJ (RPC client) @@ -43,9 +47,8 @@ subprojects { apply plugin: 'groovy' /* * Wallet-Framework will target recent JDK versions. For now library modules will - * target JDK 11 and sample apps will also use JDK 11. We intend to migrate to - * JDK 14 as soon as it is final, since 14 should have `jpackage` included. - * We want to focus on jpackaged apps and these should always use recent JDK. + * target JDK 11 and sample apps will be built using JDK 14. + * We want to focus on jpackage'd apps and these should always use recent JDK. */ sourceCompatibility = 11 targetCompatibility = 11 @@ -53,8 +56,8 @@ subprojects { dependencies { implementation "org.slf4j:slf4j-api:${slf4jVersion}" - testCompile "org.codehaus.groovy:groovy:${groovyVersion}:" - testCompile("org.spockframework:spock-core:${spockVersion}") { + testImplementation "org.codehaus.groovy:groovy:${groovyVersion}:" + testImplementation("org.spockframework:spock-core:${spockVersion}") { exclude module: "groovy-all" } @@ -68,11 +71,8 @@ subprojects { } } -//apply from: 'gradle/idea.gradle' -//apply from: 'gradle/javadoc.gradle' -//apply from: 'gradle/asciidoctor.gradle' -//apply from: 'gradle/github-pages.gradle' apply from: 'gradle/bintray.gradle' +apply from: 'gradle/downloadJPackage.gradle' task testReport(type: TestReport) { destinationDir = file("$buildDir/reports/allTests") @@ -82,4 +82,29 @@ task testReport(type: TestReport) { build.dependsOn subprojects.build -task buildCI(dependsOn: [build, 'consensusj-netwallet-fx:jlink']) +task buildCI(dependsOn: [build, 'consensusj-netwallet-fx:jlink', 'consensusj-signwallet-fx:jlink']) + +task buildJPackages(dependsOn: [buildCI, 'consensusj-netwallet-fx:jpackage', 'consensusj-signwallet-fx:jpackage']) + +/** + * Massage the version string based upon the current OS to be valid + * for the installer platforms for that OS. rpmbuild, MSI, and potentially + * others have restrictions on valid version strings. + * + * @param appVersion A typical Gradle version string + * @return a version string that should work for the currrent platform + */ +String normalizeAppVersion(final String appVersion) { + def os = org.gradle.internal.os.OperatingSystem.current() + if (os.linux) { + // Replace '-' with '.' for rpmbuild + return appVersion.replace('-', '.') + } else if (os.windows) { + // This is a hack attempt to assure the version conforms to MSI productVersion string rules + // See https://docs.microsoft.com/en-us/windows/win32/msi/productversion + // For now, we'll just remove '-SNAPSHOT' if present. + return appVersion.replaceAll('-SNAPSHOT$', '') + } else { + return appVersion + } +} diff --git a/consensusj-netwallet-fx/build.gradle b/consensusj-netwallet-fx/build.gradle index ddf16e9..83e5be3 100644 --- a/consensusj-netwallet-fx/build.gradle +++ b/consensusj-netwallet-fx/build.gradle @@ -1,12 +1,14 @@ plugins { id 'groovy' id 'application' - id 'org.openjfx.javafxplugin' version '0.0.8' - id 'org.beryx.jlink' version '2.16.0' - id 'org.javamodularity.moduleplugin' version "1.5.0" - id "de.undercouch.download" version "4.0.0" + id 'org.openjfx.javafxplugin' version '0.0.9' + id 'org.beryx.jlink' version '2.17.3' } +def appName = 'ConsensusJWallet' +mainClassName = "netwalletfx.NetWalletFxApp" +//mainClassName = "$moduleName/netwalletfx.NetWalletFxApp" + sourceCompatibility = 11 targetCompatibility = 11 @@ -32,7 +34,10 @@ dependencies { implementation "io.micronaut:micronaut-inject-java:${micronautVersion}" implementation "javax.inject:javax.inject:1" - implementation 'com.google.guava:guava:27.1-android' + implementation ('com.google.guava:guava:28.1-android') { + // prevent conflict with `io.micronaut.core` + exclude group: 'com.google.errorprone', module: 'error_prone_annotations' + } implementation 'de.jensd:fontawesomefx:8.0.0' implementation 'com.google.zxing:core:3.4.0' @@ -51,20 +56,17 @@ dependencies { } patchModules.config = [ - "java.annotation=jsr305-3.0.2.jar" + "java.annotation=jsr305-3.0.2.jar", + "org.checkerframework.checker.qual=checker-qual-2.8.1.jar", + "com.error.prone.annotations,com.error.prone.annotations.concurrent=error_prone_annotations-2.3.2.jar" ] javafx { - version = 13 - modules = [ 'javafx.controls', 'javafx.graphics', 'javafx.fxml', 'javafx.swing'] + version = javaFxVersion + modules = ['javafx.graphics', 'javafx.controls', 'javafx.fxml', 'javafx.swing'] } -//mainClassName = "$moduleName/airgapfxwallet.AirgapFxWalletApp" -mainClassName = "netwalletfx.AirgapFxWalletApp" - -def appName = 'ConsensusJWallet' - -ext.os = org.gradle.internal.os.OperatingSystem.current() +def os = org.gradle.internal.os.OperatingSystem.current() jlink { addExtraDependencies("javafx") @@ -87,61 +89,26 @@ jlink { // See https://badass-jlink-plugin.beryx.org/releases/latest/#_jpackage for // where the plugin's jpackage task finds the path to the jpackage tool by default if (Boolean.getBoolean('download.jpackage')) { - jpackageHome = downloadJPackage() + jpackageHome = rootProject.ext.jpackageHome } skipInstaller = false - // rpmbuild doesn't like '-' characters in version - def appVersionForJpackage = os.linux ? version.replace('-', '.') : version + // Massage version string to be compatible with jpackage installers + // for the current OS platform + def appVersionForJpackage = normalizeAppVersion(version) - // macOS options - imageOptions = ["--verbose", "--resource-dir", "${projectDir}/src/macos/resource-dir", "--app-version", appVersionForJpackage] + imageOptions = ["--verbose", "--app-version", appVersionForJpackage] installerOptions = ["--app-version", appVersionForJpackage] - } -} - -// #### The code below is needed only if you use the downloadJPackage() method to install the jpackage tool #### -// Code copied from build.gradle in https://github.com/beryx/fxgl-sliding-puzzle/ - -/** @return [url, extension, directory] */ -String[] getJPackageCoordinates() { - def jpackageBaseUrl = 'https://s3-us-west-2.amazonaws.com/static.msgilligan.com/jpackage' - def jpackageVersionString = 'openjdk-14-jpackage%2B1-49' - - if(os.macOsX) return [ - "${jpackageBaseUrl}/${jpackageVersionString}_osx-x64_bin.tar.gz", - 'tar.gz', - 'jdk-14.jdk/Contents/Home' - ] - if(os.windows) return [ - "${jpackageBaseUrl}/${jpackageVersionString}_windows-x64_bin.zip", - 'zip', - 'jdk-14' - ] - return [ - "${jpackageBaseUrl}/${jpackageVersionString}_linux-x64_bin.tar.gz", - 'tar.gz', - 'jdk-14' - ] -} - -String downloadJPackage() { - def (url, extension, directory) = getJPackageCoordinates() - def downloadDir = "$buildDir/download" - tasks.jpackageImage.doFirst { - def execExt = os.windows ? '.exe' : '' - if(!file("$downloadDir/$directory/bin/jpackage$execExt").file) { - def jdkArchivePath = "$downloadDir/jdk-jpackage.$extension" - download { - src url - dest jdkArchivePath - overwrite false - } - copy { - from ((extension == 'tar.gz') ? tarTree(resources.gzip(jdkArchivePath)) : zipTree(jdkArchivePath)) - into downloadDir + if (os.macOsX) { + imageOptions += [ '--resource-dir', "${projectDir}/src/macos/resource-dir" ] + if (rootProject.ext.signJPackageImages) { + logger.warn "Setting --mac-sign in jpackage imageOptions" + imageOptions += [ '--mac-sign' ] } } } - return "$downloadDir/$directory" +} + +if (Boolean.getBoolean('download.jpackage')) { + jpackage.dependsOn rootProject.expandJPackage } diff --git a/consensusj-netwallet-fx/src/macos/resource-dir/BlockchainCommonsLogo.png b/consensusj-netwallet-fx/src/macos/resource-dir/BlockchainCommonsLogo.png deleted file mode 100644 index bb12bfd..0000000 Binary files a/consensusj-netwallet-fx/src/macos/resource-dir/BlockchainCommonsLogo.png and /dev/null differ diff --git a/consensusj-netwallet-fx/src/main/java/netwalletfx/AirGapSigner.java b/consensusj-netwallet-fx/src/main/java/netwalletfx/AirGapSigner.java index 911baee..227990c 100644 --- a/consensusj-netwallet-fx/src/main/java/netwalletfx/AirGapSigner.java +++ b/consensusj-netwallet-fx/src/main/java/netwalletfx/AirGapSigner.java @@ -16,6 +16,16 @@ package netwalletfx; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.MoreExecutors; +import org.bitcoinj.core.SignatureDecodeException; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionBroadcaster; +import org.bitcoinj.script.Script; +import org.bitcoinj.walletfx.WalletSettingsController; +import org.consensusj.airgap.SignedResponseHandler; +import org.consensusj.airgap.SignedResponseParser; import org.consensusj.airgap.UnsignedTxQrGenerator; import javafx.scene.effect.DropShadow; import javafx.scene.image.Image; @@ -27,18 +37,32 @@ import org.bitcoinj.walletfx.SendMoneyController; import org.bitcoinj.walletfx.utils.QRCodeImages; import org.bitcoinj.walletfx.HardwareSigner; +import org.consensusj.airgap.fx.components.QrCaptureView; +import org.consensusj.airgap.json.TransactionSignatureResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URL; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; /** * Incomplete implementation of signing using the Airgap protocol * TODO: Get the signed transaction and pass it to the "Send" function */ public class AirGapSigner implements HardwareSigner { + private static final Logger log = LoggerFactory.getLogger(AirGapSigner.class); private final UnsignedTxQrGenerator qrJsonGenerator; - private final OverlayableWindowController windowController; + private final NetWalletFxMainWindowController windowController; + private final SignedResponseParser signedResponseParser = new SignedResponseParser(); + private final SignedResponseHandler signedResponseHandler; + private ExecutorService executorService = Executors.newFixedThreadPool(1); private SendRequest pendingSendRequest; - public AirGapSigner(Wallet wallet, OverlayableWindowController windowController) { + public AirGapSigner(Wallet wallet, NetWalletFxMainWindowController windowController) { qrJsonGenerator = new UnsignedTxQrGenerator(wallet.getParams(), wallet.getActiveKeyChain()); + signedResponseHandler = new SignedResponseHandler(wallet.getParams(), Script.ScriptType.P2PKH); this.windowController = windowController; } @@ -47,6 +71,19 @@ public void displaySigningOverlay(SendRequest sendRequest, SendMoneyController s pendingSendRequest = sendRequest; String qrJson = qrJsonGenerator.createSigningRequestString(pendingSendRequest.tx); Image qrImage = QRCodeImages.imageFromString(qrJson, 600, 450); + + + URL location = AirGapSigner.class.getResource("QrSigningView.fxml"); + final OverlayableWindowController.OverlayUI signingViewOverlay = windowController.overlayUI(location); + signingViewOverlay.ui.setEffect(new DropShadow()); + signingViewOverlay.controller.setUnsignedQrImage(qrImage); + signingViewOverlay.controller.initCaptureView(windowController.app.cameraService, this::scanListener); + } + + public void oldDisplaySigningOverlay(SendRequest sendRequest, SendMoneyController sendMoneyController) { + pendingSendRequest = sendRequest; + String qrJson = qrJsonGenerator.createSigningRequestString(pendingSendRequest.tx); + Image qrImage = QRCodeImages.imageFromString(qrJson, 600, 450); ImageView view = new ImageView(qrImage); view.setEffect(new DropShadow()); // Embed the image in a pane to ensure the drop-shadow interacts with the fade nicely, otherwise it looks weird. @@ -63,6 +100,52 @@ public String getButtonText() { return "Airgap Sign"; } + public void scanListener(String result) { + log.info("QR Scan Result {}", result); + SendRequest sendRequest = getPendingTransaction(); + + TransactionSignatureResponse response = null; + try { + response = signedResponseParser.parse(result); + } catch (IOException e) { + // TODO: Handle exception properly + e.printStackTrace(); + throw new RuntimeException(e); + } + try { + signedResponseHandler.signWithResponse(sendRequest.tx, response); + } catch (SignatureDecodeException e) { + e.printStackTrace(); + } + + executorService.submit(() -> { + broadcastTransaction(sendRequest.tx); + }); + } + + private void broadcastTransaction(Transaction transaction) { + log.info("Preparing to broadcast tx: {{}", transaction); + TransactionBroadcaster broadcaster = windowController.app.getWalletAppKit().peerGroup(); + var broadcast = broadcaster.broadcastTransaction(transaction); + Futures.addCallback(broadcast.future(), new FutureCallback(){ + + @Override + public void onSuccess(Transaction transaction) { + log.info("Broadcast success, committing transaction: {}", transaction); + // Commit tx to wallet + boolean accepted = windowController.app.getWallet().maybeCommitTx(transaction); + if (!accepted) { + log.warn("Transaction already pending"); + } + } + + @Override + public void onFailure(Throwable t) { + log.error("Broadcast failure", t); + } + }, MoreExecutors.directExecutor()); + } + public SendRequest getPendingTransaction() { return pendingSendRequest; } diff --git a/consensusj-netwallet-fx/src/main/java/netwalletfx/NetWalletFxMainWindowController.java b/consensusj-netwallet-fx/src/main/java/netwalletfx/NetWalletFxMainWindowController.java index 3b89a2d..464e1c8 100644 --- a/consensusj-netwallet-fx/src/main/java/netwalletfx/NetWalletFxMainWindowController.java +++ b/consensusj-netwallet-fx/src/main/java/netwalletfx/NetWalletFxMainWindowController.java @@ -16,13 +16,7 @@ package netwalletfx; -import org.consensusj.airgap.SignedResponseHandler; -import org.consensusj.airgap.SignedResponseParser; import org.consensusj.airgap.fx.components.QrCaptureView; -import org.consensusj.airgap.json.TransactionSignatureResponse; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.MoreExecutors; import javafx.animation.FadeTransition; import javafx.animation.ParallelTransition; import javafx.animation.TranslateTransition; @@ -37,10 +31,7 @@ import javafx.stage.Stage; import javafx.util.Duration; import org.bitcoinj.core.Coin; -import org.bitcoinj.core.SignatureDecodeException; import org.bitcoinj.core.Transaction; -import org.bitcoinj.core.TransactionBroadcaster; -import org.bitcoinj.wallet.SendRequest; import org.bitcoinj.walletfx.OverlayableWindowController; import org.bitcoinj.walletfx.SendMoneyController; import org.bitcoinj.walletfx.WalletMainWindowController; @@ -57,10 +48,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - /** * Main window controller for ConsensusJ Network Wallet @@ -71,16 +58,13 @@ public class NetWalletFxMainWindowController extends WalletMainWindowController @FXML private HBox controlsBox; @FXML private Label balance; @FXML private Button sendMoneyOutBtn; - @FXML private Button scanBtn; @FXML private ClickableBitcoinAddress addressControl; @FXML private ListView transactionListView; protected final NetWalletFxApp app; private AirGapSigner airGapHardwareSigner; - private final SignedResponseParser signedResponseParser = new SignedResponseParser(); - private final SignedResponseHandler signedResponseHandler = new SignedResponseHandler(); + private Stage standaloneQrScanStage; - private ExecutorService executorService = Executors.newFixedThreadPool(1); public NetWalletFxMainWindowController(NetWalletFxApp app) { super(app); @@ -101,9 +85,7 @@ public void onBitcoinSetup() { balance.textProperty().bind(createBalanceStringBinding(model.balanceProperty())); // Don't let the user click send money when the wallet is empty. sendMoneyOutBtn.disableProperty().bind(model.balanceProperty().isEqualTo(Coin.ZERO)); - // Don't let the user click the "scan QR" button if there is no camera - var cameraAvailable = app.cameraService != null; - scanBtn.setDisable(!cameraAvailable); + // We wait to onBitcoinSetup() to do this because prior to that getWallet() will return null. TransactionStringConverter converter = new TransactionStringConverter(this.app.getWallet()); transactionListView.setCellFactory(list -> { @@ -129,16 +111,16 @@ private void settingsClicked(ActionEvent event) { screen.controller.initialize(null); } - @FXML - public void scanClicked(ActionEvent actionEvent) { - log.info("scanClicked"); - - QrCaptureView captureView = new QrCaptureView(app.cameraService, this::scanListener); + /** + * Display old standalone QR capture window + */ + public void displayCaptureWindow() { + QrCaptureView captureView = new QrCaptureView(app.cameraService, this::scanListener, this::closeListener); Scene scene = new Scene(captureView); - final Stage stage = new Stage(); - stage.setScene(scene); - stage.show(); + standaloneQrScanStage = new Stage(); + standaloneQrScanStage.setScene(scene); + standaloneQrScanStage.show(); } @Override @@ -164,46 +146,13 @@ protected void readyToGoAnimation() { } private void scanListener(String result) { - log.info("QR Scan Result {}", result); - SendRequest sendRequest = airGapHardwareSigner.getPendingTransaction(); - - TransactionSignatureResponse response = null; - try { - response = signedResponseParser.parse(result); - } catch (IOException e) { - e.printStackTrace(); - } - try { - signedResponseHandler.signWithResponse(sendRequest.tx, response); - } catch (SignatureDecodeException e) { - e.printStackTrace(); - } - - executorService.submit(() -> { - broadcastTransaction(sendRequest.tx); - }); + airGapHardwareSigner.scanListener(result); } - private void broadcastTransaction(Transaction transaction) { - log.info("Preparing to broadcast tx: {{}", transaction); - TransactionBroadcaster broadcaster = app.getWalletAppKit().peerGroup(); - var broadcast = broadcaster.broadcastTransaction(transaction); - Futures.addCallback(broadcast.future(), new FutureCallback(){ - - @Override - public void onSuccess(Transaction transaction) { - log.info("Broadcast success, committing transaction: {}", transaction); - // Commit tx to wallet - boolean accepted = app.getWallet().maybeCommitTx(transaction); - if (!accepted) { - log.warn("Transaction already pending"); - } - } - - @Override - public void onFailure(Throwable t) { - log.error("Broadcast failure", t); - } - }, MoreExecutors.directExecutor()); + private void closeListener(Object unused) { + if (standaloneQrScanStage != null) { + standaloneQrScanStage.hide(); + } } + } diff --git a/consensusj-netwallet-fx/src/main/java/netwalletfx/QrSigningViewController.java b/consensusj-netwallet-fx/src/main/java/netwalletfx/QrSigningViewController.java new file mode 100644 index 0000000..831f2b8 --- /dev/null +++ b/consensusj-netwallet-fx/src/main/java/netwalletfx/QrSigningViewController.java @@ -0,0 +1,58 @@ +package netwalletfx; + +import javafx.fxml.FXML; +import javafx.scene.effect.DropShadow; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.VBox; +import org.bitcoinj.walletfx.OverlayWindowController; +import org.bitcoinj.walletfx.OverlayableWindowController; +import org.consensusj.airgap.fx.camera.CameraService; +import org.consensusj.airgap.fx.components.QrCaptureView; + +import javax.inject.Singleton; +import java.util.function.Consumer; + +/** + * + */ +@Singleton +public class QrSigningViewController implements OverlayWindowController { + private OverlayableWindowController.OverlayUI overlayUI; + @FXML private ImageView unsignedQrImage; + @FXML private VBox captureBox; + + private QrCaptureView captureView; + + public void setUnsignedQrImage(Image image) { + if (unsignedQrImage != null) { + unsignedQrImage.setImage(image); + } + } + + public void initCaptureView(CameraService cameraService, Consumer scanListener) { + // TODO: Make sure to gracefully handle case (e.g. inform user) where there is no camera + captureView = new QrCaptureView(cameraService, scanListener, this::closeListener); + if (captureBox != null) { + captureBox.getChildren().add(captureView); + } + } + + @Override + public OverlayableWindowController.OverlayUI getOverlayUI() { + return overlayUI; + } + + @Override + public void setOverlayUI(OverlayableWindowController.OverlayUI ui) { + overlayUI = ui; + } + + private void closeListener(Object result) { + closeOverlay(); + } + + void closeOverlay() { + overlayUI.done(); + } +} diff --git a/consensusj-netwallet-fx/src/main/resources/netwalletfx/QrSigningView.fxml b/consensusj-netwallet-fx/src/main/resources/netwalletfx/QrSigningView.fxml new file mode 100644 index 0000000..2fca539 --- /dev/null +++ b/consensusj-netwallet-fx/src/main/resources/netwalletfx/QrSigningView.fxml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/consensusj-netwallet-fx/src/main/resources/netwalletfx/main.fxml b/consensusj-netwallet-fx/src/main/resources/netwalletfx/main.fxml index bcc3287..ad0aed8 100644 --- a/consensusj-netwallet-fx/src/main/resources/netwalletfx/main.fxml +++ b/consensusj-netwallet-fx/src/main/resources/netwalletfx/main.fxml @@ -14,7 +14,7 @@ - +