diff --git a/.travis.yml b/.travis.yml
index 0d178eaa525..e052378f9c9 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,8 +1,8 @@
# configuration for https://travis-ci.org/bitcoincash-wallet/bitcoincashj
-sudo: false
+sudo: sudo
dist: trusty
language: java
-jdk: oraclejdk8
+jdk: openjdk8
addons:
postgresql: "9.3" # min supported version
cache:
@@ -14,6 +14,14 @@ services:
install: true # disable default because no need to do the mvn install before mvn verify
+before_install:
+ - wget https://oss.sonatype.org/content/repositories/releases/org/javafxports/dalvik-sdk/8.60.9/dalvik-sdk-8.60.9.zip && unzip dalvik-sdk-8.60.9.zip
+ - sudo cp dalvik-sdk/rt/lib/ext/jfxrt.jar $JAVA_HOME/jre/lib/ext
+ - sudo cp dalvik-sdk/rt/lib/jfxswt.jar $JAVA_HOME/jre/lib/
+ - sudo cp dalvik-sdk/rt/lib/javafx.properties $JAVA_HOME/jre/lib
+ - sudo cp dalvik-sdk/rt/lib/javafx.platform.properties $JAVA_HOME/jre/lib
+ - sudo cp dalvik-sdk/lib/javafx-mx.jar $JAVA_HOME/lib
+
before_script:
- psql -c "create user bitcoinj with password 'password';" -U postgres
- psql -c 'create database bitcoinj_test owner bitcoinj;' -U postgres
diff --git a/core/pom.xml b/core/pom.xml
index 6170a68a162..b016c23145a 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -460,6 +460,26 @@
1.8
true
+
+ org.apache.commons
+ commons-lang3
+ 3.3
+
+
+ commons-io
+ commons-io
+ 2.5
+
+
+ com.madgag.spongycastle
+ prov
+ 1.51.0.0
+
+
+ com.google.code.gson
+ gson
+ 2.8.2
+
@@ -487,4 +507,4 @@
test
-
+
\ No newline at end of file
diff --git a/core/src/main/java/org/bitcoinj/core/BlockChain.java b/core/src/main/java/org/bitcoinj/core/BlockChain.java
index 6fd6ca2237e..79c6c90f947 100644
--- a/core/src/main/java/org/bitcoinj/core/BlockChain.java
+++ b/core/src/main/java/org/bitcoinj/core/BlockChain.java
@@ -99,7 +99,7 @@ protected StoredBlock addToBlockStore(StoredBlock storedPrev, Block blockHeader)
}
@Override
- protected void rollbackBlockStore(int height) throws BlockStoreException {
+ public void rollbackBlockStore(int height) throws BlockStoreException {
lock.lock();
try {
int currentHeight = getBestChainHeight();
diff --git a/core/src/main/java/org/bitcoinj/core/bip47/BIP47Account.java b/core/src/main/java/org/bitcoinj/core/bip47/BIP47Account.java
new file mode 100644
index 00000000000..4f5108bf7fa
--- /dev/null
+++ b/core/src/main/java/org/bitcoinj/core/bip47/BIP47Account.java
@@ -0,0 +1,98 @@
+/* Copyright (c) 2017 Stash
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+package org.bitcoinj.core.bip47;
+
+import org.bitcoinj.core.Address;
+import org.bitcoinj.core.ECKey;
+import org.bitcoinj.core.NetworkParameters;
+import org.bitcoinj.crypto.ChildNumber;
+import org.bitcoinj.crypto.DeterministicKey;
+import org.bitcoinj.crypto.HDKeyDerivation;
+
+import static org.bitcoinj.core.bip47.BIP47PaymentCode.createMasterPubKeyFromPaymentCode;
+
+/**
+ * Created by jimmy on 8/4/17.
+ */
+
+/**
+ * p>A {@link BIP47Account} is necessary for BIP47 payment channels. It holds the notification key used to derive the
+ * notification address and the key to derive payment addresses in a channel.
+ *
+ * The BIP47 account is at the derivation path
m / 47' / coin_type' / account_id'.
.
+ *
+ * Properties:
+ *
+ */
+public class BIP47Account {
+ private NetworkParameters mNetworkParameters;
+ private DeterministicKey mKey;
+ private int mIndex;
+ private BIP47PaymentCode mBIP47PaymentCode;
+ private String mXPub;
+
+ /**
+ * Constructor expecting a coin_type' derivation path key and the identity number.
+ */
+ public BIP47Account(NetworkParameters parameters, DeterministicKey deterministicKey, int index) {
+ mNetworkParameters = parameters;
+ mIndex = index;
+ mKey = HDKeyDerivation.deriveChildKey(deterministicKey, mIndex | ChildNumber.HARDENED_BIT);
+ mBIP47PaymentCode = new BIP47PaymentCode(mKey.getPubKey(), mKey.getChainCode());
+ mXPub = mKey.serializePubB58(parameters);
+ }
+
+ /**
+ * Constructor expecting a Base58Check encoded payment code.
+ * @throws AddressFormatException if the payment code is invalid
+ */
+ public BIP47Account(NetworkParameters parameters, String strPaymentCode) {
+ mNetworkParameters = parameters;
+ mIndex = 0;
+ mKey = createMasterPubKeyFromPaymentCode(strPaymentCode);
+ mBIP47PaymentCode = new BIP47PaymentCode(strPaymentCode);
+ mXPub = mKey.serializePubB58(parameters);
+ }
+
+ /** Return the Base58Check representation of the payment code*/
+ public String getStringPaymentCode() {
+ return mBIP47PaymentCode.toString();
+ }
+
+ public String getXPub() {
+ return mXPub;
+ }
+
+ /** Returns the P2PKH address associated with the 0th public key */
+ public Address getNotificationAddress() {
+ return HDKeyDerivation.deriveChildKey(mKey, ChildNumber.ZERO).toAddress(mNetworkParameters);
+ }
+
+ /** Returns the 0th derivation key */
+ public ECKey getNotificationKey() {
+ return HDKeyDerivation.deriveChildKey(mKey, ChildNumber.ZERO);
+ }
+
+ /** Return the payment code as is */
+ public BIP47PaymentCode getPaymentCode() {
+ return mBIP47PaymentCode;
+ }
+
+ /** Returns the nth key.
+ * @param idx must be between 0 and 2147483647
+ */
+ public ECKey keyAt(int idx) {
+ return HDKeyDerivation.deriveChildKey(mKey, new ChildNumber(idx, false));
+ }
+
+ public byte[] getPrivKey(int index) {
+ return HDKeyDerivation.deriveChildKey(mKey, index).getPrivKeyBytes();
+ }
+}
diff --git a/core/src/main/java/org/bitcoinj/core/bip47/BIP47Address.java b/core/src/main/java/org/bitcoinj/core/bip47/BIP47Address.java
new file mode 100644
index 00000000000..c572518aa99
--- /dev/null
+++ b/core/src/main/java/org/bitcoinj/core/bip47/BIP47Address.java
@@ -0,0 +1,50 @@
+/* Copyright (c) 2017 Stash
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.bitcoinj.core.bip47;
+
+/**
+ * Created by jimmy on 9/29/17.
+ */
+
+public class BIP47Address {
+
+ private String address;
+ private int index = 0;
+ private boolean seen = false;
+
+ public BIP47Address() {}
+
+ public BIP47Address(String address, int index) {
+ this.address = address;
+ this.index = index;
+ }
+
+ public BIP47Address(String address, int index, boolean seen) {
+ this(address, index);
+ this.seen = seen;
+ }
+
+ public String getAddress() {
+ return address;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public boolean isSeen() {
+ return seen;
+ }
+
+ public void setSeen(boolean seen) {
+ this.seen = seen;
+ }
+
+ @Override
+ public String toString() {
+ return address;
+ }
+}
diff --git a/core/src/main/java/org/bitcoinj/core/bip47/BIP47Channel.java b/core/src/main/java/org/bitcoinj/core/bip47/BIP47Channel.java
new file mode 100644
index 00000000000..50247b18c68
--- /dev/null
+++ b/core/src/main/java/org/bitcoinj/core/bip47/BIP47Channel.java
@@ -0,0 +1,140 @@
+/* Copyright (c) 2017 Stash
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.bitcoinj.core.bip47;
+
+import org.bitcoinj.core.Sha256Hash;
+
+import org.bitcoinj.core.Address;
+import org.bitcoinj.core.ECKey;
+import org.bitcoinj.kits.BIP47AppKit;
+import org.bitcoinj.wallet.bip47.NotSecp256k1Exception;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.spec.InvalidKeySpecException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.bitcoinj.utils.BIP47Util.getReceiveAddress;
+
+public class BIP47Channel {
+ private static final String TAG = "BIP47Channel";
+
+ private static final int STATUS_NOT_SENT = -1;
+ private static final int STATUS_SENT_CFM = 1;
+
+ private static final int LOOKAHEAD = 10;
+
+ private String paymentCode;
+ private String label = "";
+ private List incomingAddresses = new ArrayList();
+ private List outgoingAddresses = new ArrayList();
+ private int status = STATUS_NOT_SENT;
+ private int currentOutgoingIndex = 0;
+ private int currentIncomingIndex = -1;
+ private Sha256Hash ntxHash;
+
+ private static final Logger log = LoggerFactory.getLogger(BIP47Channel.class);
+ public BIP47Channel() {}
+
+ public BIP47Channel(String paymentCode) {
+ this.paymentCode = paymentCode;
+ }
+
+ public BIP47Channel(String paymentCode, String label) {
+ this(paymentCode);
+ this.label = label;
+ }
+
+ public String getPaymentCode() {
+ return paymentCode;
+ }
+
+ public void setPaymentCode(String pc) {
+ paymentCode = pc;
+ }
+
+ public List getIncomingAddresses() {
+ return incomingAddresses;
+ }
+
+ public int getCurrentIncomingIndex() {
+ return currentIncomingIndex;
+ }
+
+ public void generateKeys(BIP47AppKit wallet) throws NotSecp256k1Exception, NoSuchProviderException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException {
+ for (int i = 0; i < LOOKAHEAD; i++) {
+ ECKey key = getReceiveAddress(wallet, paymentCode, i).getReceiveECKey();
+ Address address = wallet.getAddressOfKey(key);
+
+ log.debug("New address generated");
+ log.debug(address.toString());
+ wallet.importKey(key);
+ incomingAddresses.add(i, new BIP47Address(address.toString(), i));
+ }
+
+ currentIncomingIndex = LOOKAHEAD - 1;
+ }
+
+ public BIP47Address getIncomingAddress(String address) {
+ for (BIP47Address bip47Address: incomingAddresses) {
+ if (bip47Address.getAddress().equals(address)) {
+ return bip47Address;
+ }
+ }
+ return null;
+ }
+
+ public void addNewIncomingAddress(String newAddress, int nextIndex) {
+ incomingAddresses.add(nextIndex, new BIP47Address(newAddress, nextIndex));
+ currentIncomingIndex = nextIndex;
+ }
+
+ public String getLabel() {
+ return label;
+ }
+
+ public void setLabel(String l) {
+ label = l;
+ }
+
+ public List getOutgoingAddresses() {
+ return outgoingAddresses;
+ }
+
+ public boolean isNotificationTransactionSent() {
+ return status == STATUS_SENT_CFM;
+ }
+
+ public void setStatusSent() {
+ status = STATUS_SENT_CFM;
+ }
+
+ public int getCurrentOutgoingIndex() {
+ return currentOutgoingIndex;
+ }
+
+ public void incrementOutgoingIndex() {
+ currentOutgoingIndex++;
+ }
+
+ public void addAddressToOutgoingAddresses(String address) {
+ outgoingAddresses.add(address);
+ }
+
+ public void setStatusNotSent() {
+ status = STATUS_NOT_SENT;
+ }
+
+ public Sha256Hash getNtxHash() { return ntxHash; }
+
+ public void setNtxHash(Sha256Hash ntxHash) {
+ this.ntxHash = ntxHash;
+ }
+}
diff --git a/core/src/main/java/org/bitcoinj/core/bip47/BIP47PaymentAddress.java b/core/src/main/java/org/bitcoinj/core/bip47/BIP47PaymentAddress.java
new file mode 100644
index 00000000000..eb3fbcb8007
--- /dev/null
+++ b/core/src/main/java/org/bitcoinj/core/bip47/BIP47PaymentAddress.java
@@ -0,0 +1,144 @@
+/* Copyright (c) 2017 Stash
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.bitcoinj.core.bip47;
+
+import java.math.BigInteger;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.spec.InvalidKeySpecException;
+import org.bitcoinj.core.AddressFormatException;
+import org.bitcoinj.core.ECKey;
+import org.bitcoinj.core.NetworkParameters;
+import org.bitcoinj.crypto.BIP47SecretPoint;
+import org.bitcoinj.wallet.bip47.NotSecp256k1Exception;
+import org.spongycastle.asn1.x9.X9ECParameters;
+import org.spongycastle.crypto.ec.CustomNamedCurves;
+import org.spongycastle.crypto.params.ECDomainParameters;
+import org.spongycastle.math.ec.ECPoint;
+
+/**
+ * p>A {@link BIP47PaymentAddress} is derived for account deposits in a bip47 channel. It is used by a recipient's bip47 wallet to derive and watch deposits. It
+ * is also used by a sender's bip47 wallet to calculate the next addresses to send a deposit to.
+ *
+ * The BIP47 BIP47PaymentAddress is at the derivation path m / 47' / coin_type' / account_id' / idx' .
.
+ *
+ * Properties:
+ *
+ * - The account_id is irrelevant in this class, it's implied in privKey.
+ * - The owner of BIP47PaymentCode is not the same owner of privKey.
+ *
+ */
+public class BIP47PaymentAddress {
+ // if we are receiving, this is the sender's payment code
+ // if we are sending, this is the receiver's payment code
+ private org.bitcoinj.core.bip47.BIP47PaymentCode BIP47PaymentCode = null;
+ // the index to use in the derivation path
+ private int index = 0;
+ // the corresponding hardedened key bytes at the derivation path
+ private byte[] privKey = null;
+ // the network used for formatting
+ private NetworkParameters networkParameters;
+ // give the values for "a", "b", "G" in the ECDSA curve used in Bitcoin (https://en.bitcoin.it/wiki/Secp256k1)
+ private static final X9ECParameters CURVE_PARAMS = CustomNamedCurves.getByName("secp256k1");
+ // create the curve
+ private static final ECDomainParameters CURVE;
+ static {
+ CURVE = new ECDomainParameters(CURVE_PARAMS.getCurve(), CURVE_PARAMS.getG(), CURVE_PARAMS.getN(), CURVE_PARAMS.getH());
+ }
+
+ /** Creates a BIP47PaymentAddress object that the sender will use to pay, using the hardened key at idx */
+ public BIP47PaymentAddress(NetworkParameters networkParameters, BIP47PaymentCode BIP47PaymentCode, int index, byte[] privKey) throws AddressFormatException {
+ this.BIP47PaymentCode = BIP47PaymentCode;
+ this.index = index;
+ this.privKey = privKey;
+ this.networkParameters = networkParameters;
+ }
+
+ /** Creates a HD key to send a deposit */
+ public ECKey getSendECKey() throws AddressFormatException, InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, IllegalStateException, InvalidKeySpecException, NotSecp256k1Exception {
+ return this.getSendECKey(this.secretPoint());
+ }
+
+ /** Derives a deposit address to watch to receive payments from BIP47PaymentCode's owner*/
+ public ECKey getReceiveECKey() throws AddressFormatException, InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, IllegalStateException, InvalidKeySpecException, NotSecp256k1Exception {
+ return this.getReceiveECKey(this.secretPoint());
+ }
+
+ /* Use the generator "G" by */
+ //public ECPoint get_sG() throws AddressFormatException, InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, IllegalStateException, InvalidKeySpecException, NotSecp256k1Exception {
+ // return CURVE_PARAMS.getG().multiply(this.getSecretPoint());
+ //}
+
+ /* Accesor for the secret point between sender and receiver */
+ public BIP47SecretPoint getSharedSecret() throws AddressFormatException, InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, IllegalStateException, InvalidKeySpecException {
+ return this.sharedSecret();
+ }
+
+ //public BigInteger getSecretPoint() throws AddressFormatException, InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, IllegalStateException, InvalidKeySpecException, NotSecp256k1Exception {
+ // return this.secretPoint();
+ //}
+
+ public ECPoint getECPoint() throws AddressFormatException, InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, IllegalStateException, InvalidKeySpecException {
+ ECKey ecKey = ECKey.fromPublicOnly(this.BIP47PaymentCode.derivePubKeyAt(this.networkParameters, this.index));
+ return ecKey.getPubKeyPoint();
+ }
+
+ /** Returns the scalar shared secret */
+ public byte[] hashSharedSecret() throws AddressFormatException, InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, IllegalStateException, InvalidKeySpecException {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ byte[] hash = digest.digest(this.getSharedSecret().ECDHSecretAsBytes());
+ return hash;
+ }
+
+ /* Multply a times the generator G */
+ private ECPoint get_sG(BigInteger s) {
+ return CURVE_PARAMS.getG().multiply(s);
+ }
+
+ /* Derives the key for the payment address where the BIP47PaymentCode's owner will be watching for deposits */
+ private ECKey getSendECKey(BigInteger s) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
+ ECPoint ecPoint = this.getECPoint();
+ ECPoint sG = this.get_sG(s);
+ ECKey ecKey = ECKey.fromPublicOnly(ecPoint.add(sG).getEncoded(true));
+ return ecKey;
+ }
+
+ /* Calculates the ephemeral hardened key used to generate the P2PKH address where a deposit will be received */
+ private ECKey getReceiveECKey(BigInteger s) {
+ BigInteger privKeyValue = ECKey.fromPrivate(this.privKey).getPrivKey();
+ ECKey ecKey = ECKey.fromPrivate(this.addSecp256k1(privKeyValue, s));
+ return ecKey;
+ }
+
+ /* Adds two keys together */
+ private BigInteger addSecp256k1(BigInteger b1, BigInteger b2) {
+ BigInteger ret = b1.add(b2);
+ return ret.bitLength() > CURVE.getN().bitLength()?ret.mod(CURVE.getN()):ret;
+ }
+
+ /* Return the ECDH shared secret between us and the owner of BIP47PaymentCode */
+ private BIP47SecretPoint sharedSecret() throws AddressFormatException, InvalidKeySpecException, InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, NoSuchProviderException {
+ byte[] pubKey = this.BIP47PaymentCode.derivePubKeyAt(this.networkParameters, this.index);
+ return new BIP47SecretPoint(this.privKey, pubKey);
+ }
+
+ /* Returns true if the given point "b" is in the curve */
+ private boolean isSecp256k1(BigInteger b) {
+ return b.compareTo(BigInteger.ONE) > 0 && b.bitLength() <= CURVE.getN().bitLength();
+ }
+
+ /** Returns a SHA256 mask of the secret point */
+ private BigInteger secretPoint() throws AddressFormatException, InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException, NotSecp256k1Exception {
+ BigInteger s = new BigInteger(1, this.hashSharedSecret());
+ if(!this.isSecp256k1(s)) {
+ throw new NotSecp256k1Exception("secret point not on Secp256k1 curve");
+ } else {
+ return s;
+ }
+ }
+}
diff --git a/core/src/main/java/org/bitcoinj/core/bip47/BIP47PaymentCode.java b/core/src/main/java/org/bitcoinj/core/bip47/BIP47PaymentCode.java
new file mode 100644
index 00000000000..b82f671da5f
--- /dev/null
+++ b/core/src/main/java/org/bitcoinj/core/bip47/BIP47PaymentCode.java
@@ -0,0 +1,257 @@
+/* Copyright (c) 2017 Stash
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.bitcoinj.core.bip47;
+
+import java.math.BigInteger;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.tuple.Pair;
+import org.bitcoinj.core.*;
+import org.bitcoinj.crypto.ChildNumber;
+import org.bitcoinj.crypto.DeterministicKey;
+import org.bitcoinj.crypto.HDKeyDerivation;
+
+public class BIP47PaymentCode {
+ private static final int PUBLIC_KEY_Y_OFFSET = 2;
+ private static final int PUBLIC_KEY_X_OFFSET = 3;
+ private static final int CHAIN_OFFSET = 35;
+ private static final int PUBLIC_KEY_X_LEN = 32;
+ private static final int PUBLIC_KEY_Y_LEN = 1;
+ private static final int CHAIN_LEN = 32;
+ private static final int PAYLOAD_LEN = 80;
+ private String strPaymentCode = null;
+ private byte[] pubkey = null;
+ private byte[] chain = null;
+
+ public BIP47PaymentCode() {
+ this.strPaymentCode = null;
+ this.pubkey = null;
+ this.chain = null;
+ }
+
+ public BIP47PaymentCode(String payment_code) throws AddressFormatException {
+ this.strPaymentCode = payment_code;
+ this.pubkey = this.parse().getLeft();
+ this.chain = this.parse().getRight();
+ }
+
+ public BIP47PaymentCode(byte[] payload) {
+ if(payload.length == 80) {
+ this.pubkey = new byte[33];
+ this.chain = new byte[32];
+ System.arraycopy(payload, 2, this.pubkey, 0, 33);
+ System.arraycopy(payload, 35, this.chain, 0, 32);
+ this.strPaymentCode = this.makeV1();
+ }
+ }
+
+ public BIP47PaymentCode(byte[] pubkey, byte[] chain) {
+ this.pubkey = pubkey;
+ this.chain = chain;
+ this.strPaymentCode = this.makeV1();
+ }
+
+ public byte[] getPayload() throws AddressFormatException {
+ byte[] pcBytes = Base58.decodeChecked(this.strPaymentCode);
+ byte[] payload = new byte[80];
+ System.arraycopy(pcBytes, 1, payload, 0, payload.length);
+ return payload;
+ }
+
+ public int getType() throws AddressFormatException {
+ byte[] payload = this.getPayload();
+ ByteBuffer bb = ByteBuffer.wrap(payload);
+ byte type = bb.get();
+ return type;
+ }
+
+ public byte[] decode() throws AddressFormatException {
+ return Base58.decode(this.strPaymentCode);
+ }
+
+ public byte[] decodeChecked() throws AddressFormatException {
+ return Base58.decodeChecked(this.strPaymentCode);
+ }
+
+ public byte[] getPubKey() {
+ return this.pubkey;
+ }
+
+ public byte[] getChain() {
+ return this.chain;
+ }
+
+ public String toString() {
+ return this.strPaymentCode;
+ }
+
+ public static byte[] getMask(byte[] sPoint, byte[] oPoint) {
+ Mac sha512_HMAC = null;
+ byte[] mac_data = null;
+
+ try {
+ sha512_HMAC = Mac.getInstance("HmacSHA512");
+ SecretKeySpec secretkey = new SecretKeySpec(oPoint, "HmacSHA512");
+ sha512_HMAC.init(secretkey);
+ mac_data = sha512_HMAC.doFinal(sPoint);
+ } catch (InvalidKeyException var5) {
+ } catch (NoSuchAlgorithmException var6) {
+ }
+
+ return mac_data;
+ }
+
+ public static byte[] blind(byte[] payload, byte[] mask) throws AddressFormatException {
+ byte[] ret = new byte[80];
+ byte[] pubkey = new byte[32];
+ byte[] chain = new byte[32];
+ byte[] buf0 = new byte[32];
+ byte[] buf1 = new byte[32];
+ System.arraycopy(payload, 0, ret, 0, 80);
+ System.arraycopy(payload, 3, pubkey, 0, 32);
+ System.arraycopy(payload, 35, chain, 0, 32);
+ System.arraycopy(mask, 0, buf0, 0, 32);
+ System.arraycopy(mask, 32, buf1, 0, 32);
+ System.arraycopy(xor(pubkey, buf0), 0, ret, 3, 32);
+ System.arraycopy(xor(chain, buf1), 0, ret, 35, 32);
+ return ret;
+ }
+
+ private Pair parse() throws AddressFormatException {
+ byte[] pcBytes = Base58.decodeChecked(this.strPaymentCode);
+ ByteBuffer bb = ByteBuffer.wrap(pcBytes);
+ if(bb.get() != 71) {
+ throw new AddressFormatException("invalid payment code version");
+ } else {
+ byte[] chain = new byte[32];
+ byte[] pub = new byte[33];
+ bb.get();
+ bb.get();
+ bb.get(pub);
+ if(pub[0] != 2 && pub[0] != 3) {
+ throw new AddressFormatException("invalid public key");
+ } else {
+ bb.get(chain);
+ return Pair.of(pub, chain);
+ }
+ }
+ }
+
+ private String makeV1() {
+ return this.make(1);
+ }
+
+ private String makeV2() {
+ return this.make(2);
+ }
+
+ private String make(int type) {
+ String ret = null;
+ byte[] payload = new byte[80];
+ byte[] payment_code = new byte[81];
+
+ for(int checksum = 0; checksum < payload.length; ++checksum) {
+ payload[checksum] = 0;
+ }
+
+ payload[0] = (byte)type;
+ payload[1] = 0;
+ System.arraycopy(this.pubkey, 0, payload, 2, this.pubkey.length);
+ System.arraycopy(this.chain, 0, payload, 35, this.chain.length);
+ payment_code[0] = 71;
+ System.arraycopy(payload, 0, payment_code, 1, payload.length);
+ byte[] var7 = Arrays.copyOfRange(Sha256Hash.hashTwice(payment_code), 0, 4);
+ byte[] payment_code_checksum = new byte[payment_code.length + var7.length];
+ System.arraycopy(payment_code, 0, payment_code_checksum, 0, payment_code.length);
+ System.arraycopy(var7, 0, payment_code_checksum, payment_code_checksum.length - 4, var7.length);
+ ret = Base58.encode(payment_code_checksum);
+ return ret;
+ }
+
+ private DeterministicKey createMasterPubKeyFromBytes(byte[] pub, byte[] chain) throws AddressFormatException {
+ return HDKeyDerivation.createMasterPubKeyFromBytes(pub, chain);
+ }
+
+ private static byte[] xor(byte[] a, byte[] b) {
+ if(a.length != b.length) {
+ return null;
+ } else {
+ byte[] ret = new byte[a.length];
+
+ for(int i = 0; i < a.length; ++i) {
+ ret[i] = (byte)(b[i] ^ a[i]);
+ }
+
+ return ret;
+ }
+ }
+
+ public boolean isValid() {
+ try {
+ byte[] afe = Base58.decodeChecked(this.strPaymentCode);
+ ByteBuffer byteBuffer = ByteBuffer.wrap(afe);
+ if(byteBuffer.get() != 71) {
+ throw new AddressFormatException("invalid version: " + this.strPaymentCode);
+ } else {
+ byte[] chain = new byte[32];
+ byte[] pub = new byte[33];
+ byteBuffer.get();
+ byteBuffer.get();
+ byteBuffer.get(pub);
+ byteBuffer.get(chain);
+ ByteBuffer pubBytes = ByteBuffer.wrap(pub);
+ byte firstByte = pubBytes.get();
+ return firstByte == 2 || firstByte == 3;
+ }
+ } catch (BufferUnderflowException var7) {
+ return false;
+ } catch (AddressFormatException var8) {
+ return false;
+ }
+ }
+
+ public static DeterministicKey createMasterPubKeyFromPaymentCode(String payment_code_str) throws AddressFormatException {
+ byte[] paymentCodeBytes = Base58.decodeChecked(payment_code_str);
+ ByteBuffer bb = ByteBuffer.wrap(paymentCodeBytes);
+ if(bb.get() != 71) {
+ throw new AddressFormatException("invalid payment code version");
+ } else {
+ byte[] chain = new byte[32];
+ byte[] pub = new byte[33];
+ bb.get();
+ bb.get();
+ bb.get(pub);
+ bb.get(chain);
+ return HDKeyDerivation.createMasterPubKeyFromBytes(pub, chain);
+ }
+ }
+
+ /** Returns the pubkey on the ith derivation path */
+ public byte[] derivePubKeyAt(NetworkParameters networkParameters, int i) throws AddressFormatException {
+ DeterministicKey key = createMasterPubKeyFromPaymentCode(this.strPaymentCode);
+ DeterministicKey dk = HDKeyDerivation.deriveChildKey(key, new ChildNumber(i, false));
+
+ ECKey ecKey;
+ if(dk.hasPrivKey()) {
+ byte[] now = ArrayUtils.addAll(new byte[1], dk.getPrivKeyBytes());
+ ecKey = ECKey.fromPrivate(new BigInteger(now), true);
+ } else {
+ ecKey = ECKey.fromPublicOnly(dk.getPubKey());
+ }
+
+ long now1 = Utils.now().getTime() / 1000L;
+ ecKey.setCreationTimeSeconds(now1);
+
+ return ecKey.getPubKey();
+ }
+}
diff --git a/core/src/main/java/org/bitcoinj/crypto/BIP47SecretPoint.java b/core/src/main/java/org/bitcoinj/crypto/BIP47SecretPoint.java
new file mode 100644
index 00000000000..9f7be0aac2f
--- /dev/null
+++ b/core/src/main/java/org/bitcoinj/crypto/BIP47SecretPoint.java
@@ -0,0 +1,92 @@
+/* Copyright (c) 2017 Stash
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.bitcoinj.crypto;
+
+import java.math.BigInteger;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.Security;
+import java.security.spec.InvalidKeySpecException;
+import javax.crypto.KeyAgreement;
+import javax.crypto.SecretKey;
+
+import org.spongycastle.jce.ECNamedCurveTable;
+import org.spongycastle.jce.provider.BouncyCastleProvider;
+import org.spongycastle.jce.spec.ECParameterSpec;
+import org.spongycastle.jce.spec.ECPrivateKeySpec;
+import org.spongycastle.jce.spec.ECPublicKeySpec;
+import org.spongycastle.util.encoders.Hex;
+
+public class BIP47SecretPoint {
+ private static final ECParameterSpec params = ECNamedCurveTable.getParameterSpec("secp256k1");
+
+ static {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+
+ private PrivateKey privKey = null;
+ private PublicKey pubKey = null;
+ private KeyFactory kf = null;
+
+ public BIP47SecretPoint() {
+ }
+
+ public BIP47SecretPoint(byte[] dataPrv, byte[] dataPub) throws InvalidKeySpecException, InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, NoSuchProviderException {
+ this.kf = KeyFactory.getInstance("ECDH", "SC");
+ this.privKey = this.loadPrivateKey(dataPrv);
+ this.pubKey = this.loadPublicKey(dataPub);
+ }
+
+ public PrivateKey getPrivKey() {
+ return this.privKey;
+ }
+
+ public void setPrivKey(PrivateKey privKey) {
+ this.privKey = privKey;
+ }
+
+ public PublicKey getPubKey() {
+ return this.pubKey;
+ }
+
+ public void setPubKey(PublicKey pubKey) {
+ this.pubKey = pubKey;
+ }
+
+ public byte[] ECDHSecretAsBytes() throws InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, NoSuchProviderException {
+ return this.ECDHSecret().getEncoded();
+ }
+
+ public boolean isShared(BIP47SecretPoint secret) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException {
+ return this.equals(secret);
+ }
+
+ private SecretKey ECDHSecret() throws InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, NoSuchProviderException {
+ KeyAgreement ka = KeyAgreement.getInstance("ECDH", "SC");
+ ka.init(this.privKey);
+ ka.doPhase(this.pubKey, true);
+ SecretKey secret = ka.generateSecret("AES");
+ return secret;
+ }
+
+ private boolean equals(BIP47SecretPoint secret) throws InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, NoSuchProviderException {
+ return Hex.toHexString(this.ECDHSecretAsBytes()).equals(Hex.toHexString(secret.ECDHSecretAsBytes()));
+ }
+
+ private PublicKey loadPublicKey(byte[] data) throws InvalidKeySpecException {
+ ECPublicKeySpec pubKey = new ECPublicKeySpec(params.getCurve().decodePoint(data), params);
+ return this.kf.generatePublic(pubKey);
+ }
+
+ private PrivateKey loadPrivateKey(byte[] data) throws InvalidKeySpecException {
+ ECPrivateKeySpec prvkey = new ECPrivateKeySpec(new BigInteger(1, data), params);
+ return this.kf.generatePrivate(prvkey);
+ }
+}
diff --git a/core/src/main/java/org/bitcoinj/kits/BIP47AppKit.java b/core/src/main/java/org/bitcoinj/kits/BIP47AppKit.java
new file mode 100644
index 00000000000..55f4e49061f
--- /dev/null
+++ b/core/src/main/java/org/bitcoinj/kits/BIP47AppKit.java
@@ -0,0 +1,990 @@
+/* Copyright (c) 2017 Stash
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.bitcoinj.kits;
+
+
+import com.google.common.net.InetAddresses;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.reflect.TypeToken;
+import org.bitcoinj.core.*;
+import org.bitcoinj.core.bip47.*;
+import org.bitcoinj.crypto.BIP47SecretPoint;
+import org.bitcoinj.wallet.*;
+import org.bitcoinj.utils.BIP47Util;
+import org.bitcoinj.wallet.bip47.NotSecp256k1Exception;
+import org.bitcoinj.wallet.bip47.listeners.BlockchainDownloadProgressTracker;
+import org.bitcoinj.wallet.bip47.listeners.TransactionEventListener;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.bitcoinj.crypto.ChildNumber;
+import org.bitcoinj.crypto.DeterministicKey;
+import org.bitcoinj.crypto.HDKeyDerivation;
+import org.bitcoinj.net.discovery.DnsDiscovery;
+import org.bitcoinj.script.Script;
+import org.bitcoinj.script.ScriptBuilder;
+import org.bitcoinj.store.BlockStore;
+import org.bitcoinj.store.BlockStoreException;
+import org.bitcoinj.store.SPVBlockStore;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.nio.charset.Charset;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.spec.InvalidKeySpecException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+import static org.bitcoinj.core.Utils.join;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.bitcoinj.utils.BIP47Util.getReceiveAddress;
+import static org.bitcoinj.utils.BIP47Util.getSendAddress;
+
+/**
+ * Created by jimmy on 9/28/17.
+ */
+
+/**
+ * Runs a spv wallet and supports BIP 47 payments for coins. You will
+ * need to instantiate one wallet per supported coin.
+ *
+ * It produces two files in a designated directory. The directory name is the coin name. and is created in workingDirectory:
+ *
+ * The .spvchain (blockstore): maintains a maximum # of headers mapped to memory (5000)
+ * The .wallet: stores the wallet with txs, can be encrypted, storing keys
+ *
+ *
+ * By using this kit, your wallet will import keys for bip 47 payment addresses, when a BIP 47
+ * notification transaction is received.
+ *
+ */
+public class BIP47AppKit {
+ private static final String TAG = "BIP47AppKit";
+
+ // the coin name that this wallet supports. Can be: BTC, tBTC, BCH, tBCH
+ private String coinName;
+ // fee parameters and network metadata
+ private NetworkParameters params;
+ // the blokstore is used by a blockchain as a memory data structure
+ private volatile BlockChain vChain;
+ private volatile BlockStore vStore;
+ private volatile org.bitcoinj.wallet.Wallet vWallet;
+ // sync with the blockchain by using a peergroup
+ private volatile PeerGroup vPeerGroup;
+
+ // the directory will have the spvchain and the wallet files
+ private final File directory;
+ private volatile File vWalletFile;
+ // Wether this wallet is restored from a BIP39 seed and will need to replay the complete blockchain
+ // Will be null if it's not a restored wallet.
+ private DeterministicSeed restoreFromSeed;
+
+ // Support for BIP47-type accounts. Only one account is currently handled in this wallet.
+ private List mAccounts = new ArrayList(1);
+
+ // The progress tracker will callback the listener with a porcetage of the blockchain that it has downloaded, while downloading..
+ private BlockchainDownloadProgressTracker mBlockchainDownloadProgressTracker;
+
+ // This wallet allows one listener to be invoked when there are coins received and
+ private TransactionEventListener mCoinsReceivedEventListener = null;
+ // one listener when the transaction confidence changes
+ private TransactionEventListener mTransactionConfidenceListener = null;
+
+ private boolean mBlockchainDownloadStarted = false;
+
+ // The payment channels indexed by payment codes.
+ // A payment channel is created and saved if:
+ // - someone sends a notification transaction to this wallet's notifiction address
+ // - this wallet creates a notification transaction to a payment channel.
+ //
+ // It doesn't check if the notification transactions are mined before adding a payment code.
+ // If you want to know a transaction's confidence, see #{@link Transaction.getConfidence()}
+ private ConcurrentHashMap bip47MetaData = new ConcurrentHashMap();
+ private static final Logger log = LoggerFactory.getLogger(BIP47AppKit.class);
+
+ /**
+ * Creates a new wallet for a coinName, the .spvchain and .wallet files in workingDir/coinName.
+ * Any keys will be derived from deterministicSeed.
+ */
+ public BIP47AppKit(String coinName, NetworkParameters params, File walletDirectory, @Nullable DeterministicSeed deterministicSeed) throws Exception {
+ this.coinName = checkNotNull(coinName);
+ this.params = checkNotNull(params);
+ this.directory = new File(walletDirectory, coinName);
+ this.restoreFromSeed = deterministicSeed;
+
+ // ensure directory exists
+ if (!directory.exists()) {
+ if (!directory.mkdirs()) {
+ throw new IOException("Could not create directory " + directory.getAbsolutePath());
+ }
+ }
+
+ File chainFile = new File(directory, coinName + ".spvchain");
+ boolean chainFileExists = chainFile.exists();
+ // point to the file with the (possibly existent) BIP47AppKit
+ vWalletFile = new File(directory, coinName + ".wallet");
+ log.debug("BIP47AppKit: "+coinName);
+
+ // replay the wallet if deterministicSeed is defined or if it's chain file is deleted (as a trigger to replay it)
+ boolean shouldReplayWallet = (vWalletFile.exists() && !chainFileExists) || restoreFromSeed != null;
+
+ Context.propagate(new Context(params));
+ vWallet = createOrLoadWallet(shouldReplayWallet);
+ setAccount();
+
+ Address notificationAddress = mAccounts.get(0).getNotificationAddress();
+ log.debug("BIP47AppKit notification address: "+notificationAddress.toString());
+
+ if (!vWallet.isAddressWatched(notificationAddress)) {
+ vWallet.addWatchedAddress(notificationAddress);
+ }
+
+ vWallet.allowSpendingUnconfirmedTransactions();
+
+ log.debug(vWallet.toString());
+
+ // Initiate Bitcoin network objects (block store, blockchain and peer group)
+
+ // open the blockstore file
+ vStore = new SPVBlockStore(params, chainFile);
+
+ // create a fresh blockstore file before restoring a wallet
+ if (restoreFromSeed != null && chainFileExists) {
+ log.info( "Deleting the chain file in preparation from restore.");
+ vStore.close();
+ if (!chainFile.delete())
+ log.warn("start: ", new IOException("Failed to delete chain file in preparation for restore."));
+ vStore = new SPVBlockStore(params, chainFile);
+ }
+
+ try {
+ // create the blockchain object using the file-backed blockstore
+ vChain = new BlockChain(params, vStore);
+ } catch (BlockStoreException e){
+
+ // - we can create a new blockstore in case it is corrupted, the wallet should have a last height
+ if (chainFile.exists()) {
+ log.debug("deleteSpvFile: exits");
+ chainFile.delete();
+ }
+
+ vStore = new SPVBlockStore(params, chainFile);
+ vChain = new BlockChain(params, vStore);
+ }
+
+ // add the wallet so that syncing and rolling the chain can affect this wallet
+ vChain.addWallet(vWallet);
+ derivePeerGroup();
+ addTransactionsListener();
+ }
+
+ // create peergroup for the blockchain
+ private void derivePeerGroup(){
+ Context.propagate(new Context(params));
+ if (vPeerGroup == null)
+ vPeerGroup = new PeerGroup(params, vChain);
+
+ // add Stash-Crypto dedicated nodes for BCH and tBCH
+ if (coinName.equals("BCH")) {
+ vPeerGroup.setMaxConnections(vPeerGroup.DEFAULT_CONNECTIONS);
+ vPeerGroup.addAddress(new PeerAddress(InetAddresses.forString("158.69.119.35"), 8333));
+ vPeerGroup.addAddress(new PeerAddress(InetAddresses.forString("144.217.73.86"), 8333));
+ // bitcoin abc from shodan.io
+ //vPeerGroup.addAddress(new PeerAddress(InetAddresses.forString("106.14.105.56"), 8333));
+ //vPeerGroup.addAddress(new PeerAddress(InetAddresses.forString("52.211.14.233"), 8333));
+ //vPeerGroup.addAddress(new PeerAddress(InetAddresses.forString("50.39.245.26"), 8333));
+ //vPeerGroup.addAddress(new PeerAddress(InetAddresses.forString("52.57.14.67"), 8333));
+ // bucash
+ //vPeerGroup.addAddress(new PeerAddress(InetAddresses.forString("5.44.97.110"), 8333));
+ //vPeerGroup.addAddress(new PeerAddress(InetAddresses.forString("185.69.52.180"), 8333));
+ } else if (coinName.equals("tBCH")) {
+ vPeerGroup.setMaxConnections(vPeerGroup.DEFAULT_CONNECTIONS);
+ // stash crypto
+ vPeerGroup.addAddress(new PeerAddress(InetAddresses.forString("158.69.119.35"), 18333));
+ vPeerGroup.addAddress(new PeerAddress(InetAddresses.forString("144.217.73.86"), 18333));
+ // bitcoin abc from shodan.io
+ //vPeerGroup.addAddress(new PeerAddress(InetAddresses.forString("61.100.182.189"), 18333));
+ //vPeerGroup.addAddress(new PeerAddress(InetAddresses.forString("47.74.186.127"), 18333));
+ }
+
+ // connect to peer running in localhost (127.0.0.1)
+ vPeerGroup.setUseLocalhostPeerWhenPossible(true);
+ // connect to peers in the blockchain network
+ vPeerGroup.addPeerDiscovery(new DnsDiscovery(params));
+
+ // add the wallet to the peers so that every peer listener can find this wallet e.g. to invoke listeners
+ vPeerGroup.addWallet(vWallet);
+ }
+
+ // BIP47-specific listener
+ // When a new *notification* transaction is received:
+ // - new keys are generated and imported for incoming payments in the bip47 account/contact payment channel
+ // - the chain is rolled back 2 blocks so that payment transactions are not missed if in the same block as the notification transaction.
+ //
+ // When a new *payment* transaction is received:
+ // - a new key is generated and imported to the wallet
+ private void addTransactionsListener(){
+ this.addOnReceiveTransactionListener(new TransactionEventListener() {
+ @Override
+ public void onTransactionReceived(BIP47AppKit bip47AppKit, Transaction transaction) {
+
+ if (isNotificationTransaction(transaction)) {
+ log.debug("Valid notification transaction received");
+ BIP47PaymentCode BIP47PaymentCode = getPaymentCodeInNotificationTransaction(transaction);
+ if (BIP47PaymentCode == null) {
+ log.warn("Error decoding payment code in tx {}", transaction);
+ } else {
+ log.debug("Payment Code: " + BIP47PaymentCode);
+ boolean needsSaving = savePaymentCode(BIP47PaymentCode);
+ if (needsSaving) {
+ saveBip47MetaData();
+ }
+ }
+ } else if (isToBIP47Address(transaction)) {
+ log.debug("New BIP47 payment received to address: "+getAddressOfReceived(transaction));
+ boolean needsSaving = generateNewBip47IncomingAddress(getAddressOfReceived(transaction).toString());
+ if (needsSaving) {
+ saveBip47MetaData();
+ }
+ String paymentCode = getPaymentCodeForAddress(getAddressOfReceived(transaction).toString());
+ log.debug("Received tx for Payment Code: " + paymentCode);
+ } else {
+ Coin valueSentToMe = getValueSentToMe(transaction);
+ log.debug("Received tx for "+valueSentToMe.toFriendlyString() + ":" + transaction);
+ }
+ }
+
+ @Override
+ public void onTransactionConfidenceEvent(BIP47AppKit bip47AppKit, Transaction transaction) {
+ return;
+ }
+ });
+ }
+
+ // if coinName/coinName.wallet exists, we load it as a core BIP47AppKit and then manually set each of the bip47 properties
+ private org.bitcoinj.wallet.Wallet createOrLoadWallet(boolean shouldReplayWallet) throws Exception {
+ org.bitcoinj.wallet.Wallet wallet;
+
+ if (vWalletFile.exists()) {
+ wallet = loadWallet(shouldReplayWallet);
+ } else {
+ // create an empty wallet
+ wallet = createWallet();
+ // with a seed
+ wallet.freshReceiveKey();
+ // reload the wallet
+ wallet.saveToFile(vWalletFile);
+ wallet = loadWallet(false);
+ }
+
+ // every 5 seconds let's persist the transactions, keys, last block height, watched scripts, etc.
+ // does not persist channels recurrently, instead payment channels are currently saved in a separete file (.bip47 extension).
+ wallet.autosaveToFile(vWalletFile, 5, TimeUnit.SECONDS, null);
+
+ return wallet;
+ }
+
+ // Load an offline wallet from a file and return a @{link org.bitcoinj.wallet.BIP47AppKit}.
+ // If shouldReplayWallet is false, the wallet last block is reset to -1
+ private org.bitcoinj.wallet.Wallet loadWallet(boolean shouldReplayWallet) throws Exception {
+ FileInputStream walletStream = new FileInputStream(vWalletFile);
+ Protos.Wallet proto = WalletProtobufSerializer.parseToProto(walletStream);
+ final WalletProtobufSerializer serializer = new WalletProtobufSerializer();
+ org.bitcoinj.wallet.Wallet wallet = serializer.readWallet(params, null, proto);
+ if (shouldReplayWallet)
+ wallet.reset();
+ return wallet;
+ }
+
+ private org.bitcoinj.wallet.Wallet createWallet() {
+ KeyChainGroup kcg;
+ if (restoreFromSeed != null)
+ kcg = new KeyChainGroup(params, restoreFromSeed);
+ else
+ kcg = new KeyChainGroup(params);
+ return new org.bitcoinj.wallet.Wallet(params, kcg); // default
+ }
+
+ public String getCoinName() {
+ return coinName;
+ }
+
+ /**
+ * Create the account M/47'/0'/0' from the seed as a Bip47Account.
+ *
+ * After deriving, this wallet's payment code is available in @{link Bip47Wallet.getPaymentCode()}
+ */
+ public void setAccount() {
+ byte[] hd_seed = this.restoreFromSeed != null ?
+ this.restoreFromSeed.getSeedBytes() :
+ vWallet.getKeyChainSeed().getSeedBytes();
+
+ DeterministicKey mKey = HDKeyDerivation.createMasterPrivateKey(hd_seed);
+ DeterministicKey purposeKey = HDKeyDerivation.deriveChildKey(mKey, 47 | ChildNumber.HARDENED_BIT);
+ DeterministicKey coinKey = HDKeyDerivation.deriveChildKey(purposeKey, ChildNumber.HARDENED_BIT);
+
+ BIP47Account account = new BIP47Account(params, coinKey, 0);
+
+ mAccounts.clear();
+ mAccounts.add(account);
+ }
+
+ /*
+ * Connect this wallet to the network. Watch notification and payment transactions.
+ */
+ public void startBlockchainDownload() {
+ if (!isStarted() && !mBlockchainDownloadStarted) {
+ log.debug("Starting blockchain download.");
+ vPeerGroup.start();
+ vPeerGroup.startBlockChainDownload(mBlockchainDownloadProgressTracker);
+ mBlockchainDownloadStarted = true;
+ } else {
+ log.warn("Attempted to start blockchain download but it is already started.");
+ }
+ }
+
+ public List getConnectedPeers() {
+ return vPeerGroup.getConnectedPeers();
+ }
+
+ /**
+ * Disconnects the wallet from the network
+ */
+ public void stop() {
+ if (!isStarted()) {
+ return;
+ }
+
+ log.debug("Stopping peergroup");
+ vPeerGroup.stop();
+ try {
+ log.debug("Saving wallet");
+ vWallet.saveToFile(vWalletFile);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ log.debug("stopWallet: closing store");
+ try {
+ if (vStore != null)
+ vStore.close();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ vStore = null;
+ vPeerGroup = null;
+ mBlockchainDownloadStarted = false;
+ derivePeerGroup();
+
+ mBlockchainDownloadStarted = false;
+
+ log.debug("stopWallet: Done");
+ }
+
+ public boolean isStarted() {
+ if (vPeerGroup == null)
+ return false;
+ return vPeerGroup.isRunning();
+ }
+
+ public void setBlockchainDownloadProgressTracker(BlockchainDownloadProgressTracker downloadProgressTracker) {
+ mBlockchainDownloadProgressTracker = downloadProgressTracker;
+ }
+
+ /**
+ * Reads the channels from .bip47 file. Return true if any payment code was loaded.
+ */
+ public boolean loadBip47MetaData() {
+ String jsonString = readBip47MetaDataFile();
+
+ if (StringUtils.isEmpty(jsonString)) {
+ return false;
+ }
+
+ log.debug("loadBip47MetaData: "+jsonString);
+
+ return importBip47MetaData(jsonString);
+ }
+
+ /**
+ * Reads the channels from .bip47 file. Return true if any payment code was loaded.
+ */
+ public String readBip47MetaDataFile() {
+ File file = new File(directory, coinName.concat(".bip47"));
+ String jsonString;
+ try {
+ jsonString = FileUtils.readFileToString(file, Charset.defaultCharset());
+ } catch (IOException e){
+ log.debug("Creating BIP47 wallet file at " + file.getAbsolutePath() + " ...");
+ saveBip47MetaData();
+ loadBip47MetaData();
+ return null;
+ }
+
+ return jsonString;
+ }
+
+ /**
+ * Load channels from json. Return true if any payment code was loaded.
+ */
+ public boolean importBip47MetaData(String jsonString) {
+ log.debug("loadBip47MetaData: "+jsonString);
+
+ Gson gson = new Gson();
+ Type collectionType = new TypeToken>(){}.getType();
+ try {
+ List BIP47ChannelList = gson.fromJson(jsonString, collectionType);
+ if (BIP47ChannelList != null) {
+ for (BIP47Channel BIP47Channel : BIP47ChannelList) {
+ bip47MetaData.put(BIP47Channel.getPaymentCode(), BIP47Channel);
+ }
+ }
+ } catch (JsonSyntaxException e) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Persists the .bip47 file with the channels.
+ */
+ public synchronized void saveBip47MetaData() {
+ try {
+ vWallet.saveToFile(vWalletFile);
+ } catch (IOException io){
+ log.error("Failed to save wallet file",io);
+ }
+
+ Gson gson = new GsonBuilder().setPrettyPrinting().create();
+ String json = gson.toJson(bip47MetaData.values());
+
+ log.debug("saveBip47MetaData: "+json);
+
+ File file = new File(directory, coinName.concat(".bip47"));
+
+ try {
+ FileUtils.writeStringToFile(file, json, Charset.defaultCharset(), false);
+ log.debug("saveBip47MetaData: saved");
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /** A listener is added to be invoked when the wallet sees an incoming transaction.
*/
+ public void addOnReceiveTransactionListener(TransactionEventListener transactionEventListener){
+ if (this.mCoinsReceivedEventListener != null)
+ vWallet.removeCoinsReceivedEventListener(mCoinsReceivedEventListener);
+
+ transactionEventListener.setWallet(this);
+ vWallet.addCoinsReceivedEventListener(transactionEventListener);
+
+ mCoinsReceivedEventListener = transactionEventListener;
+ }
+
+ /** A listener is added to be invoked when the wallet receives blocks and builds confidence on a transaction
*/
+ public void addTransactionConfidenceEventListener(TransactionEventListener transactionEventListener){
+ if (this.mTransactionConfidenceListener != null)
+ vWallet.removeTransactionConfidenceEventListener(mTransactionConfidenceListener);
+
+ transactionEventListener.setWallet(this);
+ vWallet.addTransactionConfidenceEventListener(transactionEventListener);
+
+ mTransactionConfidenceListener = transactionEventListener;
+ }
+
+ public TransactionEventListener getCoinsReceivedEventListener(){
+ return this.mCoinsReceivedEventListener;
+ }
+
+ /** Retrieve the relevant address (P2PKH or P2PSH) and compares it with the notification address of this wallet.
*/
+ public boolean isNotificationTransaction(Transaction tx) {
+ Address address = getAddressOfReceived(tx);
+ Address myNotificationAddress = mAccounts.get(0).getNotificationAddress();
+
+ return address != null && address.toString().equals(myNotificationAddress.toString());
+ }
+
+ /** Retrieve the relevant address (P2PKH or P2PSH), return true if any key in this wallet translates to it.
*/
+ // TODO: return true if and only if it is a channel address.
+ public boolean isToBIP47Address(Transaction transaction) {
+ List keys = vWallet.getImportedKeys();
+ for (ECKey key : keys) {
+ Address address = key.toAddress(getParams());
+ if (address == null) {
+ continue;
+ }
+ Address addressOfReceived = getAddressOfReceived(transaction);
+ if (addressOfReceived != null && address.toString().equals(addressOfReceived.toString())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Find the address that received the transaction (P2PKH or P2PSH output) */
+ public Address getAddressOfReceived(Transaction tx) {
+ for (final TransactionOutput output : tx.getOutputs()) {
+ try {
+ if (output.isMineOrWatched(vWallet)) {
+ final Script script = output.getScriptPubKey();
+ return script.getToAddress(params, true);
+ }
+ } catch (final ScriptException x) {
+ // swallow
+ }
+ }
+
+ return null;
+ }
+
+ /* Find the address (in P2PKH or P2PSH output) that does not belong to this wallet. */
+ public Address getAddressOfSent(Transaction tx) {
+ for (final TransactionOutput output : tx.getOutputs()) {
+ try {
+ if (!output.isMineOrWatched(vWallet)) {
+ final Script script = output.getScriptPubKey();
+ return script.getToAddress(params, true);
+ }
+ } catch (final ScriptException x) {
+ // swallow
+ }
+ }
+
+ return null;
+ }
+
+ /** Given a notification transaction, extracts a valid payment code */
+ public BIP47PaymentCode getPaymentCodeInNotificationTransaction(Transaction tx) {
+ byte[] privKeyBytes = mAccounts.get(0).getNotificationKey().getPrivKeyBytes();
+
+ return BIP47Util.getPaymentCodeInNotificationTransaction(privKeyBytes, tx);
+ }
+
+ // Receives a payment code and returns true iff there is already an incoming address generated for the channel
+ public boolean savePaymentCode(BIP47PaymentCode BIP47PaymentCode) {
+ if (bip47MetaData.containsKey(BIP47PaymentCode.toString())) {
+ BIP47Channel BIP47Channel = bip47MetaData.get(BIP47PaymentCode.toString());
+ if (BIP47Channel.getIncomingAddresses().size() != 0) {
+ return false;
+ } else {
+ try {
+ BIP47Channel.generateKeys(this);
+ return true;
+ } catch (Exception e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+ }
+
+ BIP47Channel BIP47Channel = new BIP47Channel(BIP47PaymentCode.toString());
+
+ try {
+ BIP47Channel.generateKeys(this);
+ bip47MetaData.put(BIP47PaymentCode.toString(), BIP47Channel);
+ } catch (Exception e) {
+ e.printStackTrace();
+ return false;
+ }
+
+ return true;
+ }
+
+ public BIP47Account getAccount(int i) {
+ return mAccounts.get(i);
+ }
+
+ public NetworkParameters getParams() {
+ return this.params;
+ }
+
+ public Address getAddressOfKey(ECKey key) {
+ return key.toAddress(getParams());
+ }
+
+ public void importKey(ECKey key) {
+ vWallet.importKey(key);
+ }
+
+ /** Return true if this is the first time the address is seen used*/
+ public boolean generateNewBip47IncomingAddress(String address) {
+ for (BIP47Channel BIP47Channel : bip47MetaData.values()) {
+ for (BIP47Address bip47Address : BIP47Channel.getIncomingAddresses()) {
+ if (!bip47Address.getAddress().equals(address)) {
+ continue;
+ }
+ if (bip47Address.isSeen()) {
+ return false;
+ }
+
+ int nextIndex = BIP47Channel.getCurrentIncomingIndex() + 1;
+ try {
+ ECKey key = getReceiveAddress(this, BIP47Channel.getPaymentCode(), nextIndex).getReceiveECKey();
+ vWallet.importKey(key);
+ Address newAddress = getAddressOfKey(key);
+ BIP47Channel.addNewIncomingAddress(newAddress.toString(), nextIndex);
+ bip47Address.setSeen(true);
+ return true;
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return false;
+ }
+ }
+ return false;
+ }
+
+ public BIP47Channel getBip47MetaForAddress(String address) {
+ for (BIP47Channel BIP47Channel : bip47MetaData.values()) {
+ for (BIP47Address bip47Address : BIP47Channel.getIncomingAddresses()) {
+ if (bip47Address.getAddress().equals(address)) {
+ return BIP47Channel;
+ }
+ }
+ }
+ return null;
+ }
+
+ public String getPaymentCodeForAddress(String address) {
+ for (BIP47Channel BIP47Channel : bip47MetaData.values()) {
+ for (BIP47Address bip47Address : BIP47Channel.getIncomingAddresses()) {
+ if (bip47Address.getAddress().equals(address)) {
+ return BIP47Channel.getPaymentCode();
+ }
+ }
+ }
+ return null;
+ }
+
+ public BIP47Channel getBip47MetaForOutgoingAddress(String address) {
+ for (BIP47Channel BIP47Channel : bip47MetaData.values()) {
+ for (String outgoingAddress : BIP47Channel.getOutgoingAddresses()) {
+ if (outgoingAddress.equals(address)) {
+ return BIP47Channel;
+ }
+ }
+ }
+ return null;
+ }
+
+ public BIP47Channel getBip47MetaForPaymentCode(String paymentCode) {
+ for (BIP47Channel BIP47Channel : bip47MetaData.values()) {
+ if (BIP47Channel.getPaymentCode().equals(paymentCode)) {
+ return BIP47Channel;
+ }
+ }
+ return null;
+ }
+
+ public Coin getValueOfTransaction(Transaction transaction) {
+ return transaction.getValue(vWallet);
+ }
+
+ public Coin getValueSentToMe(Transaction transaction) {
+ return transaction.getValueSentToMe(vWallet);
+ }
+
+ public Coin getValueSentFromMe(Transaction transaction) {
+ return transaction.getValueSentFromMe(vWallet);
+ }
+
+ public List getTransactions() {
+ return vWallet.getTransactionsByTime();
+ }
+
+ public long getBalanceValue() {
+ return vWallet.getBalance(org.bitcoinj.wallet.Wallet.BalanceType.ESTIMATED_SPENDABLE).getValue();
+ }
+
+ public Coin getBalance() {
+ return vWallet.getBalance(org.bitcoinj.wallet.Wallet.BalanceType.ESTIMATED_SPENDABLE);
+ }
+
+ public boolean isDownloading() {
+ return mBlockchainDownloadProgressTracker != null && mBlockchainDownloadProgressTracker.isDownloading();
+ }
+
+ public int getBlockchainProgress() {
+ return mBlockchainDownloadProgressTracker != null ? mBlockchainDownloadProgressTracker.getProgress() : -1;
+ }
+
+ public boolean isTransactionEntirelySelf(Transaction tx) {
+ for (final TransactionInput input : tx.getInputs()) {
+ final TransactionOutput connectedOutput = input.getConnectedOutput();
+ if (connectedOutput == null || !connectedOutput.isMine(vWallet))
+ return false;
+ }
+
+ for (final TransactionOutput output : tx.getOutputs()) {
+ if (!output.isMine(vWallet))
+ return false;
+ }
+
+ return true;
+ }
+
+ public String getPaymentCode() {
+ return getAccount(0).getStringPaymentCode();
+ }
+
+ public void resetBlockchainSync() {
+ File chainFile = new File(directory, coinName+".spvchain");
+ if (chainFile.exists()) {
+ log.debug("deleteSpvFile: exits");
+ chainFile.delete();
+ }
+ }
+
+ public String getMnemonicCode() {
+ return join(this.restoreFromSeed != null ?
+ this.restoreFromSeed.getMnemonicCode() :
+ vWallet.getKeyChainSeed().getMnemonicCode());
+ }
+
+ public Address getCurrentAddress() {
+ return vWallet.currentReceiveAddress();
+ }
+
+ public Address getAddressFromBase58(String addr) {
+ return Address.fromBase58(getParams(), addr);
+ }
+
+ /** Returns true if the given address is a valid payment code or a valid address in the
+ * wallet's blockchain network.
*/
+ public boolean isValidAddress(String address) {
+ if (address == null)
+ return false;
+
+ try {
+ BIP47PaymentCode BIP47PaymentCode = new BIP47PaymentCode(address);
+ return true;
+ } catch (AddressFormatException e){
+ }
+
+ try {
+ Address.fromBase58(getParams(), address);
+ return true;
+ } catch (AddressFormatException e) {
+ try {
+ CashAddressFactory.create().getFromFormattedAddress(params, address);
+ return true;
+ } catch (AddressFormatException e2) {
+ return false;
+ }
+ }
+ }
+
+ public Transaction createSend(String strAddr, long amount) throws InsufficientMoneyException {
+ Address address;
+ try {
+ address = Address.fromBase58(getParams(), strAddr);
+ } catch (AddressFormatException e1) {
+ try {
+ address = CashAddressFactory.create().getFromFormattedAddress(params, strAddr);
+ } catch (AddressFormatException e2) {
+ return null;
+ }
+ }
+ return createSend(address, amount);
+ }
+
+ private static Coin getDefaultFee(NetworkParameters params){
+ return Transaction.DEFAULT_TX_FEE;
+ }
+
+ public Transaction createSend(Address address, long amount) throws InsufficientMoneyException {
+ SendRequest sendRequest = SendRequest.to(address, Coin.valueOf(amount));
+
+ sendRequest.feePerKb = getDefaultFee(getParams());
+
+ vWallet.completeTx(sendRequest);
+ return sendRequest.tx;
+ }
+
+ public SendRequest makeNotificationTransaction(String paymentCode) throws InsufficientMoneyException {
+ BIP47Account toAccount = new BIP47Account(getParams(), paymentCode);
+ Coin ntValue = getParams().getMinNonDustOutput();
+ Address ntAddress = toAccount.getNotificationAddress();
+
+
+ log.debug("Balance: " + vWallet.getBalance());
+ log.debug("To notification address: "+ntAddress.toString());
+ log.debug("Value: "+ntValue.toFriendlyString());
+
+ SendRequest sendRequest = SendRequest.to(ntAddress, ntValue);
+
+ sendRequest.feePerKb = getDefaultFee(getParams());
+
+ sendRequest.memo = "notification_transaction";
+
+ org.bitcoinj.utils.BIP47Util.FeeCalculation feeCalculation = BIP47Util.calculateFee(vWallet, sendRequest, ntValue, vWallet.calculateAllSpendCandidates());
+
+ for (TransactionOutput output :feeCalculation.bestCoinSelection.gathered) {
+ sendRequest.tx.addInput(output);
+ }
+
+ if (sendRequest.tx.getInputs().size() > 0) {
+ TransactionInput txIn = sendRequest.tx.getInput(0);
+ RedeemData redeemData = txIn.getConnectedRedeemData(vWallet);
+ checkNotNull(redeemData, "StashTransaction exists in wallet that we cannot redeem: %s", txIn.getOutpoint().getHash());
+ log.debug("Keys: "+redeemData.keys.size());
+ log.debug("Private key 0?: "+redeemData.keys.get(0).hasPrivKey());
+ byte[] privKey = redeemData.getFullKey().getPrivKeyBytes();
+ log.debug("Private key: "+ Utils.HEX.encode(privKey));
+ byte[] pubKey = toAccount.getNotificationKey().getPubKey();
+ log.debug("Public Key: "+Utils.HEX.encode(pubKey));
+ byte[] outpoint = txIn.getOutpoint().bitcoinSerialize();
+
+ byte[] mask = null;
+ try {
+ BIP47SecretPoint BIP47SecretPoint = new BIP47SecretPoint(privKey, pubKey);
+ log.debug("Secret Point: "+Utils.HEX.encode(BIP47SecretPoint.ECDHSecretAsBytes()));
+ log.debug("Outpoint: "+Utils.HEX.encode(outpoint));
+ mask = BIP47PaymentCode.getMask(BIP47SecretPoint.ECDHSecretAsBytes(), outpoint);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ log.debug("My payment code: "+mAccounts.get(0).getPaymentCode().toString());
+ log.debug("Mask: "+Utils.HEX.encode(mask));
+ byte[] op_return = BIP47PaymentCode.blind(mAccounts.get(0).getPaymentCode().getPayload(), mask);
+
+ sendRequest.tx.addOutput(Coin.ZERO, ScriptBuilder.createOpReturnScript(op_return));
+ }
+
+ vWallet.completeTx(sendRequest);
+
+ log.debug("Completed SendRequest");
+ log.debug(sendRequest.toString());
+ log.debug(sendRequest.tx.toString());
+
+ sendRequest.tx.verify();
+
+ return sendRequest;
+ }
+
+ public Transaction getSignedNotificationTransaction(SendRequest sendRequest, String paymentCode) {
+ //BIP47Account toAccount = new BIP47Account(getParams(), paymentCode);
+
+ // notification address pub key
+ //BIP47Util.signTransaction(vWallet, sendRequest, toAccount.getNotificationKey().getPubKey(), mAccounts.get(0).getPaymentCode());
+
+ vWallet.commitTx(sendRequest.tx);
+
+ return sendRequest.tx;
+ }
+
+ public ListenableFuture broadcastTransaction(Transaction transactionToSend) {
+ vWallet.commitTx(transactionToSend);
+ return vPeerGroup.broadcastTransaction(transactionToSend).future();
+ }
+
+ public boolean putBip47Meta(String profileId, String name, @Nullable Transaction ntx) {
+ if (bip47MetaData.containsKey(profileId)) {
+ BIP47Channel BIP47Channel = bip47MetaData.get(profileId);
+ if (ntx != null)
+ BIP47Channel.setNtxHash(ntx.getHash());
+ if (!name.equals(BIP47Channel.getLabel())) {
+ BIP47Channel.setLabel(name);
+ return true;
+ }
+ } else {
+ bip47MetaData.put(profileId, new BIP47Channel(profileId, name));
+ if (ntx != null)
+ bip47MetaData.get(profileId).setNtxHash(ntx.getHash());
+ return true;
+ }
+ return false;
+ }
+
+ /* Mark a channel's notification transaction as sent*/
+ public void putPaymenCodeStatusSent(String paymentCode, Transaction ntx) {
+ if (bip47MetaData.containsKey(paymentCode)) {
+ BIP47Channel BIP47Channel = bip47MetaData.get(paymentCode);
+ BIP47Channel.setNtxHash(ntx.getHash());
+ BIP47Channel.setStatusSent();
+ } else {
+ putBip47Meta(paymentCode, paymentCode, ntx);
+ putPaymenCodeStatusSent(paymentCode, ntx);
+ }
+ }
+
+ /* Return the next address to send a payment to */
+ public String getCurrentOutgoingAddress(BIP47Channel BIP47Channel) {
+ try {
+ ECKey key = getSendAddress(this, new BIP47PaymentCode(BIP47Channel.getPaymentCode()), BIP47Channel.getCurrentOutgoingIndex()).getSendECKey();
+ return key.toAddress(getParams()).toString();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ public void commitTx(Transaction tx) {
+ vWallet.commitTx(tx);
+ }
+
+ public org.bitcoinj.wallet.Wallet.SendResult sendCoins(SendRequest sendRequest) throws InsufficientMoneyException {
+ return vWallet.sendCoins(sendRequest);
+ }
+
+ public void rescanTxBlock(Transaction tx) throws BlockStoreException {
+ int blockHeight = tx.getConfidence().getAppearedAtChainHeight() - 2;
+ this.vChain.rollbackBlockStore(blockHeight);
+ }
+
+ public File getDirectory() {
+ return directory;
+ }
+
+ public File getvWalletFile(){
+ return this.vWalletFile;
+ }
+
+ public PeerGroup getPeerGroup() { return this.vPeerGroup; }
+
+ public org.bitcoinj.wallet.Wallet getvWallet(){
+ return vWallet;
+ }
+
+ public void closeBlockStore() throws BlockStoreException, IllegalAccessException {
+ if (isStarted())
+ throw new IllegalAccessException("Must call stop() before closing block store");
+
+ if (vStore != null) {
+ vStore.close();
+ }
+ }
+
+ public List getAddresses(int size) {
+ List deterministicKeys = vWallet.getActiveKeyChain().getLeafKeys();
+ List addresses = new ArrayList(size);
+ for (int i = 0; i < size; i++) {
+ addresses.add(deterministicKeys.get(i).toAddress(getParams()).toBase58());
+ }
+ return addresses;
+ }
+
+ public int getExternalAddressCount() {
+ return vWallet.getActiveKeyChain().getIssuedReceiveKeys().size();
+ }
+
+}
diff --git a/core/src/main/java/org/bitcoinj/utils/BIP47Util.java b/core/src/main/java/org/bitcoinj/utils/BIP47Util.java
new file mode 100644
index 00000000000..d2c98363415
--- /dev/null
+++ b/core/src/main/java/org/bitcoinj/utils/BIP47Util.java
@@ -0,0 +1,419 @@
+/* Copyright (c) 2017 Stash
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.bitcoinj.utils;
+
+import org.bitcoinj.core.*;
+import org.bitcoinj.core.bip47.BIP47PaymentAddress;
+import org.bitcoinj.core.bip47.BIP47PaymentCode;
+import org.bitcoinj.crypto.BIP47SecretPoint;
+import org.bitcoinj.kits.BIP47AppKit;
+import org.bitcoinj.script.Script;
+import org.bitcoinj.script.ScriptBuilder;
+import org.bitcoinj.script.ScriptChunk;
+import org.bitcoinj.signers.MissingSigResolutionSigner;
+import org.bitcoinj.signers.TransactionSigner;
+import org.bitcoinj.wallet.CoinSelection;
+import org.bitcoinj.wallet.CoinSelector;
+import org.bitcoinj.wallet.DecryptingKeyBag;
+import org.bitcoinj.wallet.KeyBag;
+import org.bitcoinj.wallet.RedeemData;
+import org.bitcoinj.wallet.SendRequest;
+import org.bitcoinj.wallet.bip47.NotSecp256k1Exception;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.Nullable;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.spec.InvalidKeySpecException;
+import java.util.LinkedList;
+import java.util.List;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static org.bitcoinj.core.Utils.HEX;
+
+/**
+ * Created by jimmy on 10/3/17.
+ */
+
+public class BIP47Util {
+ private static final String TAG = "BIP47Util";
+ private static final Logger log = LoggerFactory.getLogger(BIP47Util.class);
+
+ public static class FeeCalculation {
+ public CoinSelection bestCoinSelection;
+ public TransactionOutput bestChangeOutput;
+ }
+
+ public static FeeCalculation calculateFee(org.bitcoinj.wallet.Wallet vWallet, SendRequest req, Coin value, List candidates) throws InsufficientMoneyException {
+ CoinSelector selector = vWallet.getCoinSelector();
+ // There are 3 possibilities for what adding change might do:
+ // 1) No effect
+ // 2) Causes increase in fee (change < 0.01 COINS)
+ // 3) Causes the transaction to have a dust output or change < fee increase (ie change will be thrown away)
+ // If we get either of the last 2, we keep note of what the inputs looked like at the time and try to
+ // add inputs as we go up the list (keeping track of minimum inputs for each category). At the end, we pick
+ // the best input set as the one which generates the lowest total fee.
+ Coin additionalValueForNextCategory = null;
+ CoinSelection selection3 = null;
+ CoinSelection selection2 = null;
+ TransactionOutput selection2Change = null;
+ CoinSelection selection1 = null;
+ TransactionOutput selection1Change = null;
+ // We keep track of the last size of the transaction we calculated but only if the act of adding inputs and
+ // change resulted in the size crossing a 1000 byte boundary. Otherwise it stays at zero.
+ int lastCalculatedSize = 0;
+ Coin valueNeeded, valueMissing = null;
+
+ Coin referenceDefaultMinTxFee = Transaction.REFERENCE_DEFAULT_MIN_TX_FEE;
+
+ while (true) {
+ req.tx.clearInputs();
+ Coin fees = req.feePerKb.multiply(lastCalculatedSize).divide(1000);
+ if (fees.compareTo(referenceDefaultMinTxFee) < 0)
+ fees = referenceDefaultMinTxFee;
+
+ valueNeeded = value.add(fees);
+ if (additionalValueForNextCategory != null)
+ valueNeeded = valueNeeded.add(additionalValueForNextCategory);
+ Coin additionalValueSelected = additionalValueForNextCategory;
+
+ // Of the coins we could spend, pick some that we actually will spend.
+ // selector is allowed to modify candidates list.
+ CoinSelection selection = selector.select(valueNeeded, new LinkedList(candidates));
+ // Can we afford this?
+ if (selection.valueGathered.compareTo(valueNeeded) < 0) {
+ valueMissing = valueNeeded.subtract(selection.valueGathered);
+ break;
+ }
+
+ // We keep track of an upper bound on transaction size to calculate fees that need to be added.
+ // Note that the difference between the upper bound and lower bound is usually small enough that it
+ // will be very rare that we pay a fee we do not need to.
+ //
+ // We can't be sure a selection is valid until we check fee per kb at the end, so we just store
+ // them here temporarily.
+ boolean eitherCategory2Or3 = false;
+ boolean isCategory3 = false;
+
+ Coin change = selection.valueGathered.subtract(valueNeeded);
+ if (additionalValueSelected != null)
+ change = change.add(additionalValueSelected);
+
+ // If change is < 0.01 BTC, we will need to have at least minfee to be accepted by the network
+ if (req.ensureMinRequiredFee && !change.equals(Coin.ZERO) &&
+ change.compareTo(Coin.CENT) < 0 && fees.compareTo(referenceDefaultMinTxFee) < 0) {
+ // This solution may fit into category 2, but it may also be category 3, we'll check that later
+ eitherCategory2Or3 = true;
+ additionalValueForNextCategory = Coin.CENT;
+ // If the change is smaller than the fee we want to add, this will be negative
+ change = change.subtract(referenceDefaultMinTxFee.subtract(fees));
+ }
+
+ int size = 0;
+ TransactionOutput changeOutput = null;
+ if (change.signum() > 0) {
+ // The value of the inputs is greater than what we want to send. Just like in real life then,
+ // we need to take back some coins ... this is called "change". Add another output that sends the change
+ // back to us. The address comes either from the request or currentChangeAddress() as a default.
+ Address changeAddress = req.changeAddress;
+ if (changeAddress == null)
+ changeAddress = vWallet.currentChangeAddress();
+ changeOutput = new TransactionOutput(vWallet.getNetworkParameters(), req.tx, change, changeAddress);
+ // If the change output would result in this transaction being rejected as dust, just drop the change and make it a fee
+ if (req.ensureMinRequiredFee && changeOutput.isDust()) {
+ // This solution definitely fits in category 3
+ isCategory3 = true;
+ additionalValueForNextCategory = referenceDefaultMinTxFee.add(
+ changeOutput.getMinNonDustValue().add(Coin.SATOSHI));
+ } else {
+ size += changeOutput.unsafeBitcoinSerialize().length + VarInt.sizeOf(req.tx.getOutputs().size()) - VarInt.sizeOf(req.tx.getOutputs().size() - 1);
+ // This solution is either category 1 or 2
+ if (!eitherCategory2Or3) // must be category 1
+ additionalValueForNextCategory = null;
+ }
+ } else {
+ if (eitherCategory2Or3) {
+ // This solution definitely fits in category 3 (we threw away change because it was smaller than MIN_TX_FEE)
+ isCategory3 = true;
+ additionalValueForNextCategory = referenceDefaultMinTxFee.add(Coin.SATOSHI);
+ }
+ }
+
+ // Now add unsigned inputs for the selected coins.
+ for (TransactionOutput output : selection.gathered) {
+ TransactionInput input = req.tx.addInput(output);
+ // If the scriptBytes don't default to none, our size calculations will be thrown off.
+ checkState(input.getScriptBytes().length == 0);
+ }
+
+ // Estimate transaction size and loop again if we need more fee per kb. The serialized tx doesn't
+ // include things we haven't added yet like input signatures/scripts or the change output.
+ size += req.tx.unsafeBitcoinSerialize().length;
+ size += estimateBytesForSigning(vWallet, selection);
+ if (size > lastCalculatedSize && req.feePerKb.signum() > 0) {
+ lastCalculatedSize = size;
+ // We need more fees anyway, just try again with the same additional value
+ additionalValueForNextCategory = additionalValueSelected;
+ continue;
+ }
+
+ if (isCategory3) {
+ if (selection3 == null)
+ selection3 = selection;
+ } else if (eitherCategory2Or3) {
+ // If we are in selection2, we will require at least CENT additional. If we do that, there is no way
+ // we can end up back here because CENT additional will always get us to 1
+ checkState(selection2 == null);
+ checkState(additionalValueForNextCategory.equals(Coin.CENT));
+ selection2 = selection;
+ selection2Change = checkNotNull(changeOutput); // If we get no change in category 2, we are actually in category 3
+ } else {
+ // Once we get a category 1 (change kept), we should break out of the loop because we can't do better
+ checkState(selection1 == null);
+ checkState(additionalValueForNextCategory == null);
+ selection1 = selection;
+ selection1Change = changeOutput;
+ }
+
+ if (additionalValueForNextCategory != null) {
+ if (additionalValueSelected != null)
+ checkState(additionalValueForNextCategory.compareTo(additionalValueSelected) > 0);
+ continue;
+ }
+ break;
+ }
+
+ req.tx.clearInputs();
+
+ if (selection3 == null && selection2 == null && selection1 == null) {
+ checkNotNull(valueMissing);
+ log.warn("Insufficient value in wallet for send: needed "+valueMissing.toFriendlyString()+" more");
+ throw new InsufficientMoneyException(valueMissing);
+ }
+
+ Coin lowestFee = null;
+ FeeCalculation result = new FeeCalculation();
+ if (selection1 != null) {
+ if (selection1Change != null)
+ lowestFee = selection1.valueGathered.subtract(selection1Change.getValue());
+ else
+ lowestFee = selection1.valueGathered;
+ result.bestCoinSelection = selection1;
+ result.bestChangeOutput = selection1Change;
+ }
+
+ if (selection2 != null) {
+ Coin fee = selection2.valueGathered.subtract(checkNotNull(selection2Change).getValue());
+ if (lowestFee == null || fee.compareTo(lowestFee) < 0) {
+ lowestFee = fee;
+ result.bestCoinSelection = selection2;
+ result.bestChangeOutput = selection2Change;
+ }
+ }
+
+ if (selection3 != null) {
+ if (lowestFee == null || selection3.valueGathered.compareTo(lowestFee) < 0) {
+ result.bestCoinSelection = selection3;
+ result.bestChangeOutput = null;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Given a send request containing transaction, attempts to sign it's inputs. This method expects transaction
+ * to have all necessary inputs connected or they will be ignored.
+ * Actual signing is done by pluggable signers and it's not guaranteed that
+ * transaction will be complete in the end.
+ */
+ static void signTransaction(org.bitcoinj.wallet.Wallet vWallet, SendRequest req, byte[] pubKey, BIP47PaymentCode myBIP47PaymentCode) {
+ Transaction tx = req.tx;
+ List inputs = tx.getInputs();
+ List outputs = tx.getOutputs();
+ checkState(inputs.size() > 0);
+ checkState(outputs.size() > 0);
+
+ KeyBag maybeDecryptingKeyBag = new DecryptingKeyBag(vWallet, req.aesKey);
+
+ int numInputs = tx.getInputs().size();
+ for (int i = 0; i < numInputs; i++) {
+ TransactionInput txIn = tx.getInput(i);
+ if (txIn.getConnectedOutput() == null) {
+ // Missing connected output, assuming already signed.
+ continue;
+ }
+
+ try {
+ // We assume if its already signed, its hopefully got a SIGHASH type that will not invalidate when
+ // we sign missing pieces (to check this would require either assuming any signatures are signing
+ // standard output types or a way to get processed signatures out of script execution)
+ txIn.getScriptSig().correctlySpends(tx, i, txIn.getConnectedOutput().getScriptPubKey());
+ continue;
+ } catch (ScriptException e) {
+ log.debug("Input contained an incorrect signature", e);
+ // Expected.
+ }
+
+ Script scriptPubKey = txIn.getConnectedOutput().getScriptPubKey();
+ RedeemData redeemData = txIn.getConnectedRedeemData(maybeDecryptingKeyBag);
+ checkNotNull(redeemData, "StashTransaction exists in wallet that we cannot redeem: %s", txIn.getOutpoint().getHash());
+ Script scriptSig = scriptPubKey.createEmptyInputScript(redeemData.keys.get(0), redeemData.redeemScript);
+ txIn.setScriptSig(scriptSig);
+ if (i == 0) {
+ log.debug("Keys: "+redeemData.keys.size());
+ log.debug("Private key 0?: "+redeemData.keys.get(0).hasPrivKey());
+ byte[] privKey = redeemData.getFullKey().getPrivKeyBytes();
+ log.debug("Private key: "+ HEX.encode(privKey));
+ log.debug("Public Key: "+HEX.encode(pubKey));
+ byte[] outpoint = txIn.getOutpoint().bitcoinSerialize();
+
+ byte[] mask = null;
+ try {
+ BIP47SecretPoint BIP47SecretPoint = new BIP47SecretPoint(privKey, pubKey);
+ log.debug("Secret Point: "+HEX.encode(BIP47SecretPoint.ECDHSecretAsBytes()));
+ log.debug("Outpoint: "+HEX.encode(outpoint));
+ mask = BIP47PaymentCode.getMask(BIP47SecretPoint.ECDHSecretAsBytes(), outpoint);
+ } catch (InvalidKeyException e) {
+ e.printStackTrace();
+ } catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ } catch (NoSuchProviderException e) {
+ e.printStackTrace();
+ } catch (InvalidKeySpecException e) {
+ e.printStackTrace();
+ }
+ log.debug("My payment code: "+ myBIP47PaymentCode.toString());
+ log.debug("Mask: "+HEX.encode(mask));
+ byte[] op_return = BIP47PaymentCode.blind(myBIP47PaymentCode.getPayload(), mask);
+
+ tx.addOutput(Coin.ZERO, ScriptBuilder.createOpReturnScript(op_return));
+ }
+ }
+
+ tx.shuffleOutputs();
+
+ TransactionSigner.ProposedTransaction proposal = new TransactionSigner.ProposedTransaction(tx, true);
+ for (TransactionSigner signer : vWallet.getTransactionSigners()) {
+ if (!signer.signInputs(proposal, maybeDecryptingKeyBag))
+ log.debug(signer.getClass().getName()+" returned false for the tx");
+ }
+
+ // resolve missing sigs if any
+ new MissingSigResolutionSigner(req.missingSigsMode).signInputs(proposal, maybeDecryptingKeyBag);
+ }
+
+ private static int estimateBytesForSigning(org.bitcoinj.wallet.Wallet vWallet, CoinSelection selection) {
+ int size = 0;
+ for (TransactionOutput output : selection.gathered) {
+ try {
+ Script script = output.getScriptPubKey();
+ ECKey key = null;
+ Script redeemScript = null;
+ if (script.isSentToAddress()) {
+ key = vWallet.findKeyFromPubHash(script.getPubKeyHash());
+ checkNotNull(key, "Coin selection includes unspendable outputs");
+ } else if (script.isPayToScriptHash()) {
+ redeemScript = vWallet.findRedeemDataFromScriptHash(script.getPubKeyHash()).redeemScript;
+ checkNotNull(redeemScript, "Coin selection includes unspendable outputs");
+ }
+ size += script.getNumberOfBytesRequiredToSpend(key, redeemScript);
+ } catch (ScriptException e) {
+ // If this happens it means an output script in a wallet tx could not be understood. That should never
+ // happen, if it does it means the wallet has got into an inconsistent state.
+ throw new IllegalStateException(e);
+ }
+ }
+ return size;
+ }
+
+ /**
+ * Finds the first output in a transaction whose op code is OP_RETURN.
+ */
+ @Nullable
+ public static TransactionOutput getOpCodeOutput(Transaction tx) {
+ List outputs = tx.getOutputs();
+ for (TransactionOutput o : outputs) {
+ if (o.getScriptPubKey().isOpReturn()) {
+ return o;
+ }
+ }
+ return null;
+ }
+
+ /** Returns true if the OP_RETURN op code begins with the byte 0x01 (version 1), */
+ public static boolean isValidNotificationTransactionOpReturn(TransactionOutput transactionOutput) {
+ byte[] data = getOpCodeData(transactionOutput);
+ return data != null && HEX.encode(data, 0, 1).equals("01");
+ }
+
+ /** Return the payload of the first op code e.g. OP_RETURN. */
+ private static byte[] getOpCodeData(TransactionOutput opReturnOutput) {
+ List chunks = opReturnOutput.getScriptPubKey().getChunks();
+ for (ScriptChunk chunk : chunks) {
+ if (!chunk.isOpCode() && chunk.data != null) {
+ return chunk.data;
+ }
+ }
+ return null;
+ }
+
+ /* Extract the payment code from an incoming notification transaction */
+ public static BIP47PaymentCode getPaymentCodeInNotificationTransaction(byte[] privKeyBytes, Transaction tx) {
+ log.debug( "Getting pub key");
+ byte[] pubKeyBytes = tx.getInput(0).getScriptSig().getPubKey();
+
+ log.debug( "Private Key: "+ HEX.encode(privKeyBytes));
+ log.debug( "Public Key: "+HEX.encode(pubKeyBytes));
+
+ log.debug( "Getting op_code data");
+ TransactionOutput opReturnOutput = getOpCodeOutput(tx);
+ if (opReturnOutput == null) {
+ return null;
+ }
+ byte[] data = getOpCodeData(opReturnOutput);
+
+ try {
+ log.debug( "Getting secret point..");
+ BIP47SecretPoint BIP47SecretPoint = new BIP47SecretPoint(privKeyBytes, pubKeyBytes);
+ log.debug( "Secret Point: "+ HEX.encode(BIP47SecretPoint.ECDHSecretAsBytes()));
+ log.debug( "Outpoint: "+ HEX.encode(tx.getInput(0).getOutpoint().bitcoinSerialize()));
+ log.debug( "Getting mask...");
+ byte[] s = BIP47PaymentCode.getMask(BIP47SecretPoint.ECDHSecretAsBytes(), tx.getInput(0).getOutpoint().bitcoinSerialize());
+ log.debug( "Getting payload...");
+ log.debug( "OpCode Data: "+HEX.encode(data));
+ log.debug( "Mask: "+HEX.encode(s));
+ byte[] payload = BIP47PaymentCode.blind(data, s);
+ log.debug( "Getting payment code...");
+ BIP47PaymentCode BIP47PaymentCode = new BIP47PaymentCode(payload);
+ log.debug( "Payment Code: "+ BIP47PaymentCode.toString());
+ return BIP47PaymentCode;
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ /** Derives the receive address at idx in depositWallet for senderPaymentCode to deposit, in the wallet's bip47 0th account, i.e. m / 47' / coin_type' / 0' / idx' .
. */
+ public static BIP47PaymentAddress getReceiveAddress(BIP47AppKit depositWallet, String senderPaymentCode, int idx) throws AddressFormatException, NotSecp256k1Exception {
+ ECKey accountKey = depositWallet.getAccount(0).keyAt(idx);
+ return getPaymentAddress(depositWallet.getParams(), new BIP47PaymentCode(senderPaymentCode), 0, accountKey);
+ }
+
+ /** Get the address of receiverBIP47PaymentCode's owner to send a payment to, using BTC as coin_type */
+ public static BIP47PaymentAddress getSendAddress(BIP47AppKit spendWallet, BIP47PaymentCode receiverBIP47PaymentCode, int idx) throws AddressFormatException, NotSecp256k1Exception {
+ ECKey key = spendWallet.getAccount(0).keyAt(0);
+ return getPaymentAddress(spendWallet.getParams(), receiverBIP47PaymentCode, idx, key);
+ }
+
+ /** Creates a BIP47PaymentAddress object that the sender will use to pay, using the hardened key at idx */
+ private static BIP47PaymentAddress getPaymentAddress(NetworkParameters networkParameters, BIP47PaymentCode pcode, int idx, ECKey key) throws AddressFormatException, NotSecp256k1Exception {
+ return new BIP47PaymentAddress(networkParameters, pcode, idx, key.getPrivKeyBytes());
+ }
+}
diff --git a/core/src/main/java/org/bitcoinj/wallet/bip47/NotSecp256k1Exception.java b/core/src/main/java/org/bitcoinj/wallet/bip47/NotSecp256k1Exception.java
new file mode 100644
index 00000000000..cc28eaf30be
--- /dev/null
+++ b/core/src/main/java/org/bitcoinj/wallet/bip47/NotSecp256k1Exception.java
@@ -0,0 +1,23 @@
+/* Copyright (c) 2017 Stash
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.bitcoinj.wallet.bip47;
+
+public class NotSecp256k1Exception extends Exception {
+ public NotSecp256k1Exception() {
+ }
+
+ public NotSecp256k1Exception(String message) {
+ super(message);
+ }
+
+ public NotSecp256k1Exception(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public NotSecp256k1Exception(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/core/src/main/java/org/bitcoinj/wallet/bip47/listeners/BlockchainDownloadProgressTracker.java b/core/src/main/java/org/bitcoinj/wallet/bip47/listeners/BlockchainDownloadProgressTracker.java
new file mode 100644
index 00000000000..96a1eb7d42f
--- /dev/null
+++ b/core/src/main/java/org/bitcoinj/wallet/bip47/listeners/BlockchainDownloadProgressTracker.java
@@ -0,0 +1,33 @@
+/* Copyright (c) 2017 Stash
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.bitcoinj.wallet.bip47.listeners;
+
+import org.bitcoinj.core.listeners.DownloadProgressTracker;
+
+/**
+ * Created by jimmy on 9/29/17.
+ */
+
+public abstract class BlockchainDownloadProgressTracker extends DownloadProgressTracker {
+ protected boolean isDownloading = false;
+ private String mCoin;
+
+ public BlockchainDownloadProgressTracker(String coin) {
+ super();
+
+ mCoin = coin;
+ }
+
+ public String getCoin() {
+ return mCoin;
+ }
+
+ public boolean isDownloading() {
+ return isDownloading;
+ }
+
+ public abstract int getProgress();
+}
diff --git a/core/src/main/java/org/bitcoinj/wallet/bip47/listeners/TransactionEventListener.java b/core/src/main/java/org/bitcoinj/wallet/bip47/listeners/TransactionEventListener.java
new file mode 100644
index 00000000000..baac3584c55
--- /dev/null
+++ b/core/src/main/java/org/bitcoinj/wallet/bip47/listeners/TransactionEventListener.java
@@ -0,0 +1,39 @@
+/* Copyright (c) 2017 Stash
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.bitcoinj.wallet.bip47.listeners;
+
+import org.bitcoinj.kits.BIP47AppKit;
+
+import org.bitcoinj.core.Coin;
+import org.bitcoinj.core.Transaction;
+import org.bitcoinj.core.listeners.TransactionConfidenceEventListener;
+import org.bitcoinj.wallet.listeners.WalletCoinsReceivedEventListener;
+
+/**
+ * Created by jimmy on 9/29/17.
+ */
+
+public abstract class TransactionEventListener implements WalletCoinsReceivedEventListener, TransactionConfidenceEventListener {
+ protected BIP47AppKit wallet;
+
+ public void setWallet(BIP47AppKit wallet) {
+ this.wallet = wallet;
+ }
+
+ @Override
+ public void onCoinsReceived(org.bitcoinj.wallet.Wallet wallet, Transaction transaction, Coin coin, Coin coin1) {
+ onTransactionReceived(this.wallet, transaction);
+ }
+
+ @Override
+ public void onTransactionConfidenceChanged(org.bitcoinj.wallet.Wallet wallet, Transaction transaction) {
+ onTransactionConfidenceEvent(this.wallet, transaction);
+ }
+
+ public abstract void onTransactionReceived(BIP47AppKit wallet, Transaction transaction);
+
+ public abstract void onTransactionConfidenceEvent(BIP47AppKit wallet, Transaction transaction);
+}
diff --git a/core/src/test/java/org/bitcoinj/crypto/bip47/BIP47AccountTest.java b/core/src/test/java/org/bitcoinj/crypto/bip47/BIP47AccountTest.java
new file mode 100644
index 00000000000..02dd67ff2a6
--- /dev/null
+++ b/core/src/test/java/org/bitcoinj/crypto/bip47/BIP47AccountTest.java
@@ -0,0 +1,40 @@
+package org.bitcoinj.crypto.bip47;
+
+import org.bitcoinj.core.AddressFormatException;
+import org.bitcoinj.core.bip47.BIP47Account;
+import org.bitcoinj.params.MainNetParams;
+import org.bitcoinj.params.TestNet3Params;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static junit.framework.TestCase.assertTrue;
+import static org.junit.Assert.assertEquals;
+
+public class BIP47AccountTest {
+ private static final Logger log = LoggerFactory.getLogger(BIP47AccountTest.class);
+
+ private final String ALICE_PAYMENT_CODE_V1 = "PM8TJTLJbPRGxSbc8EJi42Wrr6QbNSaSSVJ5Y3E4pbCYiTHUskHg13935Ubb7q8tx9GVbh2UuRnBc3WSyJHhUrw8KhprKnn9eDznYGieTzFcwQRya4GA";
+ private final String ALICE_NOTIFICATION_ADDRESS = "1JDdmqFLhpzcUwPeinhJbUPw4Co3aWLyzW";
+ private final String ALICE_NOTIFICATION_TESTADDRESS = "mxjb4tLKWrRsG3sGSMfgRPcFvCPkVgM4td";
+
+ @Test
+ public void constructFromPaymentCode() throws Exception {
+ // a valid payment code
+ BIP47Account acc = new BIP47Account(MainNetParams.get(), ALICE_PAYMENT_CODE_V1);
+ assertEquals(acc.getStringPaymentCode(), ALICE_PAYMENT_CODE_V1);
+ assertEquals(ALICE_NOTIFICATION_ADDRESS, acc.getNotificationAddress().toString());
+
+
+ BIP47Account testAcc = new BIP47Account(TestNet3Params.get(), ALICE_PAYMENT_CODE_V1);
+ assertEquals(testAcc.getStringPaymentCode(), ALICE_PAYMENT_CODE_V1);
+ assertEquals(ALICE_NOTIFICATION_TESTADDRESS, testAcc.getNotificationAddress().toString());
+
+ // invalid payment code
+ try {
+ BIP47Account badAcc = new BIP47Account(MainNetParams.get(), ALICE_PAYMENT_CODE_V1.substring(0, 10));
+ } catch (AddressFormatException expected){
+ assertTrue(expected.getMessage().equalsIgnoreCase("Checksum does not validate"));
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/src/test/java/org/bitcoinj/testing/TestWithBIP47AppKit.java b/core/src/test/java/org/bitcoinj/testing/TestWithBIP47AppKit.java
new file mode 100644
index 00000000000..89547d93384
--- /dev/null
+++ b/core/src/test/java/org/bitcoinj/testing/TestWithBIP47AppKit.java
@@ -0,0 +1,14 @@
+package org.bitcoinj.testing;
+
+import org.bitcoinj.kits.BIP47AppKit;
+
+public class TestWithBIP47AppKit extends TestWithWallet {
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ }
+
+ public void setWallet(BIP47AppKit w){
+ this.wallet = w.getvWallet();
+ }
+}
diff --git a/core/src/test/java/org/bitcoinj/wallet/bip47/BIP47AppKitTest.java b/core/src/test/java/org/bitcoinj/wallet/bip47/BIP47AppKitTest.java
new file mode 100644
index 00000000000..4dbe44188d6
--- /dev/null
+++ b/core/src/test/java/org/bitcoinj/wallet/bip47/BIP47AppKitTest.java
@@ -0,0 +1,275 @@
+package org.bitcoinj.wallet.bip47;
+
+import org.bitcoinj.core.*;
+import org.bitcoinj.core.bip47.BIP47Channel;
+import org.bitcoinj.crypto.MnemonicCode;
+import org.bitcoinj.kits.BIP47AppKit;
+import org.bitcoinj.params.MainNetParams;
+import org.bitcoinj.params.TestNet3Params;
+import org.bitcoinj.testing.TestWithBIP47AppKit;
+import org.bitcoinj.utils.BIP47Util;
+import org.bitcoinj.wallet.*;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.util.List;
+
+import static org.bitcoinj.core.Utils.HEX;
+import static org.junit.Assert.*;
+
+import org.bitcoinj.crypto.MnemonicCodeTest;
+import org.spongycastle.jce.provider.BouncyCastleProvider;
+
+public class BIP47AppKitTest extends TestWithBIP47AppKit {
+ private static final Logger log = LoggerFactory.getLogger(org.bitcoinj.wallet.WalletTest.class);
+
+ // - test vectors
+ private final String ALICE_BIP39_MNEMONIC = "response seminar brave tip suit recall often sound stick owner lottery motion";
+ private final String ALICE_BIP39_RAW_ENTROPY = "b7b8706d714d9166e66e7ed5b3c61048";
+ private final String ALICE_BIP32_SEED = "64dca76abc9c6f0cf3d212d248c380c4622c8f93b2c425ec6a5567fd5db57e10d3e6f94a2f6af4ac2edb8998072aad92098db73558c323777abf5bd1082d970a";
+ private final String ALICE_PAYMENT_CODE_V1 = "PM8TJTLJbPRGxSbc8EJi42Wrr6QbNSaSSVJ5Y3E4pbCYiTHUskHg13935Ubb7q8tx9GVbh2UuRnBc3WSyJHhUrw8KhprKnn9eDznYGieTzFcwQRya4GA";
+ private final String ALICE_NOTIFICATION_ADDRESS = "1JDdmqFLhpzcUwPeinhJbUPw4Co3aWLyzW";
+
+ private final String BOB_BIP39_MNEMONIC = "reward upper indicate eight swift arch injury crystal super wrestle already dentist";
+ private final String BOB_BIP39_RAW_ENTROPY = "b8bde1cba37dbc161d09aad9bfc81c9d";
+ private final String BOB_BIP32_SEED = "87eaaac5a539ab028df44d9110defbef3797ddb805ca309f61a69ff96dbaa7ab5b24038cf029edec5235d933110f0aea8aeecf939ed14fc20730bba71e4b1110";
+ private final String BOB_PAYMENT_CODE_V1 = "PM8TJS2JxQ5ztXUpBBRnpTbcUXbUHy2T1abfrb3KkAAtMEGNbey4oumH7Hc578WgQJhPjBxteQ5GHHToTYHE3A1w6p7tU6KSoFmWBVbFGjKPisZDbP97";
+ private final String BOB_NOTIFICATION_ADDRESS = "1ChvUUvht2hUQufHBXF8NgLhW8SwE2ecGV";
+
+ private final String SHARED_SECRET_0 = "f5bb84706ee366052471e6139e6a9a969d586e5fe6471a9b96c3d8caefe86fef";
+ private final String SHARED_SECRET_1 = "adfb9b18ee1c4460852806a8780802096d67a8c1766222598dc801076beb0b4d";
+ private final String SHARED_SECRET_2 = "79e860c3eb885723bb5a1d54e5cecb7df5dc33b1d56802906762622fa3c18ee5";
+ private final String SHARED_SECRET_3 = "d8339a01189872988ed4bd5954518485edebf52762bf698b75800ac38e32816d";
+ private final String SHARED_SECRET_4 = "14c687bc1a01eb31e867e529fee73dd7540c51b9ff98f763adf1fc2f43f98e83";
+ private final String SHARED_SECRET_5 = "725a8e3e4f74a50ee901af6444fb035cb8841e0f022da2201b65bc138c6066a2";
+ private final String SHARED_SECRET_6 = "521bf140ed6fb5f1493a5164aafbd36d8a9e67696e7feb306611634f53aa9d1f";
+ private final String SHARED_SECRET_7 = "5f5ecc738095a6fb1ea47acda4996f1206d3b30448f233ef6ed27baf77e81e46";
+ private final String SHARED_SECRET_8 = "1e794128ac4c9837d7c3696bbc169a8ace40567dc262974206fcf581d56defb4";
+ private final String SHARED_SECRET_9 = "fe36c27c62c99605d6cd7b63bf8d9fe85d753592b14744efca8be20a4d767c37";
+
+ // - keypairs M'/47'/0'/0'/0' .. M'/47'/0'/0'/2147483647'\
+
+ // - parameters to generate keys in ECDH.
+ private String ALICE_;
+
+
+ private final String CHANNEL_NTX = "010000000186f411ab1c8e70ae8a0795ab7a6757aea6e4d5ae1826fc7b8f00c597d500609c010000"
+ + "006b483045022100ac8c6dbc482c79e86c18928a8b364923c774bfdbd852059f6b3778f2319b59a7022029d7cc5724e2f41ab1fcf"
+ + "c0ba5a0d4f57ca76f72f19530ba97c860c70a6bf0a801210272d83d8a1fa323feab1c085157a0791b46eba34afb8bfbfaeb3a3fcc3"
+ + "f2c9ad8ffffffff0210270000000000001976a9148066a8e7ee82e5c5b9b7dc1765038340dc5420a988ac1027000000000000536a4"
+ + "c50010002063e4eb95e62791b06c50e1a3a942e1ecaaa9afbbeb324d16ae6821e091611fa96c0cf048f607fe51a0327f5e252897931"
+ + "1c78cb2de0d682c61e1180fc3d543b0000000000000000000000000000000000";
+
+ private final String CARLOS_BIP39_MNEMONIC = "fetch genuine seek want smile sea orient elbow basic where arrange display mask country walnut shuffle usage airport juice price grant scan wild alone";
+ private final String CARLOS_PAYMENT_CODE = "PM8TJaWSfZYLLuJnXctJ8npNYrUr5UCeT6KGmayJ4ENDSqj7VZr7uyX9exCo5JA8mFLkeXPaHoCBKuMDpYFs3tdxP2UxNiHSsZtb1KkKSVQyiwFhdLTZ";
+
+ private final Address OTHER_ADDRESS = new ECKey().toAddress(PARAMS);
+
+
+ // - blockchains to test
+ public static final String[] SUPPORTED_COINS = { "BCH", "BTC", "tBCH", "tBTC" };
+
+ // -
+
+ static {
+ // Adds a new provider, at a specified position
+ Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 2);
+ Security.addProvider(new BouncyCastleProvider());
+ }
+ private BIP47AppKit createWallet(String coinName, NetworkParameters params, File workingDir, String mnemonic) throws Exception {
+ DeterministicSeed seed = null;
+ if (mnemonic != null)
+ seed = new DeterministicSeed(mnemonic, null, "", Utils.currentTimeSeconds());
+ return new BIP47AppKit(coinName, params, workingDir, seed);
+ };
+
+ static void deleteFolder(String dirname){
+ File dir = new File(dirname);
+ if (!dir.exists())
+ return;
+ String[] entries = dir.list();
+ for(String s: entries){
+ File currentFile = new File(dir.getPath(),s);
+ if (currentFile.isDirectory())
+ deleteFolder((currentFile.getAbsolutePath()));
+ else
+ currentFile.delete();
+ }
+ dir.delete();
+ }
+
+ @Test
+ public void aliceWalletTest() throws Exception {
+
+ // - test bip 39
+ MnemonicCode mc = new MnemonicCode();
+ List code = mc.toMnemonic(HEX.decode(ALICE_BIP39_RAW_ENTROPY));
+ byte[] seed = MnemonicCode.toSeed(code,"");
+ byte[] entropy = mc.toEntropy(MnemonicCodeTest.split(ALICE_BIP39_MNEMONIC));
+
+ assertEquals(ALICE_BIP39_RAW_ENTROPY, HEX.encode(entropy));
+ assertEquals(ALICE_BIP39_MNEMONIC, Utils.join(code));
+ assertEquals(ALICE_BIP32_SEED, HEX.encode(seed));
+
+ File workingDir = new File("alice");
+
+ // - test bip 47
+ BIP47AppKit w = createWallet("BCH", MainNetParams.get(),workingDir,ALICE_BIP39_MNEMONIC);
+ assertEquals("xpub6D3t231wUi5v9PEa8mgmyV7Tovg3CzrGEUGNQTfm9cK93je3PgX9udfhzUDx29pkeeHQBPpTSHpAxnDgsf2XRbvLrmbCUQybjtHx8SUb3JB", w.getAccount(0).getXPub());
+ byte[] BCH_PUBKEY = w.getAccount(0).getPaymentCode().getPubKey();
+ assertEquals(ALICE_PAYMENT_CODE_V1, w.getPaymentCode());
+ assertEquals(ALICE_NOTIFICATION_ADDRESS, w.getAccount(0).getNotificationAddress().toString());
+ }
+
+ @Test
+ public void bobWalletTest() throws Exception {
+ // - test bip 39
+ MnemonicCode mc = new MnemonicCode();
+ List code = mc.toMnemonic(HEX.decode(BOB_BIP39_RAW_ENTROPY));
+ byte[] seed = MnemonicCode.toSeed(code,"");
+ byte[] entropy = mc.toEntropy(MnemonicCodeTest.split(BOB_BIP39_MNEMONIC));
+ assertEquals(BOB_BIP39_RAW_ENTROPY, HEX.encode(entropy));
+ assertEquals(BOB_BIP39_MNEMONIC, Utils.join(code));
+ assertEquals(BOB_BIP32_SEED, HEX.encode(seed));
+
+ File workingDir = new File("bob");
+
+ BIP47AppKit w = createWallet("BTC", MainNetParams.get(), workingDir, BOB_BIP39_MNEMONIC);
+ //assertEquals("tpubDCyvczNnKRM37QUHTCG1d6dFbXXkPUNfoay6XjVRhBKaGy47i1nFJQEmusyybMjaHBgpBbPFJRvwsWjtqQ8GTNiDw62ngm18w3QqyV6eHrY", w.getAccount(0).getXPub());
+ assertEquals(BOB_PAYMENT_CODE_V1, w.getPaymentCode());
+
+ w = createWallet("tBTC", TestNet3Params.get(),workingDir,BOB_BIP39_MNEMONIC);
+ //assertEquals("tpubDDf84AmZb36BcJKZHrpuToRNj9bhDEwQ1PbrZ7guzq3EHFLxhW9ZjtghxLZauHVwXsm42wSRRxrNEkbFJu4qmvA1PyK8rYTa1o33XVsr6vw", w.getAccount(0).getXPub());
+ assertEquals(BOB_PAYMENT_CODE_V1, w.getPaymentCode());
+ //assertEquals(BOB_NOTIFICATION_ADDRESS, w.getAccount(0).getNotificationAddress().toString());
+ }
+
+ @Test
+ public void notificationTransactionTest() throws Exception {
+ super.setUp();
+ // folders for alice and bob wallets
+
+ deleteFolder("alice2");deleteFolder("bob2");
+ File aliceDir = new File("alice2");
+ File bobDir = new File("bob2");
+
+ BIP47AppKit Alice = createWallet("BTC", MainNetParams.get(), aliceDir, ALICE_BIP39_MNEMONIC);
+ BIP47AppKit Bob = createWallet("BTC", MainNetParams.get(), bobDir, BOB_BIP39_MNEMONIC);
+
+ // Alice sends a payment to Bob, she saves Bob's payment code.
+
+ setWallet(Alice);
+ assertTrue(Alice.getCoinsReceivedEventListener() != null);
+ assertTrue(Alice.getAccount(0) != null);
+
+ // both have issued 1 receive address
+ assertEquals(1, Alice.getExternalAddressCount());
+ assertEquals(1, Bob.getExternalAddressCount());
+ assertEquals(0, Bob.getvWallet().getImportedKeys().size());
+
+ sendMoneyToWallet(Alice.getvWallet(), AbstractBlockChain.NewBlockType.BEST_CHAIN, Coin.COIN, Alice.getCurrentAddress());
+
+ SendRequest ntxRequest = Alice.makeNotificationTransaction(Bob.getPaymentCode());
+
+ // outpoint of first UTXO in Alice's NTX to bob'
+ //assertEquals("9414f1681fb1255bd168a806254321a837008dd4480c02226063183deb100204", ntxRequest.tx.getHash());
+
+ // Bob receives a NTX with Alice's payment code. Bob's wallet generates keys for Alice to use.
+ Bob.savePaymentCode(Alice.getAccount(0).getPaymentCode()); // bob saves alice
+ BIP47Channel channel = Bob.getBip47MetaForPaymentCode(Alice.getPaymentCode());
+ assertEquals(10, channel.getIncomingAddresses().size()); // bob's # of incoming addresses
+ assertEquals(10, Bob.getvWallet().getImportedKeys().size());
+
+ // - addresses used by Alice for sending to Bob
+ assertEquals("141fi7TY3h936vRUKh1qfUZr8rSBuYbVBK", channel.getIncomingAddresses().get(0).getAddress());
+ assertEquals("12u3Uued2fuko2nY4SoSFGCoGLCBUGPkk6", channel.getIncomingAddresses().get(1).getAddress());
+ assertEquals("1FsBVhT5dQutGwaPePTYMe5qvYqqjxyftc", channel.getIncomingAddresses().get(2).getAddress());
+ assertEquals("1CZAmrbKL6fJ7wUxb99aETwXhcGeG3CpeA", channel.getIncomingAddresses().get(3).getAddress());
+ assertEquals("1KQvRShk6NqPfpr4Ehd53XUhpemBXtJPTL", channel.getIncomingAddresses().get(4).getAddress());
+ assertEquals("1KsLV2F47JAe6f8RtwzfqhjVa8mZEnTM7t", channel.getIncomingAddresses().get(5).getAddress());
+ assertEquals("1DdK9TknVwvBrJe7urqFmaxEtGF2TMWxzD", channel.getIncomingAddresses().get(6).getAddress());
+ assertEquals("16DpovNuhQJH7JUSZQFLBQgQYS4QB9Wy8e", channel.getIncomingAddresses().get(7).getAddress());
+ assertEquals("17qK2RPGZMDcci2BLQ6Ry2PDGJErrNojT5", channel.getIncomingAddresses().get(8).getAddress());
+ assertEquals("1GxfdfP286uE24qLZ9YRP3EWk2urqXgC4s", channel.getIncomingAddresses().get(9).getAddress());
+
+ assertEquals(SHARED_SECRET_0, HEX.encode(BIP47Util.getReceiveAddress(Bob, ALICE_PAYMENT_CODE_V1, 0).getSharedSecret().ECDHSecretAsBytes()));
+ assertEquals(SHARED_SECRET_1, HEX.encode(BIP47Util.getReceiveAddress(Bob, ALICE_PAYMENT_CODE_V1, 1).getSharedSecret().ECDHSecretAsBytes()));
+ assertEquals(SHARED_SECRET_2, HEX.encode(BIP47Util.getReceiveAddress(Bob, ALICE_PAYMENT_CODE_V1, 2).getSharedSecret().ECDHSecretAsBytes()));
+ assertEquals(SHARED_SECRET_3, HEX.encode(BIP47Util.getReceiveAddress(Bob, ALICE_PAYMENT_CODE_V1, 3).getSharedSecret().ECDHSecretAsBytes()));
+ assertEquals(SHARED_SECRET_4, HEX.encode(BIP47Util.getReceiveAddress(Bob, ALICE_PAYMENT_CODE_V1, 4).getSharedSecret().ECDHSecretAsBytes()));
+ assertEquals(SHARED_SECRET_5, HEX.encode(BIP47Util.getReceiveAddress(Bob, ALICE_PAYMENT_CODE_V1, 5).getSharedSecret().ECDHSecretAsBytes()));
+ assertEquals(SHARED_SECRET_6, HEX.encode(BIP47Util.getReceiveAddress(Bob, ALICE_PAYMENT_CODE_V1, 6).getSharedSecret().ECDHSecretAsBytes()));
+ assertEquals(SHARED_SECRET_7, HEX.encode(BIP47Util.getReceiveAddress(Bob, ALICE_PAYMENT_CODE_V1, 7).getSharedSecret().ECDHSecretAsBytes()));
+ assertEquals(SHARED_SECRET_8, HEX.encode(BIP47Util.getReceiveAddress(Bob, ALICE_PAYMENT_CODE_V1, 8).getSharedSecret().ECDHSecretAsBytes()));
+ assertEquals(SHARED_SECRET_9, HEX.encode(BIP47Util.getReceiveAddress(Bob, ALICE_PAYMENT_CODE_V1, 9).getSharedSecret().ECDHSecretAsBytes()));
+
+
+ //assertEquals("736a25d9250238ad64ed5da03450c6a3f4f8f4dcdf0b58d1ed69029d76ead48d", HEX.encode(firstAddress.getSharedSecret().getPrivKey().getEncoded()));
+ //assertEquals("tpubDCyvczNnKRM37QUHTCG1d6dFbXXkPUNfoay6XjVRhBKaGy47i1nFJQEmusyybMjaHBgpBbPFJRvwsWjtqQ8GTNiDw62ngm18w3QqyV6eHrY", w.getAccount(0).getXPub());
+
+ }
+
+ @Test
+ public void carlosWalletTest() throws Exception {
+ File workingDir = new File("carlos");
+
+ BIP47AppKit w = createWallet("tBTC", TestNet3Params.get(), workingDir,CARLOS_BIP39_MNEMONIC);
+ assertEquals("tpubDCfC54qrR5PkDXCL2TkCJ46pYbFt7CX3UDF9e7qxsQw8Nm9HQy7eZ7tL3FrHhJhxAZU8dwmqpzhntLxax93914cq8vQUTsAxcKPBBoZDm28", w.getAccount(0).getXPub());
+ assertEquals(CARLOS_PAYMENT_CODE, w.getPaymentCode());
+
+ w = createWallet("BTC", MainNetParams.get(), workingDir,CARLOS_BIP39_MNEMONIC);
+ //assertEquals("tpubDDX2RK6EL7nuqjxFuZZTKsyMDx7PvPnbXmAtwuZaL9QorhjtussQTW5ReBF3G8G3wAY3RyusFkW2AuWz8YsiNXtkHZn2DmJRXA6m3rRwH8A", w.getAccount(0).getXPub());
+ }
+
+ @Test
+ public void testPeerGroupStart() throws Exception{
+ BIP47AppKit w = new BIP47AppKit("tBTC", TestNet3Params.get(), new File("peerGroup"), null);
+ assertFalse(w.isStarted());
+ assertFalse(w.isStarted());
+ w.startBlockchainDownload();
+ assertTrue(w.isStarted());
+ w.startBlockchainDownload();
+ assertTrue(w.isStarted());
+ w.stop();
+ assertFalse(w.isStarted());
+ w.startBlockchainDownload();
+ assertTrue(w.isStarted());
+ }
+
+ @Test
+ public void testIsValidAddress() throws Exception {
+ // test tbch
+ BIP47AppKit w = new BIP47AppKit("BCH", MainNetParams.get(), new File("validAdress"), null);
+ assertFalse(w.isValidAddress(null));
+ assertFalse(w.isValidAddress(""));
+ assertTrue(w.isValidAddress(ALICE_PAYMENT_CODE_V1));
+ assertTrue(w.isValidAddress("bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a"));
+ }
+
+ /* Test that a wallet restored from seed is persistent */
+ @Test
+ public void testMnemonicWordsPersistence() throws Exception{
+ // create a fresh new wallet
+ String davesPath = "src/test/resources/org/bitcoinj/wallet/dave-bip47";
+ File davesDir = new File(davesPath);
+ deleteFolder(davesPath);
+ DeterministicSeed davesSeed = new DeterministicSeed(new SecureRandom(), 256, "", System.currentTimeMillis() / 1000);
+ assertFalse(davesDir.exists()); //delete previous wallets created by this test
+ //create Dave's wallet and save it
+ BIP47AppKit Dave = new BIP47AppKit("BTC", MainNetParams.get(), davesDir, davesSeed);
+ String davesMnemonic = Dave.getMnemonicCode();
+ String davesPaymentCode = Dave.getPaymentCode();
+ assertTrue(davesDir.exists());
+ Dave.stop();
+ Dave.closeBlockStore();
+ // the same directory/coin will have the same seed as saved before.
+ BIP47AppKit DaveReload = new BIP47AppKit("BTC", MainNetParams.get(), davesDir, null);
+ assertEquals(DaveReload.getMnemonicCode(), davesMnemonic);
+ assertEquals(DaveReload.getPaymentCode(), davesPaymentCode);
+ deleteFolder(davesPath);
+ }
+}
diff --git a/core/src/test/java/org/bitcoinj/wallet/bip47/BIP47PaymentCodeTest.java b/core/src/test/java/org/bitcoinj/wallet/bip47/BIP47PaymentCodeTest.java
new file mode 100644
index 00000000000..76592aea04d
--- /dev/null
+++ b/core/src/test/java/org/bitcoinj/wallet/bip47/BIP47PaymentCodeTest.java
@@ -0,0 +1,47 @@
+package org.bitcoinj.wallet.bip47;
+
+import org.bitcoinj.core.AddressFormatException;
+import org.bitcoinj.core.bip47.BIP47Account;
+import org.bitcoinj.core.bip47.BIP47PaymentCode;
+import org.bitcoinj.params.MainNetParams;
+import org.junit.Test;
+
+import static org.bitcoinj.core.Utils.HEX;
+import static org.junit.Assert.assertEquals;
+
+public class BIP47PaymentCodeTest {
+ private final String ALICE_PAYMENT_CODE_V1 = "PM8TJTLJbPRGxSbc8EJi42Wrr6QbNSaSSVJ5Y3E4pbCYiTHUskHg13935Ubb7q8tx9GVbh2UuRnBc3WSyJHhUrw8KhprKnn9eDznYGieTzFcwQRya4GA";
+ private final String ALICE_NOTIFICATION_ADDRESS = "1JDdmqFLhpzcUwPeinhJbUPw4Co3aWLyzW";
+ private final String ALICE_NOTIFICATION_TESTADDRESS = "mxjb4tLKWrRsG3sGSMfgRPcFvCPkVgM4td";
+
+ @Test
+ public void pubKeyDeriveTests(){
+
+ BIP47PaymentCode alice = new BIP47PaymentCode(ALICE_PAYMENT_CODE_V1);
+ BIP47Account acc = new BIP47Account(MainNetParams.get(), ALICE_PAYMENT_CODE_V1);
+
+ byte[] alice0th = alice.derivePubKeyAt(MainNetParams.get(),0);
+ byte[] acc0th = acc.getNotificationKey().getPubKey();
+
+ byte[] alice1st = alice.derivePubKeyAt(MainNetParams.get(),1);
+ byte[] acc1st = acc.keyAt(1).getPubKey();
+
+ assertEquals(HEX.encode(alice0th), HEX.encode(acc0th));
+ assertEquals(HEX.encode(alice1st), HEX.encode(acc1st));
+ }
+
+ @Test(expected = AddressFormatException.class)
+ public void invalidPaymentCodeTest1(){
+ BIP47PaymentCode invalid = new BIP47PaymentCode("XXXTJTLJbPRGxSbc8EJi42Wrr6QbNSaSSVJ5Y3E4pbCYiTHUskHg13935Ubb7q8tx9GVbh2UuRnBc3WSyJHhUrw8KhprKnn9eDznYGieTzFcwQRya4GA");
+ }
+
+ @Test(expected = AddressFormatException.class)
+ public void invalidPaymentCodeTest2(){
+ BIP47PaymentCode invalid = new BIP47PaymentCode("");
+ }
+
+ @Test(expected = AddressFormatException.class)
+ public void invalidPaymentCodeTest3(){
+ new BIP47PaymentCode(ALICE_PAYMENT_CODE_V1.replace('x','y'));
+ }
+}
\ No newline at end of file