add bip47 keystores

This commit is contained in:
Craig Raw 2022-02-22 12:02:40 +02:00
parent f73cabad3c
commit 7bb07ab39e
21 changed files with 1116 additions and 96 deletions

View file

@ -11,6 +11,10 @@ public class KeyDerivation {
private final String derivationPath; private final String derivationPath;
private transient List<ChildNumber> derivation; private transient List<ChildNumber> derivation;
public KeyDerivation(String masterFingerprint, List<ChildNumber> derivation) {
this(masterFingerprint, writePath(derivation));
}
public KeyDerivation(String masterFingerprint, String derivationPath) { public KeyDerivation(String masterFingerprint, String derivationPath) {
this.masterFingerprint = masterFingerprint == null ? null : masterFingerprint.toLowerCase(); this.masterFingerprint = masterFingerprint == null ? null : masterFingerprint.toLowerCase();
this.derivationPath = derivationPath; this.derivationPath = derivationPath;
@ -91,6 +95,10 @@ public class KeyDerivation {
return true; return true;
} }
public static List<ChildNumber> getBip47Derivation(int account) {
return List.of(new ChildNumber(47, true), new ChildNumber(Network.get() == Network.MAINNET ? 0 : 1, true), new ChildNumber(account, true));
}
public KeyDerivation copy() { public KeyDerivation copy() {
return new KeyDerivation(masterFingerprint, derivationPath); return new KeyDerivation(masterFingerprint, derivationPath);
} }

View file

@ -9,6 +9,11 @@ public enum KeyPurpose {
public static final List<KeyPurpose> DEFAULT_PURPOSES = List.of(RECEIVE, CHANGE); public static final List<KeyPurpose> DEFAULT_PURPOSES = List.of(RECEIVE, CHANGE);
//The receive derivation is also used for BIP47 notifications
public static final KeyPurpose NOTIFICATION = RECEIVE;
//The change derivation is reused for the send chain in BIP47 wallets
public static final KeyPurpose SEND = CHANGE;
private final ChildNumber pathIndex; private final ChildNumber pathIndex;
KeyPurpose(ChildNumber pathIndex) { KeyPurpose(ChildNumber pathIndex) {

View file

@ -500,18 +500,24 @@ public class OutputDescriptor {
Utils.LexicographicByteArrayComparator lexicographicByteArrayComparator = new Utils.LexicographicByteArrayComparator(); Utils.LexicographicByteArrayComparator lexicographicByteArrayComparator = new Utils.LexicographicByteArrayComparator();
sortedKeys.sort((o1, o2) -> { sortedKeys.sort((o1, o2) -> {
List<ChildNumber> derivation1 = getDerivations(mapChildrenDerivations.get(o1)).get(0); ECKey key1 = getChildKeyForExtendedPubKey(o1);
derivation1.add(0, o1.getKeyChildNumber()); ECKey key2 = getChildKeyForExtendedPubKey(o2);
ECKey key1 = o1.getKey(derivation1);
List<ChildNumber> derivation2 = getDerivations(mapChildrenDerivations.get(o2)).get(0);
derivation2.add(0, o2.getKeyChildNumber());
ECKey key2 = o2.getKey(derivation2);
return lexicographicByteArrayComparator.compare(key1.getPubKey(), key2.getPubKey()); return lexicographicByteArrayComparator.compare(key1.getPubKey(), key2.getPubKey());
}); });
return sortedKeys; return sortedKeys;
} }
private ECKey getChildKeyForExtendedPubKey(ExtendedKey extendedKey) {
if(mapChildrenDerivations.get(extendedKey) == null) {
return extendedKey.getKey();
}
List<ChildNumber> derivation = getDerivations(mapChildrenDerivations.get(extendedKey)).get(0);
derivation.add(0, extendedKey.getKeyChildNumber());
return extendedKey.getKey(derivation);
}
private List<List<ChildNumber>> getDerivations(String childDerivation) { private List<List<ChildNumber>> getDerivations(String childDerivation) {
Matcher matcher = MULTIPATH_PATTERN.matcher(childDerivation); Matcher matcher = MULTIPATH_PATTERN.matcher(childDerivation);
if(matcher.find()) { if(matcher.find()) {

View file

@ -0,0 +1,19 @@
package com.sparrowwallet.drongo.bip47;
public class InvalidPaymentCodeException extends Exception {
public InvalidPaymentCodeException() {
super();
}
public InvalidPaymentCodeException(String msg) {
super(msg);
}
public InvalidPaymentCodeException(Throwable cause) {
super(cause);
}
public InvalidPaymentCodeException(String message, Throwable cause) {
super(message, cause);
}
}

View file

@ -0,0 +1,19 @@
package com.sparrowwallet.drongo.bip47;
public class NotSecp256k1Exception extends Exception {
public NotSecp256k1Exception() {
super();
}
public NotSecp256k1Exception(String msg) {
super(msg);
}
public NotSecp256k1Exception(Throwable cause) {
super(cause);
}
public NotSecp256k1Exception(String message, Throwable cause) {
super(message, cause);
}
}

View file

@ -0,0 +1,104 @@
package com.sparrowwallet.drongo.bip47;
import com.sparrowwallet.drongo.crypto.ECKey;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.ec.CustomNamedCurves;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.math.ec.ECPoint;
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;
public class PaymentAddress {
private static final X9ECParameters CURVE_PARAMS = CustomNamedCurves.getByName("secp256k1");
private static final ECDomainParameters CURVE = new ECDomainParameters(CURVE_PARAMS.getCurve(), CURVE_PARAMS.getG(), CURVE_PARAMS.getN(), CURVE_PARAMS.getH());
private final PaymentCode paymentCode;
private final int index;
private final byte[] privKey;
public PaymentAddress(PaymentCode paymentCode, int index, byte[] privKey) {
this.paymentCode = paymentCode;
this.index = index;
this.privKey = privKey;
}
public ECKey getSendECKey() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, IllegalStateException, InvalidKeySpecException, NotSecp256k1Exception {
return getSendECKey(getSecretPoint());
}
public ECKey getReceiveECKey() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, IllegalStateException, InvalidKeySpecException, NotSecp256k1Exception {
return getReceiveECKey(getSecretPoint());
}
public SecretPoint getSharedSecret() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, IllegalStateException, InvalidKeySpecException {
return sharedSecret();
}
public BigInteger getSecretPoint() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, IllegalStateException, InvalidKeySpecException, NotSecp256k1Exception {
return secretPoint();
}
public ECPoint getECPoint() {
ECKey ecKey = ECKey.fromPublicOnly(paymentCode.getKey(index).getPubKey());
return ecKey.getPubKeyPoint();
}
public byte[] hashSharedSecret() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, IllegalStateException, InvalidKeySpecException {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return digest.digest(getSharedSecret().ECDHSecretAsBytes());
}
private ECPoint get_sG(BigInteger s) {
return CURVE_PARAMS.getG().multiply(s);
}
private ECKey getSendECKey(BigInteger s) throws IllegalStateException {
ECPoint ecPoint = getECPoint();
ECPoint sG = get_sG(s);
return ECKey.fromPublicOnly(ecPoint.add(sG).getEncoded(true));
}
private ECKey getReceiveECKey(BigInteger s) {
BigInteger privKeyValue = ECKey.fromPrivate(privKey).getPrivKey();
return ECKey.fromPrivate(addSecp256k1(privKeyValue, s));
}
private BigInteger addSecp256k1(BigInteger b1, BigInteger b2) {
BigInteger ret = b1.add(b2);
if(ret.bitLength() > CURVE.getN().bitLength()) {
return ret.mod(CURVE.getN());
}
return ret;
}
private SecretPoint sharedSecret() throws InvalidKeySpecException, InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, NoSuchProviderException {
return new SecretPoint(privKey, paymentCode.getKey(index).getPubKey());
}
private boolean isSecp256k1(BigInteger b) {
return b.compareTo(BigInteger.ONE) > 0 && b.bitLength() <= CURVE.getN().bitLength();
}
private BigInteger secretPoint() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException, NotSecp256k1Exception {
//
// convert hash to value 's'
//
BigInteger s = new BigInteger(1, hashSharedSecret());
//
// check that 's' is on the secp256k1 curve
//
if(!isSecp256k1(s)) {
throw new NotSecp256k1Exception("Secret point not on Secp256k1 curve");
}
return s;
}
}

View file

@ -0,0 +1,380 @@
package com.sparrowwallet.drongo.bip47;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.DeterministicKey;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.crypto.HDKeyDerivation;
import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.wallet.Keystore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
public class PaymentCode {
private static final Logger log = LoggerFactory.getLogger(PaymentCode.class);
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 static final int SAMOURAI_FEATURE_BYTE = 79;
private static final int SAMOURAI_SEGWIT_BIT = 0;
private final String strPaymentCode;
private final byte[] pubkey;
private final byte[] chain;
private PaymentCode(String strPaymentCode, byte[] pubkey, byte[] chain) {
this.strPaymentCode = strPaymentCode;
this.pubkey = pubkey;
this.chain = chain;
}
public PaymentCode(String payment_code) throws InvalidPaymentCodeException {
strPaymentCode = payment_code;
Map.Entry<byte[], byte[]> pubKeyChain = parse().entrySet().iterator().next();
this.pubkey = pubKeyChain.getKey();
this.chain = pubKeyChain.getValue();
}
public PaymentCode(byte[] payload) {
if(payload.length != 80) {
throw new IllegalArgumentException("Payment code must be 80 bytes");
}
pubkey = new byte[PUBLIC_KEY_Y_LEN + PUBLIC_KEY_X_LEN];
chain = new byte[CHAIN_LEN];
System.arraycopy(payload, PUBLIC_KEY_Y_OFFSET, pubkey, 0, PUBLIC_KEY_Y_LEN + PUBLIC_KEY_X_LEN);
System.arraycopy(payload, CHAIN_OFFSET, chain, 0, CHAIN_LEN);
strPaymentCode = makeV1();
}
public PaymentCode(byte[] pubkey, byte[] chain) {
this.pubkey = pubkey;
this.chain = chain;
strPaymentCode = makeV1();
}
public ECKey getNotificationKey() {
DeterministicKey masterPubKey = createMasterPubKeyFromBytes();
return HDKeyDerivation.deriveChildKey(masterPubKey, ChildNumber.ZERO);
}
public Address getNotificationAddress() {
return ScriptType.P2PKH.getAddress(getNotificationKey());
}
public ECKey getKey(int index) {
DeterministicKey masterPubKey = createMasterPubKeyFromBytes();
return HDKeyDerivation.deriveChildKey(masterPubKey, new ChildNumber(index));
}
public byte[] getPayload() {
byte[] pcBytes = Base58.decodeChecked(strPaymentCode);
byte[] payload = new byte[PAYLOAD_LEN];
System.arraycopy(pcBytes, 1, payload, 0, payload.length);
return payload;
}
public int getType() throws InvalidPaymentCodeException {
byte[] payload = getPayload();
ByteBuffer bb = ByteBuffer.wrap(payload);
return bb.get();
}
public boolean isSegwitEnabled() {
return isBitSet(getPayload()[SAMOURAI_FEATURE_BYTE], SAMOURAI_SEGWIT_BIT);
}
public String toString() {
return strPaymentCode;
}
public static PaymentCode getPaymentCode(Transaction transaction, Keystore keystore) throws InvalidPaymentCodeException {
try {
TransactionInput txInput = getDesignatedInput(transaction);
ECKey pubKey = getDesignatedPubKey(txInput);
ECKey notificationPrivKey = keystore.getBip47ExtendedPrivateKey().getKey(List.of(ChildNumber.ZERO_HARDENED, new ChildNumber(0)));
SecretPoint secretPoint = new SecretPoint(notificationPrivKey.getPrivKeyBytes(), pubKey.getPubKey());
byte[] blindingMask = getMask(secretPoint.ECDHSecretAsBytes(), txInput.getOutpoint().bitcoinSerialize());
byte[] blindedPaymentCode = getOpReturnData(transaction);
return new PaymentCode(PaymentCode.blind(blindedPaymentCode, blindingMask));
} catch(Exception e) {
throw new InvalidPaymentCodeException("Could not determine payment code from transaction", e);
}
}
public static TransactionInput getDesignatedInput(Transaction transaction) {
for(TransactionInput txInput : transaction.getInputs()) {
if(getDesignatedPubKey(txInput) != null) {
return txInput;
}
}
throw new IllegalArgumentException("Cannot find designated input in notification transaction");
}
private static ECKey getDesignatedPubKey(TransactionInput txInput) {
for(ScriptChunk scriptChunk : txInput.getScriptSig().getChunks()) {
if(scriptChunk.isPubKey()) {
return scriptChunk.getPubKey();
}
}
for(ScriptChunk scriptChunk : txInput.getWitness().asScriptChunks()) {
if(scriptChunk.isPubKey()) {
return scriptChunk.getPubKey();
}
}
return null;
}
public static byte[] getOpReturnData(Transaction transaction) {
for(TransactionOutput txOutput : transaction.getOutputs()) {
List<ScriptChunk> scriptChunks = getOpReturnChunks(txOutput);
if(scriptChunks == null) {
continue;
}
return scriptChunks.get(1).getData();
}
throw new IllegalArgumentException("Cannot find OP_RETURN output in notification transaction");
}
private static List<ScriptChunk> getOpReturnChunks(TransactionOutput txOutput) {
List<ScriptChunk> scriptChunks = txOutput.getScript().getChunks();
if(scriptChunks.size() != 2) {
return null;
}
if(scriptChunks.get(0).getOpcode() != ScriptOpCodes.OP_RETURN) {
return null;
}
if(scriptChunks.get(1).getData() != null && scriptChunks.get(1).getData().length != 80) {
return null;
}
return scriptChunks;
}
public static byte[] getMask(byte[] sPoint, byte[] oPoint) {
Mac sha512_HMAC;
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 | NoSuchAlgorithmException ignored) {
//ignore
}
return mac_data;
}
public static byte[] blind(byte[] payload, byte[] mask) throws InvalidPaymentCodeException {
byte[] ret = new byte[PAYLOAD_LEN];
byte[] pubkey = new byte[PUBLIC_KEY_X_LEN];
byte[] chain = new byte[CHAIN_LEN];
byte[] buf0 = new byte[PUBLIC_KEY_X_LEN];
byte[] buf1 = new byte[CHAIN_LEN];
System.arraycopy(payload, 0, ret, 0, PAYLOAD_LEN);
System.arraycopy(payload, PUBLIC_KEY_X_OFFSET, pubkey, 0, PUBLIC_KEY_X_LEN);
System.arraycopy(payload, CHAIN_OFFSET, chain, 0, CHAIN_LEN);
System.arraycopy(mask, 0, buf0, 0, PUBLIC_KEY_X_LEN);
System.arraycopy(mask, PUBLIC_KEY_X_LEN, buf1, 0, CHAIN_LEN);
System.arraycopy(xor(pubkey, buf0), 0, ret, PUBLIC_KEY_X_OFFSET, PUBLIC_KEY_X_LEN);
System.arraycopy(xor(chain, buf1), 0, ret, CHAIN_OFFSET, CHAIN_LEN);
return ret;
}
private Map<byte[], byte[]> parse() throws InvalidPaymentCodeException {
byte[] pcBytes = Base58.decodeChecked(strPaymentCode);
ByteBuffer bb = ByteBuffer.wrap(pcBytes);
if(bb.get() != 0x47) {
throw new InvalidPaymentCodeException("Invalid payment code version");
}
byte[] chain = new byte[CHAIN_LEN];
byte[] pub = new byte[PUBLIC_KEY_X_LEN + PUBLIC_KEY_Y_LEN];
// type:
bb.get();
// features:
bb.get();
bb.get(pub);
if(pub[0] != 0x02 && pub[0] != 0x03) {
throw new InvalidPaymentCodeException("Invalid public key");
}
bb.get(chain);
return Map.of(pub, chain);
}
private String makeV1() {
return make(0x01);
}
private String make(int type) {
byte[] payload = new byte[PAYLOAD_LEN];
byte[] payment_code = new byte[PAYLOAD_LEN + 1];
for(int i = 0; i < payload.length; i++) {
payload[i] = (byte) 0x00;
}
// byte 0: type.
payload[0] = (byte) type;
// byte 1: features bit field. All bits must be zero except where specified elsewhere in this specification
// bit 0: Bitmessage notification
// bits 1-7: reserved
payload[1] = (byte) 0x00;
// replace sign & x code (33 bytes)
System.arraycopy(pubkey, 0, payload, PUBLIC_KEY_Y_OFFSET, pubkey.length);
// replace chain code (32 bytes)
System.arraycopy(chain, 0, payload, CHAIN_OFFSET, chain.length);
// add version byte
payment_code[0] = (byte) 0x47;
System.arraycopy(payload, 0, payment_code, 1, payload.length);
// append checksum
return base58EncodeChecked(payment_code);
}
public String makeSamouraiPaymentCode() throws InvalidPaymentCodeException {
byte[] payload = getPayload();
// set bit0 = 1 in 'Samourai byte' for segwit. Can send/receive P2PKH, P2SH-P2WPKH, P2WPKH (bech32)
payload[SAMOURAI_FEATURE_BYTE] = setBit(payload[SAMOURAI_FEATURE_BYTE], SAMOURAI_SEGWIT_BIT);
byte[] payment_code = new byte[PAYLOAD_LEN + 1];
// add version byte
payment_code[0] = (byte) 0x47;
System.arraycopy(payload, 0, payment_code, 1, payload.length);
// append checksum
return base58EncodeChecked(payment_code);
}
private String base58EncodeChecked(byte[] buf) {
byte[] checksum = Arrays.copyOfRange(Sha256Hash.hashTwice(buf), 0, 4);
byte[] bufChecked = new byte[buf.length + checksum.length];
System.arraycopy(buf, 0, bufChecked, 0, buf.length);
System.arraycopy(checksum, 0, bufChecked, bufChecked.length - 4, checksum.length);
return Base58.encode(bufChecked);
}
private boolean isBitSet(byte b, int pos) {
byte test = 0;
return (setBit(test, pos) & b) > 0;
}
private byte setBit(byte b, int pos) {
return (byte) (b | (1 << pos));
}
private DeterministicKey createMasterPubKeyFromBytes() {
return HDKeyDerivation.createMasterPubKeyFromBytes(pubkey, chain);
}
private static byte[] xor(byte[] a, byte[] b) {
if(a.length != b.length) {
log.error("Invalid length for xor: " + a.length + " vs " + b.length);
return null;
}
byte[] ret = new byte[a.length];
for(int i = 0; i < a.length; i++) {
ret[i] = (byte) ((int) b[i] ^ (int) a[i]);
}
return ret;
}
public boolean isValid() {
try {
byte[] pcodeBytes = Base58.decodeChecked(strPaymentCode);
ByteBuffer byteBuffer = ByteBuffer.wrap(pcodeBytes);
if(byteBuffer.get() != 0x47) {
throw new InvalidPaymentCodeException("Invalid version: " + strPaymentCode);
} else {
byte[] chain = new byte[32];
byte[] pub = new byte[33];
// type:
byteBuffer.get();
// feature:
byteBuffer.get();
byteBuffer.get(pub);
byteBuffer.get(chain);
ByteBuffer pubBytes = ByteBuffer.wrap(pub);
int firstByte = pubBytes.get();
return firstByte == 0x02 || firstByte == 0x03;
}
} catch(BufferUnderflowException | InvalidPaymentCodeException bue) {
return false;
}
}
public static PaymentCode fromString(String strPaymentCode) {
try {
return new PaymentCode(strPaymentCode);
} catch(InvalidPaymentCodeException e) {
log.error("Invalid payment code", e);
}
return null;
}
public PaymentCode copy() {
return new PaymentCode(strPaymentCode, pubkey, chain);
}
@Override
public boolean equals(Object o) {
if(this == o) {
return true;
}
if(o == null || getClass() != o.getClass()) {
return false;
}
PaymentCode that = (PaymentCode) o;
return strPaymentCode.equals(that.strPaymentCode);
}
@Override
public int hashCode() {
return strPaymentCode.hashCode();
}
}

View file

@ -0,0 +1,53 @@
package com.sparrowwallet.drongo.bip47;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.spec.ECParameterSpec;
import org.bouncycastle.jce.spec.ECPrivateKeySpec;
import org.bouncycastle.jce.spec.ECPublicKeySpec;
import javax.crypto.KeyAgreement;
import javax.crypto.SecretKey;
import java.math.BigInteger;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
public class SecretPoint {
private static final ECParameterSpec params = ECNamedCurveTable.getParameterSpec("secp256k1");
private static final String KEY_PROVIDER = "BC";
private final PrivateKey privKey;
private final PublicKey pubKey;
private final KeyFactory kf;
static {
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
}
public SecretPoint(byte[] dataPrv, byte[] dataPub) throws InvalidKeySpecException, InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, NoSuchProviderException {
kf = KeyFactory.getInstance("ECDH", KEY_PROVIDER);
privKey = loadPrivateKey(dataPrv);
pubKey = loadPublicKey(dataPub);
}
public byte[] ECDHSecretAsBytes() throws InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, NoSuchProviderException {
return ECDHSecret().getEncoded();
}
private SecretKey ECDHSecret() throws InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, NoSuchProviderException {
KeyAgreement ka = KeyAgreement.getInstance("ECDH", KEY_PROVIDER);
ka.init(privKey);
ka.doPhase(pubKey, true);
return ka.generateSecret("AES");
}
private PublicKey loadPublicKey(byte[] data) throws InvalidKeySpecException {
ECPublicKeySpec pubKey = new ECPublicKeySpec(params.getCurve().decodePoint(data), params);
return kf.generatePublic(pubKey);
}
private PrivateKey loadPrivateKey(byte[] data) throws InvalidKeySpecException {
ECPrivateKeySpec prvkey = new ECPrivateKeySpec(new BigInteger(1, data), params);
return kf.generatePrivate(prvkey);
}
}

View file

@ -38,6 +38,10 @@ public class HDKeyDerivation {
return new DeterministicKey(childNumberPath, chainCode, priv, null); return new DeterministicKey(childNumberPath, chainCode, priv, null);
} }
public static DeterministicKey createMasterPubKeyFromBytes(byte[] pubKeyBytes, byte[] chainCode) {
return new DeterministicKey(List.of(), chainCode, new LazyECPoint(ECKey.CURVE.getCurve(), pubKeyBytes), null, null);
}
public static DeterministicKey deriveChildKey(DeterministicKey parent, ChildNumber childNumber) throws HDDerivationException { public static DeterministicKey deriveChildKey(DeterministicKey parent, ChildNumber childNumber) throws HDDerivationException {
if(parent.isPubKeyOnly()) { if(parent.isPubKeyOnly()) {
RawKeyBytes rawKey = deriveChildKeyBytesFromPublic(parent, childNumber); RawKeyBytes rawKey = deriveChildKeyBytesFromPublic(parent, childNumber);

View file

@ -3,6 +3,7 @@ package com.sparrowwallet.drongo.protocol;
import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.Address;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.Objects; import java.util.Objects;
@ -51,6 +52,18 @@ public class TransactionOutPoint extends ChildMessage {
this.addresses = addresses; this.addresses = addresses;
} }
public byte[] bitcoinSerialize() {
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
bitcoinSerializeToStream(outputStream);
return outputStream.toByteArray();
} catch (IOException e) {
//can't happen
}
return null;
}
@Override @Override
protected void bitcoinSerializeToStream(OutputStream stream) throws IOException { protected void bitcoinSerializeToStream(OutputStream stream) throws IOException {
stream.write(hash.getReversedBytes()); stream.write(hash.getReversedBytes());

View file

@ -136,8 +136,8 @@ public class PSBT {
outputNodes.add(wallet.getWalletAddresses().getOrDefault(address, null)); outputNodes.add(wallet.getWalletAddresses().getOrDefault(address, null));
} }
} catch(NonStandardScriptException e) { } catch(NonStandardScriptException e) {
//Should never happen //Ignore, likely OP_RETURN output
throw new IllegalArgumentException(e); outputNodes.add(null);
} }
} }

View file

@ -4,11 +4,20 @@ import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.bip47.PaymentAddress;
import com.sparrowwallet.drongo.bip47.PaymentCode;
import com.sparrowwallet.drongo.crypto.*; import com.sparrowwallet.drongo.crypto.*;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List; import java.util.List;
public class Keystore extends Persistable { public class Keystore extends Persistable {
private static final Logger log = LoggerFactory.getLogger(Keystore.class);
public static final String DEFAULT_LABEL = "Keystore 1"; public static final String DEFAULT_LABEL = "Keystore 1";
public static final int MAX_LABEL_LENGTH = 16; public static final int MAX_LABEL_LENGTH = 16;
@ -17,9 +26,13 @@ public class Keystore extends Persistable {
private WalletModel walletModel = WalletModel.SPARROW; private WalletModel walletModel = WalletModel.SPARROW;
private KeyDerivation keyDerivation; private KeyDerivation keyDerivation;
private ExtendedKey extendedPublicKey; private ExtendedKey extendedPublicKey;
private PaymentCode externalPaymentCode;
private MasterPrivateExtendedKey masterPrivateExtendedKey; private MasterPrivateExtendedKey masterPrivateExtendedKey;
private DeterministicSeed seed; private DeterministicSeed seed;
//For BIP47 keystores - not persisted but must be unencrypted to generate keys
private ExtendedKey bip47ExtendedPrivateKey;
public Keystore() { public Keystore() {
this(DEFAULT_LABEL); this(DEFAULT_LABEL);
} }
@ -72,6 +85,14 @@ public class Keystore extends Persistable {
this.extendedPublicKey = extendedPublicKey; this.extendedPublicKey = extendedPublicKey;
} }
public PaymentCode getExternalPaymentCode() {
return externalPaymentCode;
}
public void setExternalPaymentCode(PaymentCode paymentCode) {
this.externalPaymentCode = paymentCode;
}
public boolean hasMasterPrivateExtendedKey() { public boolean hasMasterPrivateExtendedKey() {
return masterPrivateExtendedKey != null; return masterPrivateExtendedKey != null;
} }
@ -96,10 +117,34 @@ public class Keystore extends Persistable {
this.seed = seed; this.seed = seed;
} }
public boolean hasPrivateKey() { public boolean hasMasterPrivateKey() {
return hasSeed() || hasMasterPrivateExtendedKey(); return hasSeed() || hasMasterPrivateExtendedKey();
} }
public boolean hasPrivateKey() {
return hasMasterPrivateKey() || (source == KeystoreSource.SW_PAYMENT_CODE && bip47ExtendedPrivateKey != null);
}
public PaymentCode getPaymentCode() {
DeterministicKey bip47Key = bip47ExtendedPrivateKey.getKey();
return new PaymentCode(bip47Key.getPubKey(), bip47Key.getChainCode());
}
public ExtendedKey getBip47ExtendedPrivateKey() {
return bip47ExtendedPrivateKey;
}
public void setBip47ExtendedPrivateKey(ExtendedKey bip47ExtendedPrivateKey) {
this.bip47ExtendedPrivateKey = bip47ExtendedPrivateKey;
}
public PaymentAddress getPaymentAddress(KeyPurpose keyPurpose, int index) {
List<ChildNumber> derivation = keyDerivation.getDerivation();
ChildNumber derivationStart = keyDerivation.getDerivation().isEmpty() ? ChildNumber.ZERO_HARDENED : keyDerivation.getDerivation().get(derivation.size() - 1);
DeterministicKey privateKey = bip47ExtendedPrivateKey.getKey(List.of(derivationStart, new ChildNumber(keyPurpose == KeyPurpose.SEND ? 0 : index)));
return new PaymentAddress(externalPaymentCode, keyPurpose == KeyPurpose.SEND ? index : 0, privateKey.getPrivKeyBytes());
}
public DeterministicKey getMasterPrivateKey() throws MnemonicException { public DeterministicKey getMasterPrivateKey() throws MnemonicException {
if(seed == null && masterPrivateExtendedKey == null) { if(seed == null && masterPrivateExtendedKey == null) {
throw new IllegalArgumentException("Keystore does not contain a master private key, or seed to derive one from"); throw new IllegalArgumentException("Keystore does not contain a master private key, or seed to derive one from");
@ -136,22 +181,44 @@ public class Keystore extends Persistable {
return ExtendedKey.fromDescriptor(xprv.toString()); return ExtendedKey.fromDescriptor(xprv.toString());
} }
public DeterministicKey getKey(WalletNode walletNode) throws MnemonicException { public ECKey getKey(WalletNode walletNode) throws MnemonicException {
return getKey(walletNode.getKeyPurpose(), walletNode.getIndex()); if(source == KeystoreSource.SW_PAYMENT_CODE) {
} try {
if(walletNode.getKeyPurpose() != KeyPurpose.RECEIVE) {
throw new IllegalArgumentException("Cannot get private key for non-receive chain");
}
PaymentAddress paymentAddress = getPaymentAddress(walletNode.getKeyPurpose(), walletNode.getIndex());
return paymentAddress.getReceiveECKey();
} catch(IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid payment code " + externalPaymentCode, e);
} catch(Exception e) {
log.error("Cannot get receive private key at index " + walletNode.getIndex() + " for payment code " + externalPaymentCode, e);
}
}
public DeterministicKey getKey(KeyPurpose keyPurpose, int keyIndex) throws MnemonicException {
ExtendedKey extendedPrivateKey = getExtendedPrivateKey(); ExtendedKey extendedPrivateKey = getExtendedPrivateKey();
List<ChildNumber> derivation = List.of(extendedPrivateKey.getKeyChildNumber(), keyPurpose.getPathIndex(), new ChildNumber(keyIndex)); List<ChildNumber> derivation = new ArrayList<>();
derivation.add(extendedPrivateKey.getKeyChildNumber());
derivation.addAll(walletNode.getDerivation());
return extendedPrivateKey.getKey(derivation); return extendedPrivateKey.getKey(derivation);
} }
public DeterministicKey getPubKey(WalletNode walletNode) { public ECKey getPubKey(WalletNode walletNode) {
return getPubKey(walletNode.getKeyPurpose(), walletNode.getIndex()); if(source == KeystoreSource.SW_PAYMENT_CODE) {
} try {
PaymentAddress paymentAddress = getPaymentAddress(walletNode.getKeyPurpose(), walletNode.getIndex());
return walletNode.getKeyPurpose() == KeyPurpose.RECEIVE ? ECKey.fromPublicOnly(paymentAddress.getReceiveECKey()) : paymentAddress.getSendECKey();
} catch(IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid payment code " + externalPaymentCode, e);
} catch(Exception e) {
log.error("Cannot get receive private key at index " + walletNode.getIndex() + " for payment code " + externalPaymentCode, e);
}
}
public DeterministicKey getPubKey(KeyPurpose keyPurpose, int keyIndex) { List<ChildNumber> derivation = new ArrayList<>();
List<ChildNumber> derivation = List.of(extendedPublicKey.getKeyChildNumber(), keyPurpose.getPathIndex(), new ChildNumber(keyIndex)); derivation.add(extendedPublicKey.getKeyChildNumber());
derivation.addAll(walletNode.getDerivation());
return extendedPublicKey.getKey(derivation); return extendedPublicKey.getKey(derivation);
} }
@ -225,6 +292,16 @@ public class Keystore extends Persistable {
} }
} }
} }
if(source == KeystoreSource.SW_PAYMENT_CODE) {
if(externalPaymentCode == null) {
throw new InvalidKeystoreException("Source of " + source + " but no payment code is present");
}
if(bip47ExtendedPrivateKey == null) {
throw new InvalidKeystoreException("Source of " + source + " but no extended private key is present");
}
}
} }
public Keystore copy() { public Keystore copy() {
@ -244,6 +321,12 @@ public class Keystore extends Persistable {
if(seed != null) { if(seed != null) {
copy.setSeed(seed.copy()); copy.setSeed(seed.copy());
} }
if(externalPaymentCode != null) {
copy.setExternalPaymentCode(externalPaymentCode.copy());
}
if(bip47ExtendedPrivateKey != null) {
copy.setBip47ExtendedPrivateKey(bip47ExtendedPrivateKey.copy());
}
return copy; return copy;
} }
@ -274,6 +357,13 @@ public class Keystore extends Persistable {
keystore.setWalletModel(WalletModel.SPARROW); keystore.setWalletModel(WalletModel.SPARROW);
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, KeyDerivation.writePath(derivation))); keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, KeyDerivation.writePath(derivation)));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub.toString())); keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub.toString()));
int account = ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE).stream()
.mapToInt(scriptType -> scriptType.getAccount(keystore.getKeyDerivation().getDerivationPath())).filter(idx -> idx > -1).findFirst().orElse(0);
List<ChildNumber> bip47Derivation = KeyDerivation.getBip47Derivation(account);
DeterministicKey bip47Key = xprv.getKey(bip47Derivation);
ExtendedKey bip47ExtendedPrivateKey = new ExtendedKey(bip47Key, bip47Key.getParentFingerprint(), bip47Derivation.get(bip47Derivation.size() - 1));
keystore.setBip47ExtendedPrivateKey(ExtendedKey.fromDescriptor(bip47ExtendedPrivateKey.toString()));
} }
public boolean isEncrypted() { public boolean isEncrypted() {

View file

@ -1,9 +1,13 @@
package com.sparrowwallet.drongo.wallet; package com.sparrowwallet.drongo.wallet;
public enum KeystoreSource { public enum KeystoreSource {
HW_USB("Connected Hardware Wallet"), HW_AIRGAPPED("Airgapped Hardware Wallet"), SW_SEED("Software Wallet"), SW_WATCH("Watch Only Wallet"); HW_USB("Connected Hardware Wallet"),
HW_AIRGAPPED("Airgapped Hardware Wallet"),
SW_SEED("Software Wallet"),
SW_WATCH("Watch Only Wallet"),
SW_PAYMENT_CODE("Payment Code Wallet");
private String displayName; private final String displayName;
KeystoreSource(String displayName) { KeystoreSource(String displayName) {
this.displayName = displayName; this.displayName = displayName;

View file

@ -7,9 +7,16 @@ import java.util.stream.Collectors;
public class PresetUtxoSelector extends SingleSetUtxoSelector { public class PresetUtxoSelector extends SingleSetUtxoSelector {
private final Collection<BlockTransactionHashIndex> presetUtxos; private final Collection<BlockTransactionHashIndex> presetUtxos;
private final boolean maintainOrder;
public PresetUtxoSelector(Collection<BlockTransactionHashIndex> presetUtxos) { public PresetUtxoSelector(Collection<BlockTransactionHashIndex> presetUtxos) {
this.presetUtxos = presetUtxos; this.presetUtxos = presetUtxos;
this.maintainOrder = false;
}
public PresetUtxoSelector(Collection<BlockTransactionHashIndex> presetUtxos, boolean maintainOrder) {
this.presetUtxos = presetUtxos;
this.maintainOrder = maintainOrder;
} }
@Override @Override
@ -26,10 +33,19 @@ public class PresetUtxoSelector extends SingleSetUtxoSelector {
} }
} }
if(maintainOrder && utxos.containsAll(presetUtxos)) {
return presetUtxos;
}
return utxos; return utxos;
} }
public Collection<BlockTransactionHashIndex> getPresetUtxos() { public Collection<BlockTransactionHashIndex> getPresetUtxos() {
return presetUtxos; return presetUtxos;
} }
@Override
public boolean shuffleInputs() {
return !maintainOrder;
}
} }

View file

@ -5,4 +5,7 @@ import java.util.List;
public interface UtxoSelector { public interface UtxoSelector {
List<Collection<BlockTransactionHashIndex>> selectSets(long targetValue, Collection<OutputGroup> candidates); List<Collection<BlockTransactionHashIndex>> selectSets(long targetValue, Collection<OutputGroup> candidates);
default boolean shuffleInputs() {
return true;
}
} }

View file

@ -1,11 +1,10 @@
package com.sparrowwallet.drongo.wallet; package com.sparrowwallet.drongo.wallet;
import com.sparrowwallet.drongo.BitcoinUnit; import com.sparrowwallet.drongo.*;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.bip47.PaymentCode;
import com.sparrowwallet.drongo.crypto.ChildNumber; import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.DeterministicKey;
import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.crypto.Key; import com.sparrowwallet.drongo.crypto.Key;
import com.sparrowwallet.drongo.policy.Policy; import com.sparrowwallet.drongo.policy.Policy;
@ -109,7 +108,7 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
throw new IllegalStateException("Cannot add child wallet to existing child wallet"); throw new IllegalStateException("Cannot add child wallet to existing child wallet");
} }
if(childWallet.containsPrivateKeys() && childWallet.isEncrypted()) { if(childWallet.containsMasterPrivateKeys() && childWallet.isEncrypted()) {
throw new IllegalStateException("Cannot derive child wallet xpub from encrypted wallet"); throw new IllegalStateException("Cannot derive child wallet xpub from encrypted wallet");
} }
@ -136,7 +135,7 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
childDerivation.add(standardAccount.getChildNumber()); childDerivation.add(standardAccount.getChildNumber());
} }
if(keystore.hasPrivateKey()) { if(keystore.hasMasterPrivateKey()) {
try { try {
Keystore derivedKeystore = keystore.hasSeed() ? Keystore.fromSeed(keystore.getSeed(), childDerivation) : Keystore.fromMasterPrivateExtendedKey(keystore.getMasterPrivateExtendedKey(), childDerivation); Keystore derivedKeystore = keystore.hasSeed() ? Keystore.fromSeed(keystore.getSeed(), childDerivation) : Keystore.fromMasterPrivateExtendedKey(keystore.getMasterPrivateExtendedKey(), childDerivation);
keystore.setKeyDerivation(derivedKeystore.getKeyDerivation()); keystore.setKeyDerivation(derivedKeystore.getKeyDerivation());
@ -167,6 +166,65 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
return null; return null;
} }
public Wallet addChildWallet(PaymentCode externalPaymentCode, ScriptType childScriptType, BlockTransactionHashIndex notificationOutput, BlockTransaction notificationTransaction) {
Wallet bip47Wallet = addChildWallet(externalPaymentCode, childScriptType);
WalletNode notificationNode = bip47Wallet.getNode(KeyPurpose.NOTIFICATION);
notificationNode.getTransactionOutputs().add(notificationOutput);
bip47Wallet.updateTransactions(Map.of(notificationTransaction.getHash(), notificationTransaction));
return bip47Wallet;
}
public Wallet addChildWallet(PaymentCode externalPaymentCode, ScriptType childScriptType) {
if(policyType != PolicyType.SINGLE) {
throw new IllegalStateException("Cannot add payment code wallet to " + policyType.getName() + " wallet");
}
if(scriptType != P2PKH && scriptType != P2WPKH) {
throw new IllegalStateException("Cannot add payment code wallet to " + scriptType.getName() + " wallet");
}
Keystore masterKeystore = getKeystores().get(0);
if(masterKeystore.getBip47ExtendedPrivateKey() == null) {
throw new IllegalStateException("Cannot add payment code wallet, BIP47 extended private key not present");
}
Wallet childWallet = new Wallet(childScriptType + "-" + externalPaymentCode.toString());
childWallet.setPolicyType(PolicyType.SINGLE);
childWallet.setScriptType(childScriptType);
childWallet.setGapLimit(5);
Keystore keystore = new Keystore("BIP47");
keystore.setSource(KeystoreSource.SW_PAYMENT_CODE);
keystore.setWalletModel(WalletModel.SPARROW);
List<ChildNumber> derivation = KeyDerivation.getBip47Derivation(getAccountIndex());
keystore.setKeyDerivation(new KeyDerivation(masterKeystore.getKeyDerivation().getMasterFingerprint(), derivation));
keystore.setExternalPaymentCode(externalPaymentCode);
keystore.setBip47ExtendedPrivateKey(masterKeystore.getBip47ExtendedPrivateKey());
DeterministicKey pubKey = keystore.getBip47ExtendedPrivateKey().getKey().dropPrivateBytes().dropParent();
keystore.setExtendedPublicKey(new ExtendedKey(pubKey, keystore.getBip47ExtendedPrivateKey().getParentFingerprint(), derivation.get(derivation.size() - 1)));
childWallet.getKeystores().add(keystore);
childWallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, childWallet.getKeystores(), 1));
childWallet.setMasterWallet(this);
getChildWallets().add(childWallet);
return childWallet;
}
public Wallet getChildWallet(PaymentCode externalPaymentCode, ScriptType childScriptType) {
for(Wallet childWallet : getChildWallets()) {
if(childWallet.getKeystores().size() == 1 && externalPaymentCode != null && childWallet.getScriptType() == childScriptType &&
childWallet.getKeystores().get(0).getExternalPaymentCode() != null &&
(externalPaymentCode.equals(childWallet.getKeystores().get(0).getExternalPaymentCode()) ||
externalPaymentCode.getNotificationAddress().equals(childWallet.getKeystores().get(0).getExternalPaymentCode().getNotificationAddress()))) {
return childWallet;
}
}
return null;
}
public List<Wallet> getAllWallets() { public List<Wallet> getAllWallets() {
List<Wallet> allWallets = new ArrayList<>(); List<Wallet> allWallets = new ArrayList<>();
Wallet masterWallet = isMasterWallet() ? this : getMasterWallet(); Wallet masterWallet = isMasterWallet() ? this : getMasterWallet();
@ -175,6 +233,70 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
return allWallets; return allWallets;
} }
public boolean hasPaymentCode() {
return getKeystores().size() == 1 && getKeystores().get(0).getBip47ExtendedPrivateKey() != null;
}
public PaymentCode getPaymentCode() {
if(hasPaymentCode()) {
return getKeystores().get(0).getPaymentCode();
}
return null;
}
public Wallet getNotificationWallet() {
if(isMasterWallet() && hasPaymentCode()) {
Wallet notificationWallet = new Wallet();
notificationWallet.setPolicyType(PolicyType.SINGLE);
notificationWallet.setScriptType(ScriptType.P2PKH);
notificationWallet.setGapLimit(0);
Keystore masterKeystore = getKeystores().get(0);
Keystore keystore = new Keystore();
keystore.setSource(KeystoreSource.SW_WATCH);
keystore.setWalletModel(WalletModel.SPARROW);
keystore.setKeyDerivation(new KeyDerivation(masterKeystore.getKeyDerivation().getMasterFingerprint(), KeyDerivation.getBip47Derivation(getAccountIndex())));
keystore.setExtendedPublicKey(masterKeystore.getBip47ExtendedPrivateKey());
keystore.setBip47ExtendedPrivateKey(masterKeystore.getBip47ExtendedPrivateKey());
notificationWallet.getKeystores().add(keystore);
notificationWallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2PKH, notificationWallet.getKeystores(), 1));
return notificationWallet;
}
return null;
}
public Map<BlockTransaction, WalletNode> getNotificationTransaction(PaymentCode externalPaymentCode) {
Address notificationAddress = externalPaymentCode.getNotificationAddress();
for(Map.Entry<BlockTransactionHashIndex, WalletNode> txoEntry : getWalletTxos().entrySet()) {
if(txoEntry.getKey().isSpent()) {
BlockTransaction blockTransaction = transactions.get(txoEntry.getKey().getSpentBy().getHash());
if(blockTransaction != null) {
for(TransactionOutput txOutput : blockTransaction.getTransaction().getOutputs()) {
if(notificationAddress.equals(txOutput.getScript().getToAddress())) {
try {
PaymentCode.getOpReturnData(blockTransaction.getTransaction());
return Map.of(blockTransaction, txoEntry.getValue());
} catch(Exception e) {
//ignore
}
}
}
}
}
}
return Collections.emptyMap();
}
public boolean isBip47() {
return !isMasterWallet() && getKeystores().size() == 1 && getKeystores().get(0).getSource() == KeystoreSource.SW_PAYMENT_CODE;
}
public StandardAccount getStandardAccountType() { public StandardAccount getStandardAccountType() {
int accountIndex = getAccountIndex(); int accountIndex = getAccountIndex();
return Arrays.stream(StandardAccount.values()).filter(standardAccount -> standardAccount.getChildNumber().num() == accountIndex).findFirst().orElse(null); return Arrays.stream(StandardAccount.values()).filter(standardAccount -> standardAccount.getChildNumber().num() == accountIndex).findFirst().orElse(null);
@ -454,10 +576,6 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
} }
public ECKey getPubKey(WalletNode node) { public ECKey getPubKey(WalletNode node) {
return getPubKey(node.getKeyPurpose(), node.getIndex());
}
public ECKey getPubKey(KeyPurpose keyPurpose, int index) {
if(policyType == PolicyType.MULTI) { if(policyType == PolicyType.MULTI) {
throw new IllegalStateException("Attempting to retrieve a single key for a multisig policy wallet"); throw new IllegalStateException("Attempting to retrieve a single key for a multisig policy wallet");
} else if(policyType == PolicyType.CUSTOM) { } else if(policyType == PolicyType.CUSTOM) {
@ -465,33 +583,25 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
} }
Keystore keystore = getKeystores().get(0); Keystore keystore = getKeystores().get(0);
return keystore.getPubKey(keyPurpose, index); return keystore.getPubKey(node);
} }
public List<ECKey> getPubKeys(WalletNode node) { public List<ECKey> getPubKeys(WalletNode node) {
return getPubKeys(node.getKeyPurpose(), node.getIndex());
}
public List<ECKey> getPubKeys(KeyPurpose keyPurpose, int index) {
if(policyType == PolicyType.SINGLE) { if(policyType == PolicyType.SINGLE) {
throw new IllegalStateException("Attempting to retrieve multiple keys for a singlesig policy wallet"); throw new IllegalStateException("Attempting to retrieve multiple keys for a singlesig policy wallet");
} else if(policyType == PolicyType.CUSTOM) { } else if(policyType == PolicyType.CUSTOM) {
throw new UnsupportedOperationException("Cannot determine public keys for a custom policy"); throw new UnsupportedOperationException("Cannot determine public keys for a custom policy");
} }
return getKeystores().stream().map(keystore -> keystore.getPubKey(keyPurpose, index)).collect(Collectors.toList()); return getKeystores().stream().map(keystore -> keystore.getPubKey(node)).collect(Collectors.toList());
} }
public Address getAddress(WalletNode node) { public Address getAddress(WalletNode node) {
return getAddress(node.getKeyPurpose(), node.getIndex());
}
public Address getAddress(KeyPurpose keyPurpose, int index) {
if(policyType == PolicyType.SINGLE) { if(policyType == PolicyType.SINGLE) {
ECKey pubKey = getPubKey(keyPurpose, index); ECKey pubKey = getPubKey(node);
return scriptType.getAddress(pubKey); return scriptType.getAddress(pubKey);
} else if(policyType == PolicyType.MULTI) { } else if(policyType == PolicyType.MULTI) {
List<ECKey> pubKeys = getPubKeys(keyPurpose, index); List<ECKey> pubKeys = getPubKeys(node);
Script script = ScriptType.MULTISIG.getOutputScript(defaultPolicy.getNumSignaturesRequired(), pubKeys); Script script = ScriptType.MULTISIG.getOutputScript(defaultPolicy.getNumSignaturesRequired(), pubKeys);
return scriptType.getAddress(script); return scriptType.getAddress(script);
} else { } else {
@ -500,15 +610,11 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
} }
public Script getOutputScript(WalletNode node) { public Script getOutputScript(WalletNode node) {
return getOutputScript(node.getKeyPurpose(), node.getIndex());
}
public Script getOutputScript(KeyPurpose keyPurpose, int index) {
if(policyType == PolicyType.SINGLE) { if(policyType == PolicyType.SINGLE) {
ECKey pubKey = getPubKey(keyPurpose, index); ECKey pubKey = getPubKey(node);
return scriptType.getOutputScript(pubKey); return scriptType.getOutputScript(pubKey);
} else if(policyType == PolicyType.MULTI) { } else if(policyType == PolicyType.MULTI) {
List<ECKey> pubKeys = getPubKeys(keyPurpose, index); List<ECKey> pubKeys = getPubKeys(node);
Script script = ScriptType.MULTISIG.getOutputScript(defaultPolicy.getNumSignaturesRequired(), pubKeys); Script script = ScriptType.MULTISIG.getOutputScript(defaultPolicy.getNumSignaturesRequired(), pubKeys);
return scriptType.getOutputScript(script); return scriptType.getOutputScript(script);
} else { } else {
@ -517,15 +623,11 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
} }
public String getOutputDescriptor(WalletNode node) { public String getOutputDescriptor(WalletNode node) {
return getOutputDescriptor(node.getKeyPurpose(), node.getIndex());
}
public String getOutputDescriptor(KeyPurpose keyPurpose, int index) {
if(policyType == PolicyType.SINGLE) { if(policyType == PolicyType.SINGLE) {
ECKey pubKey = getPubKey(keyPurpose, index); ECKey pubKey = getPubKey(node);
return scriptType.getOutputDescriptor(pubKey); return scriptType.getOutputDescriptor(pubKey);
} else if(policyType == PolicyType.MULTI) { } else if(policyType == PolicyType.MULTI) {
List<ECKey> pubKeys = getPubKeys(keyPurpose, index); List<ECKey> pubKeys = getPubKeys(node);
Script script = ScriptType.MULTISIG.getOutputScript(defaultPolicy.getNumSignaturesRequired(), pubKeys); Script script = ScriptType.MULTISIG.getOutputScript(defaultPolicy.getNumSignaturesRequired(), pubKeys);
return scriptType.getOutputDescriptor(script); return scriptType.getOutputDescriptor(script);
} else { } else {
@ -533,10 +635,20 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
} }
} }
public List<KeyPurpose> getWalletKeyPurposes() {
return isBip47() ? List.of(KeyPurpose.RECEIVE) : KeyPurpose.DEFAULT_PURPOSES;
}
public KeyPurpose getChangeKeyPurpose() {
return isBip47() ? KeyPurpose.RECEIVE : KeyPurpose.CHANGE;
}
public Map<WalletNode, Set<BlockTransactionHashIndex>> getWalletNodes() { public Map<WalletNode, Set<BlockTransactionHashIndex>> getWalletNodes() {
Map<WalletNode, Set<BlockTransactionHashIndex>> walletNodes = new LinkedHashMap<>(); Map<WalletNode, Set<BlockTransactionHashIndex>> walletNodes = new LinkedHashMap<>();
getNode(KeyPurpose.RECEIVE).getChildren().forEach(childNode -> walletNodes.put(childNode, childNode.getTransactionOutputs())); for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
getNode(KeyPurpose.CHANGE).getChildren().forEach(childNode -> walletNodes.put(childNode, childNode.getTransactionOutputs())); getNode(keyPurpose).getChildren().forEach(childNode -> walletNodes.put(childNode, childNode.getTransactionOutputs()));
}
return walletNodes; return walletNodes;
} }
@ -546,8 +658,10 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
public Map<Address, WalletNode> getWalletAddresses() { public Map<Address, WalletNode> getWalletAddresses() {
Map<Address, WalletNode> walletAddresses = new LinkedHashMap<>(); Map<Address, WalletNode> walletAddresses = new LinkedHashMap<>();
getWalletAddresses(walletAddresses, getNode(KeyPurpose.RECEIVE)); for(KeyPurpose keyPurpose : getWalletKeyPurposes()) {
getWalletAddresses(walletAddresses, getNode(KeyPurpose.CHANGE)); getWalletAddresses(walletAddresses, getNode(keyPurpose));
}
return walletAddresses; return walletAddresses;
} }
@ -562,10 +676,18 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
} }
public Map<Script, WalletNode> getWalletOutputScripts() { public Map<Script, WalletNode> getWalletOutputScripts() {
return getWalletOutputScripts(KeyPurpose.RECEIVE, KeyPurpose.CHANGE); return getWalletOutputScripts(getWalletKeyPurposes());
} }
public Map<Script, WalletNode> getWalletOutputScripts(KeyPurpose... keyPurposes) { public Map<Script, WalletNode> getWalletOutputScripts(KeyPurpose keyPurpose) {
if(!getWalletKeyPurposes().contains(keyPurpose)) {
return Collections.emptyMap();
}
return getWalletOutputScripts(List.of(keyPurpose));
}
private Map<Script, WalletNode> getWalletOutputScripts(List<KeyPurpose> keyPurposes) {
Map<Script, WalletNode> walletOutputScripts = new LinkedHashMap<>(); Map<Script, WalletNode> walletOutputScripts = new LinkedHashMap<>();
for(KeyPurpose keyPurpose : keyPurposes) { for(KeyPurpose keyPurpose : keyPurposes) {
getWalletOutputScripts(walletOutputScripts, getNode(keyPurpose)); getWalletOutputScripts(walletOutputScripts, getNode(keyPurpose));
@ -593,8 +715,10 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
public Map<BlockTransactionHashIndex, WalletNode> getWalletTxos() { public Map<BlockTransactionHashIndex, WalletNode> getWalletTxos() {
Map<BlockTransactionHashIndex, WalletNode> walletTxos = new TreeMap<>(); Map<BlockTransactionHashIndex, WalletNode> walletTxos = new TreeMap<>();
getWalletTxos(walletTxos, getNode(KeyPurpose.RECEIVE)); for(KeyPurpose keyPurpose : getWalletKeyPurposes()) {
getWalletTxos(walletTxos, getNode(KeyPurpose.CHANGE)); getWalletTxos(walletTxos, getNode(keyPurpose));
}
return walletTxos; return walletTxos;
} }
@ -612,8 +736,10 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
public Map<BlockTransactionHashIndex, WalletNode> getWalletUtxos(boolean includeSpentMempoolOutputs) { public Map<BlockTransactionHashIndex, WalletNode> getWalletUtxos(boolean includeSpentMempoolOutputs) {
Map<BlockTransactionHashIndex, WalletNode> walletUtxos = new TreeMap<>(); Map<BlockTransactionHashIndex, WalletNode> walletUtxos = new TreeMap<>();
getWalletUtxos(walletUtxos, getNode(KeyPurpose.RECEIVE), includeSpentMempoolOutputs); for(KeyPurpose keyPurpose : getWalletKeyPurposes()) {
getWalletUtxos(walletUtxos, getNode(KeyPurpose.CHANGE), includeSpentMempoolOutputs); getWalletUtxos(walletUtxos, getNode(keyPurpose), includeSpentMempoolOutputs);
}
return walletUtxos; return walletUtxos;
} }
@ -785,7 +911,7 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
} }
for(int i = 1; i < numSets; i+=2) { for(int i = 1; i < numSets; i+=2) {
WalletNode mixNode = getFreshNode(KeyPurpose.CHANGE); WalletNode mixNode = getFreshNode(getChangeKeyPurpose());
txExcludedChangeNodes.add(mixNode); txExcludedChangeNodes.add(mixNode);
Payment fakeMixPayment = new Payment(getAddress(mixNode), ".." + mixNode + " (Fake Mix)", totalPaymentAmount, false); Payment fakeMixPayment = new Payment(getAddress(mixNode), ".." + mixNode + " (Fake Mix)", totalPaymentAmount, false);
fakeMixPayment.setType(Payment.Type.FAKE_MIX); fakeMixPayment.setType(Payment.Type.FAKE_MIX);
@ -842,9 +968,9 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
long costOfChangeAmt = getCostOfChange(noChangeFeeRate, longTermFeeRate); long costOfChangeAmt = getCostOfChange(noChangeFeeRate, longTermFeeRate);
if(setChangeAmts.stream().allMatch(amt -> amt > costOfChangeAmt) || (numSets > 1 && differenceAmt / transaction.getVirtualSize() > noChangeFeeRate * 2)) { if(setChangeAmts.stream().allMatch(amt -> amt > costOfChangeAmt) || (numSets > 1 && differenceAmt / transaction.getVirtualSize() > noChangeFeeRate * 2)) {
//Change output is required, determine new fee once change output has been added //Change output is required, determine new fee once change output has been added
WalletNode changeNode = getFreshNode(KeyPurpose.CHANGE); WalletNode changeNode = getFreshNode(getChangeKeyPurpose());
while(txExcludedChangeNodes.contains(changeNode)) { while(txExcludedChangeNodes.contains(changeNode)) {
changeNode = getFreshNode(KeyPurpose.CHANGE, changeNode); changeNode = getFreshNode(getChangeKeyPurpose(), changeNode);
} }
TransactionOutput changeOutput = new TransactionOutput(transaction, setChangeAmts.iterator().next(), getOutputScript(changeNode)); TransactionOutput changeOutput = new TransactionOutput(transaction, setChangeAmts.iterator().next(), getOutputScript(changeNode));
double changeVSize = noChangeVSize + changeOutput.getLength() * numSets; double changeVSize = noChangeVSize + changeOutput.getLength() * numSets;
@ -860,7 +986,7 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
for(Long setChangeAmt : setChangeAmts) { for(Long setChangeAmt : setChangeAmts) {
transaction.addOutput(setChangeAmt, getOutputScript(changeNode)); transaction.addOutput(setChangeAmt, getOutputScript(changeNode));
changeMap.put(changeNode, setChangeAmt); changeMap.put(changeNode, setChangeAmt);
changeNode = getFreshNode(KeyPurpose.CHANGE, changeNode); changeNode = getFreshNode(getChangeKeyPurpose(), changeNode);
} }
if(setChangeAmts.stream().anyMatch(amt -> amt < costOfChangeAmt)) { if(setChangeAmts.stream().anyMatch(amt -> amt < costOfChangeAmt)) {
@ -958,7 +1084,9 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
total += selectedInputs.stream().mapToLong(BlockTransactionHashIndex::getValue).sum(); total += selectedInputs.stream().mapToLong(BlockTransactionHashIndex::getValue).sum();
Map<BlockTransactionHashIndex, WalletNode> selectedInputsMap = new LinkedHashMap<>(); Map<BlockTransactionHashIndex, WalletNode> selectedInputsMap = new LinkedHashMap<>();
List<BlockTransactionHashIndex> shuffledInputs = new ArrayList<>(selectedInputs); List<BlockTransactionHashIndex> shuffledInputs = new ArrayList<>(selectedInputs);
Collections.shuffle(shuffledInputs); if(utxoSelector.shuffleInputs()) {
Collections.shuffle(shuffledInputs);
}
for(BlockTransactionHashIndex shuffledInput : shuffledInputs) { for(BlockTransactionHashIndex shuffledInput : shuffledInputs) {
selectedInputsMap.put(shuffledInput, utxos.get(shuffledInput)); selectedInputsMap.put(shuffledInput, utxos.get(shuffledInput));
} }
@ -976,8 +1104,10 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
private List<OutputGroup> getGroupedUtxos(List<UtxoFilter> utxoFilters, double feeRate, double longTermFeeRate, boolean groupByAddress, boolean includeSpentMempoolOutputs) { private List<OutputGroup> getGroupedUtxos(List<UtxoFilter> utxoFilters, double feeRate, double longTermFeeRate, boolean groupByAddress, boolean includeSpentMempoolOutputs) {
List<OutputGroup> outputGroups = new ArrayList<>(); List<OutputGroup> outputGroups = new ArrayList<>();
getGroupedUtxos(outputGroups, getNode(KeyPurpose.RECEIVE), utxoFilters, feeRate, longTermFeeRate, groupByAddress, includeSpentMempoolOutputs); for(KeyPurpose keyPurpose : getWalletKeyPurposes()) {
getGroupedUtxos(outputGroups, getNode(KeyPurpose.CHANGE), utxoFilters, feeRate, longTermFeeRate, groupByAddress, includeSpentMempoolOutputs); getGroupedUtxos(outputGroups, getNode(keyPurpose), utxoFilters, feeRate, longTermFeeRate, groupByAddress, includeSpentMempoolOutputs);
}
return outputGroups; return outputGroups;
} }
@ -1527,9 +1657,9 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
return copy; return copy;
} }
public boolean containsPrivateKeys() { public boolean containsMasterPrivateKeys() {
for(Keystore keystore : keystores) { for(Keystore keystore : keystores) {
if(keystore.hasPrivateKey()) { if(keystore.hasMasterPrivateKey()) {
return true; return true;
} }
} }

View file

@ -14,5 +14,6 @@ open module com.sparrowwallet.drongo {
exports com.sparrowwallet.drongo.wallet; exports com.sparrowwallet.drongo.wallet;
exports com.sparrowwallet.drongo.policy; exports com.sparrowwallet.drongo.policy;
exports com.sparrowwallet.drongo.uri; exports com.sparrowwallet.drongo.uri;
exports com.sparrowwallet.drongo.bip47;
exports org.bitcoin; exports org.bitcoin;
} }

View file

@ -47,7 +47,7 @@ public class OutputDescriptorTest {
@Test @Test
public void masterP2PKH() { public void masterP2PKH() {
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("pkh([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)"); OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("pkh([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)");
Assert.assertEquals("pkh([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)", descriptor.toString()); Assert.assertEquals("pkh([d34db33f/44h/0h/0h]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)", descriptor.toString());
ExtendedKey extendedPublicKey = descriptor.getSingletonExtendedPublicKey(); ExtendedKey extendedPublicKey = descriptor.getSingletonExtendedPublicKey();
KeyDerivation derivation = descriptor.getKeyDerivation(extendedPublicKey); KeyDerivation derivation = descriptor.getKeyDerivation(extendedPublicKey);
Assert.assertEquals("d34db33f", derivation.getMasterFingerprint()); Assert.assertEquals("d34db33f", derivation.getMasterFingerprint());
@ -58,7 +58,7 @@ public class OutputDescriptorTest {
@Test @Test
public void singleP2SH_P2WPKH() { public void singleP2SH_P2WPKH() {
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("sh(wpkh([f09a3b29/49h/0h/0h]xpub6CjUWYtkq9KT1zkM5NPMxoJTCMm8JSFw7JPyMG6YLBzv5AsCTkASnsVyJhqL1aaqF5XSsFinHK3FDi8RoeEWcTG3DQA2TjqrZ6HJtatYbsU/0/*))"); OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("sh(wpkh([f09a3b29/49h/0h/0h]xpub6CjUWYtkq9KT1zkM5NPMxoJTCMm8JSFw7JPyMG6YLBzv5AsCTkASnsVyJhqL1aaqF5XSsFinHK3FDi8RoeEWcTG3DQA2TjqrZ6HJtatYbsU/0/*))");
Assert.assertEquals("sh(wpkh([f09a3b29/49'/0'/0']xpub6CjUWYtkq9KT1zkM5NPMxoJTCMm8JSFw7JPyMG6YLBzv5AsCTkASnsVyJhqL1aaqF5XSsFinHK3FDi8RoeEWcTG3DQA2TjqrZ6HJtatYbsU/0/*))", descriptor.toString()); Assert.assertEquals("sh(wpkh([f09a3b29/49h/0h/0h]xpub6CjUWYtkq9KT1zkM5NPMxoJTCMm8JSFw7JPyMG6YLBzv5AsCTkASnsVyJhqL1aaqF5XSsFinHK3FDi8RoeEWcTG3DQA2TjqrZ6HJtatYbsU/0/*))", descriptor.toString());
ExtendedKey extendedPublicKey = descriptor.getSingletonExtendedPublicKey(); ExtendedKey extendedPublicKey = descriptor.getSingletonExtendedPublicKey();
KeyDerivation derivation = descriptor.getKeyDerivation(extendedPublicKey); KeyDerivation derivation = descriptor.getKeyDerivation(extendedPublicKey);
Assert.assertEquals("f09a3b29", derivation.getMasterFingerprint()); Assert.assertEquals("f09a3b29", derivation.getMasterFingerprint());
@ -95,7 +95,7 @@ public class OutputDescriptorTest {
@Test @Test
public void testChecksum() { public void testChecksum() {
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjg09x5t"); OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("sh(multi(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjg09x5t");
Assert.assertEquals("sh(sortedmulti(2,[00000000/111'/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#s66h0xn6", descriptor.toString(true)); Assert.assertEquals("sh(sortedmulti(2,[00000000/111h/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#vqfgjk5v", descriptor.toString(true));
} }
@Test(expected = IllegalArgumentException.class) @Test(expected = IllegalArgumentException.class)

View file

@ -0,0 +1,171 @@
package com.sparrowwallet.drongo.bip47;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.DeterministicKey;
import com.sparrowwallet.drongo.crypto.DumpedPrivateKey;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.policy.Policy;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.wallet.*;
import org.junit.Assert;
import org.junit.Test;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.spec.InvalidKeySpecException;
import java.util.List;
public class PaymentCodeTest {
@Test
public void testNotificationAddress() throws InvalidPaymentCodeException, InvalidKeySpecException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, MnemonicException {
PaymentCode alicePaymentCode = new PaymentCode("PM8TJTLJbPRGxSbc8EJi42Wrr6QbNSaSSVJ5Y3E4pbCYiTHUskHg13935Ubb7q8tx9GVbh2UuRnBc3WSyJHhUrw8KhprKnn9eDznYGieTzFcwQRya4GA");
Address aliceNotificationAddress = alicePaymentCode.getNotificationAddress();
Assert.assertEquals("1JDdmqFLhpzcUwPeinhJbUPw4Co3aWLyzW", aliceNotificationAddress.toString());
ECKey alicePrivKey = DumpedPrivateKey.fromBase58("Kx983SRhAZpAhj7Aac1wUXMJ6XZeyJKqCxJJ49dxEbYCT4a1ozRD").getKey();
byte[] alicePayload = alicePaymentCode.getPayload();
Assert.assertEquals("010002b85034fb08a8bfefd22848238257b252721454bbbfba2c3667f168837ea2cdad671af9f65904632e2dcc0c6ad314e11d53fc82fa4c4ea27a4a14eccecc478fee00000000000000000000000000", Utils.bytesToHex(alicePayload));
PaymentCode paymentCodeBob = new PaymentCode("PM8TJS2JxQ5ztXUpBBRnpTbcUXbUHy2T1abfrb3KkAAtMEGNbey4oumH7Hc578WgQJhPjBxteQ5GHHToTYHE3A1w6p7tU6KSoFmWBVbFGjKPisZDbP97");
ECKey bobNotificationPubKey = paymentCodeBob.getNotificationKey();
Assert.assertEquals("024ce8e3b04ea205ff49f529950616c3db615b1e37753858cc60c1ce64d17e2ad8", Utils.bytesToHex(bobNotificationPubKey.getPubKey()));
TransactionOutPoint transactionOutPoint = new TransactionOutPoint(Sha256Hash.wrapReversed(Utils.hexToBytes("86f411ab1c8e70ae8a0795ab7a6757aea6e4d5ae1826fc7b8f00c597d500609c")), 1);
Assert.assertEquals("86f411ab1c8e70ae8a0795ab7a6757aea6e4d5ae1826fc7b8f00c597d500609c01000000", Utils.bytesToHex(transactionOutPoint.bitcoinSerialize()));
SecretPoint secretPoint = new SecretPoint(alicePrivKey.getPrivKeyBytes(), bobNotificationPubKey.getPubKey());
Assert.assertEquals("736a25d9250238ad64ed5da03450c6a3f4f8f4dcdf0b58d1ed69029d76ead48d", Utils.bytesToHex(secretPoint.ECDHSecretAsBytes()));
byte[] blindingMask = PaymentCode.getMask(secretPoint.ECDHSecretAsBytes(), transactionOutPoint.bitcoinSerialize());
Assert.assertEquals("be6e7a4256cac6f4d4ed4639b8c39c4cb8bece40010908e70d17ea9d77b4dc57f1da36f2d6641ccb37cf2b9f3146686462e0fa3161ae74f88c0afd4e307adbd5", Utils.bytesToHex(blindingMask));
byte[] blindedPaymentCode = PaymentCode.blind(alicePayload, blindingMask);
Assert.assertEquals("010002063e4eb95e62791b06c50e1a3a942e1ecaaa9afbbeb324d16ae6821e091611fa96c0cf048f607fe51a0327f5e2528979311c78cb2de0d682c61e1180fc3d543b00000000000000000000000000", Utils.bytesToHex(blindedPaymentCode));
Transaction transaction = new Transaction();
List<ScriptChunk> inputChunks = List.of(ScriptChunk.fromData(Utils.hexToBytes("3045022100ac8c6dbc482c79e86c18928a8b364923c774bfdbd852059f6b3778f2319b59a7022029d7cc5724e2f41ab1fcfc0ba5a0d4f57ca76f72f19530ba97c860c70a6bf0a801")), ScriptChunk.fromData(alicePrivKey.getPubKey()));
transaction.addInput(transactionOutPoint.getHash(), transactionOutPoint.getIndex(), new Script(inputChunks));
transaction.addOutput(10000, paymentCodeBob.getNotificationAddress());
List<ScriptChunk> opReturnChunks = List.of(ScriptChunk.fromOpcode(ScriptOpCodes.OP_RETURN), ScriptChunk.fromData(blindedPaymentCode));
transaction.addOutput(10000, new Script(opReturnChunks));
Assert.assertEquals("010000000186f411ab1c8e70ae8a0795ab7a6757aea6e4d5ae1826fc7b8f00c597d500609c010000006b483045022100ac8c6dbc482c79e86c18928a8b364923c774bfdb" +
"d852059f6b3778f2319b59a7022029d7cc5724e2f41ab1fcfc0ba5a0d4f57ca76f72f19530ba97c860c70a6bf0a801210272d83d8a1fa323feab1c085157a0791b46eba34afb8bfbfaeb3a3fcc3f2" +
"c9ad8ffffffff0210270000000000001976a9148066a8e7ee82e5c5b9b7dc1765038340dc5420a988ac1027000000000000536a4c50010002063e4eb95e62791b06c50e1a3a942e1ecaaa9afbbeb3" +
"24d16ae6821e091611fa96c0cf048f607fe51a0327f5e2528979311c78cb2de0d682c61e1180fc3d543b0000000000000000000000000000000000", Utils.bytesToHex(transaction.bitcoinSerialize()));
Assert.assertEquals("9414f1681fb1255bd168a806254321a837008dd4480c02226063183deb100204", transaction.getTxId().toString());
ECKey alicePubKey = ECKey.fromPublicOnly(transaction.getInputs().get(0).getScriptSig().getChunks().get(1).data);
Assert.assertArrayEquals(alicePubKey.getPubKey(), alicePrivKey.getPubKey());
DeterministicSeed bobSeed = new DeterministicSeed("reward upper indicate eight swift arch injury crystal super wrestle already dentist", "", 0, DeterministicSeed.Type.BIP39);
Keystore bobKeystore = Keystore.fromSeed(bobSeed, List.of(new ChildNumber(47, true), ChildNumber.ZERO_HARDENED, ChildNumber.ZERO_HARDENED));
ECKey bobNotificationPrivKey = bobKeystore.getBip47ExtendedPrivateKey().getKey(List.of(ChildNumber.ZERO_HARDENED, new ChildNumber(0)));
SecretPoint bobSecretPoint = new SecretPoint(bobNotificationPrivKey.getPrivKeyBytes(), alicePubKey.getPubKey());
Assert.assertEquals("736a25d9250238ad64ed5da03450c6a3f4f8f4dcdf0b58d1ed69029d76ead48d", Utils.bytesToHex(bobSecretPoint.ECDHSecretAsBytes()));
byte[] bobBlindingMask = PaymentCode.getMask(secretPoint.ECDHSecretAsBytes(), transaction.getInputs().get(0).getOutpoint().bitcoinSerialize());
Assert.assertEquals("be6e7a4256cac6f4d4ed4639b8c39c4cb8bece40010908e70d17ea9d77b4dc57f1da36f2d6641ccb37cf2b9f3146686462e0fa3161ae74f88c0afd4e307adbd5", Utils.bytesToHex(bobBlindingMask));
PaymentCode unblindedPaymentCode = new PaymentCode(PaymentCode.blind(transaction.getOutputs().get(1).getScript().getChunks().get(1).data, blindingMask));
Assert.assertEquals(alicePaymentCode, unblindedPaymentCode);
PaymentCode unblindedPaymentCode2 = PaymentCode.getPaymentCode(transaction, bobKeystore);
Assert.assertEquals(alicePaymentCode, unblindedPaymentCode2);
}
@Test
public void testFromSeed() throws MnemonicException {
DeterministicSeed aliceSeed = new DeterministicSeed("response seminar brave tip suit recall often sound stick owner lottery motion", "", 0, DeterministicSeed.Type.BIP39);
Keystore aliceKeystore = Keystore.fromSeed(aliceSeed, List.of(new ChildNumber(47, true), ChildNumber.ZERO_HARDENED, ChildNumber.ZERO_HARDENED));
DeterministicKey bip47PubKey = aliceKeystore.getExtendedPublicKey().getKey();
PaymentCode alicePaymentCode = new PaymentCode(bip47PubKey.getPubKey(), bip47PubKey.getChainCode());
Assert.assertEquals("PM8TJTLJbPRGxSbc8EJi42Wrr6QbNSaSSVJ5Y3E4pbCYiTHUskHg13935Ubb7q8tx9GVbh2UuRnBc3WSyJHhUrw8KhprKnn9eDznYGieTzFcwQRya4GA", alicePaymentCode.toString());
}
@Test
public void testPaymentAddress() throws MnemonicException, InvalidPaymentCodeException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, NoSuchProviderException, NotSecp256k1Exception {
DeterministicSeed seed = new DeterministicSeed("response seminar brave tip suit recall often sound stick owner lottery motion", "", 0, DeterministicSeed.Type.BIP39);
Keystore keystore = Keystore.fromSeed(seed, List.of(new ChildNumber(47, true), ChildNumber.ZERO_HARDENED, ChildNumber.ZERO_HARDENED));
DeterministicKey privateKey = keystore.getExtendedPrivateKey().getKey(List.of(ChildNumber.ZERO_HARDENED, ChildNumber.ZERO));
PaymentCode paymentCodeBob = new PaymentCode("PM8TJS2JxQ5ztXUpBBRnpTbcUXbUHy2T1abfrb3KkAAtMEGNbey4oumH7Hc578WgQJhPjBxteQ5GHHToTYHE3A1w6p7tU6KSoFmWBVbFGjKPisZDbP97");
PaymentAddress paymentAddress0 = new PaymentAddress(paymentCodeBob, 0, privateKey.getPrivKeyBytes());
ECKey sendKey0 = paymentAddress0.getSendECKey();
Assert.assertEquals("141fi7TY3h936vRUKh1qfUZr8rSBuYbVBK", ScriptType.P2PKH.getAddress(sendKey0).toString());
PaymentAddress paymentAddress1 = new PaymentAddress(paymentCodeBob, 1, privateKey.getPrivKeyBytes());
ECKey sendKey1 = paymentAddress1.getSendECKey();
Assert.assertEquals("12u3Uued2fuko2nY4SoSFGCoGLCBUGPkk6", ScriptType.P2PKH.getAddress(sendKey1).toString());
}
@Test
public void testChildWallet() throws MnemonicException, InvalidPaymentCodeException {
DeterministicSeed aliceSeed = new DeterministicSeed("response seminar brave tip suit recall often sound stick owner lottery motion", "", 0, DeterministicSeed.Type.BIP39);
Wallet aliceWallet = new Wallet();
aliceWallet.setPolicyType(PolicyType.SINGLE);
aliceWallet.setScriptType(ScriptType.P2PKH);
aliceWallet.getKeystores().add(Keystore.fromSeed(aliceSeed, aliceWallet.getScriptType().getDefaultDerivation()));
aliceWallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2PKH, aliceWallet.getKeystores(), 1));
PaymentCode paymentCodeBob = new PaymentCode("PM8TJS2JxQ5ztXUpBBRnpTbcUXbUHy2T1abfrb3KkAAtMEGNbey4oumH7Hc578WgQJhPjBxteQ5GHHToTYHE3A1w6p7tU6KSoFmWBVbFGjKPisZDbP97");
Wallet aliceBip47Wallet = aliceWallet.addChildWallet(paymentCodeBob, ScriptType.P2PKH);
PaymentCode paymentCodeAlice = aliceBip47Wallet.getKeystores().get(0).getPaymentCode();
Assert.assertEquals(aliceWallet.getPaymentCode(), aliceBip47Wallet.getPaymentCode());
Assert.assertEquals("PM8TJTLJbPRGxSbc8EJi42Wrr6QbNSaSSVJ5Y3E4pbCYiTHUskHg13935Ubb7q8tx9GVbh2UuRnBc3WSyJHhUrw8KhprKnn9eDznYGieTzFcwQRya4GA", paymentCodeAlice.toString());
Assert.assertEquals("1JDdmqFLhpzcUwPeinhJbUPw4Co3aWLyzW", paymentCodeAlice.getNotificationAddress().toString());
WalletNode sendNode0 = aliceBip47Wallet.getFreshNode(KeyPurpose.SEND);
Address address0 = aliceBip47Wallet.getAddress(sendNode0);
Assert.assertEquals("141fi7TY3h936vRUKh1qfUZr8rSBuYbVBK", address0.toString());
WalletNode sendNode1 = aliceBip47Wallet.getFreshNode(KeyPurpose.SEND, sendNode0);
Address address1 = aliceBip47Wallet.getAddress(sendNode1);
Assert.assertEquals("12u3Uued2fuko2nY4SoSFGCoGLCBUGPkk6", address1.toString());
WalletNode sendNode2 = aliceBip47Wallet.getFreshNode(KeyPurpose.SEND, sendNode1);
Address address2 = aliceBip47Wallet.getAddress(sendNode2);
Assert.assertEquals("1FsBVhT5dQutGwaPePTYMe5qvYqqjxyftc", address2.toString());
DeterministicSeed bobSeed = new DeterministicSeed("reward upper indicate eight swift arch injury crystal super wrestle already dentist", "", 0, DeterministicSeed.Type.BIP39);
Wallet bobWallet = new Wallet();
bobWallet.setPolicyType(PolicyType.SINGLE);
bobWallet.setScriptType(ScriptType.P2PKH);
bobWallet.getKeystores().add(Keystore.fromSeed(bobSeed, bobWallet.getScriptType().getDefaultDerivation()));
bobWallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2PKH, bobWallet.getKeystores(), 1));
Wallet bobBip47Wallet = bobWallet.addChildWallet(paymentCodeAlice, ScriptType.P2PKH);
Assert.assertEquals(paymentCodeBob.toString(), bobBip47Wallet.getKeystores().get(0).getPaymentCode().toString());
Assert.assertEquals("1ChvUUvht2hUQufHBXF8NgLhW8SwE2ecGV", paymentCodeBob.getNotificationAddress().toString());
WalletNode receiveNode0 = bobBip47Wallet.getFreshNode(KeyPurpose.RECEIVE);
Address receiveAddress0 = bobBip47Wallet.getAddress(receiveNode0);
Assert.assertEquals("141fi7TY3h936vRUKh1qfUZr8rSBuYbVBK", receiveAddress0.toString());
WalletNode receiveNode1 = bobBip47Wallet.getFreshNode(KeyPurpose.RECEIVE, receiveNode0);
Address receiveAddress1 = bobBip47Wallet.getAddress(receiveNode1);
Assert.assertEquals("12u3Uued2fuko2nY4SoSFGCoGLCBUGPkk6", receiveAddress1.toString());
WalletNode receiveNode2 = bobBip47Wallet.getFreshNode(KeyPurpose.RECEIVE, receiveNode1);
Address receiveAddress2 = bobBip47Wallet.getAddress(receiveNode2);
Assert.assertEquals("1FsBVhT5dQutGwaPePTYMe5qvYqqjxyftc", receiveAddress2.toString());
ECKey privKey0 = bobWallet.getKeystores().get(0).getKey(receiveNode0);
ECKey pubKey0 = bobWallet.getKeystores().get(0).getPubKey(receiveNode0);
Assert.assertArrayEquals(privKey0.getPubKey(), pubKey0.getPubKey());
ECKey privKey1 = bobWallet.getKeystores().get(0).getKey(receiveNode1);
ECKey pubKey1 = bobWallet.getKeystores().get(0).getPubKey(receiveNode1);
Assert.assertArrayEquals(privKey1.getPubKey(), pubKey1.getPubKey());
}
}

View file

@ -19,12 +19,6 @@ public class PSBTTest {
PSBT.fromString(psbt); PSBT.fromString(psbt);
} }
@Test(expected = PSBTParseException.class)
public void missingOutputs() throws PSBTParseException {
String psbt = "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAA==";
PSBT.fromString(psbt);
}
@Test(expected = PSBTParseException.class) @Test(expected = PSBTParseException.class)
public void unsignedTxWithScriptSig() throws PSBTParseException { public void unsignedTxWithScriptSig() throws PSBTParseException {
String psbt = "cHNidP8BAP0KAQIAAAACqwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QAAAAAakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpL+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAABASAA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHhwEEFgAUhdE1N/LiZUBaNNuvqePdoB+4IwgAAAA="; String psbt = "cHNidP8BAP0KAQIAAAACqwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QAAAAAakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpL+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAABASAA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHhwEEFgAUhdE1N/LiZUBaNNuvqePdoB+4IwgAAAA=";

View file

@ -104,8 +104,8 @@ public class WalletTest {
wallet.getKeystores().add(keystore); wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2PKH, wallet.getKeystores(), 1)); wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2PKH, wallet.getKeystores(), 1));
Assert.assertEquals("12kTQjuWDp7Uu6PwY6CsS1KLTt3d1DBHZa", wallet.getAddress(KeyPurpose.RECEIVE, 0).toString()); Assert.assertEquals("12kTQjuWDp7Uu6PwY6CsS1KLTt3d1DBHZa", wallet.getAddress(new WalletNode(KeyPurpose.RECEIVE, 0)).toString());
Assert.assertEquals("1HbQwQCitHQxVtP39isXmUdHx7hQCZovrK", wallet.getAddress(KeyPurpose.RECEIVE, 1).toString()); Assert.assertEquals("1HbQwQCitHQxVtP39isXmUdHx7hQCZovrK", wallet.getAddress(new WalletNode(KeyPurpose.RECEIVE, 1)).toString());
} }
@Test @Test
@ -119,8 +119,8 @@ public class WalletTest {
wallet.getKeystores().add(keystore); wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2SH_P2WPKH, wallet.getKeystores(), 1)); wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2SH_P2WPKH, wallet.getKeystores(), 1));
Assert.assertEquals("3NZLE4TntsjtcZ5MbrfxwtYo9meBVybVQj", wallet.getAddress(KeyPurpose.RECEIVE, 0).toString()); Assert.assertEquals("3NZLE4TntsjtcZ5MbrfxwtYo9meBVybVQj", wallet.getAddress(new WalletNode(KeyPurpose.RECEIVE, 0)).toString());
Assert.assertEquals("32YBBuRsp8XTeLx4T6BmD2L4nANGaNDkSg", wallet.getAddress(KeyPurpose.RECEIVE, 1).toString()); Assert.assertEquals("32YBBuRsp8XTeLx4T6BmD2L4nANGaNDkSg", wallet.getAddress(new WalletNode(KeyPurpose.RECEIVE, 1)).toString());
} }
@Test @Test
@ -134,8 +134,8 @@ public class WalletTest {
wallet.getKeystores().add(keystore); wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2WPKH, wallet.getKeystores(), 1)); wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2WPKH, wallet.getKeystores(), 1));
Assert.assertEquals("bc1quvxdut936uswuxwxrk6nvjmgwxh463r0fjwn55", wallet.getAddress(KeyPurpose.RECEIVE, 0).toString()); Assert.assertEquals("bc1quvxdut936uswuxwxrk6nvjmgwxh463r0fjwn55", wallet.getAddress(new WalletNode(KeyPurpose.RECEIVE, 0)).toString());
Assert.assertEquals("bc1q95j2862dz7mqpraw6qdjc70gumyu5z7adgq9x9", wallet.getAddress(KeyPurpose.RECEIVE, 1).toString()); Assert.assertEquals("bc1q95j2862dz7mqpraw6qdjc70gumyu5z7adgq9x9", wallet.getAddress(new WalletNode(KeyPurpose.RECEIVE, 1)).toString());
} }
@Test @Test
@ -159,8 +159,8 @@ public class WalletTest {
wallet.getKeystores().add(keystore2); wallet.getKeystores().add(keystore2);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI, ScriptType.P2SH, wallet.getKeystores(), 2)); wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI, ScriptType.P2SH, wallet.getKeystores(), 2));
Assert.assertEquals("38kq6yz4VcYymTExQPY3gppbz38mtPLveK", wallet.getAddress(KeyPurpose.RECEIVE, 0).toString()); Assert.assertEquals("38kq6yz4VcYymTExQPY3gppbz38mtPLveK", wallet.getAddress(new WalletNode(KeyPurpose.RECEIVE, 0)).toString());
Assert.assertEquals("3EdKaNsnjBTBggWcSMRyVju6GbHWy68mAH", wallet.getAddress(KeyPurpose.CHANGE, 1).toString()); Assert.assertEquals("3EdKaNsnjBTBggWcSMRyVju6GbHWy68mAH", wallet.getAddress(new WalletNode(KeyPurpose.CHANGE, 1)).toString());
} }
@Test @Test
@ -184,8 +184,8 @@ public class WalletTest {
wallet.getKeystores().add(keystore2); wallet.getKeystores().add(keystore2);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI, ScriptType.P2SH_P2WSH, wallet.getKeystores(), 2)); wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI, ScriptType.P2SH_P2WSH, wallet.getKeystores(), 2));
Assert.assertEquals("3Mw8xqAHh8g3eBvh7q1UEUmoexqdXDK9Tf", wallet.getAddress(KeyPurpose.RECEIVE, 0).toString()); Assert.assertEquals("3Mw8xqAHh8g3eBvh7q1UEUmoexqdXDK9Tf", wallet.getAddress(new WalletNode(KeyPurpose.RECEIVE, 0)).toString());
Assert.assertEquals("35dFo1ivJ8jyHpyf42MWvnYf5LBU8Siren", wallet.getAddress(KeyPurpose.CHANGE, 1).toString()); Assert.assertEquals("35dFo1ivJ8jyHpyf42MWvnYf5LBU8Siren", wallet.getAddress(new WalletNode(KeyPurpose.CHANGE, 1)).toString());
} }
@Test @Test
@ -209,8 +209,8 @@ public class WalletTest {
wallet.getKeystores().add(keystore2); wallet.getKeystores().add(keystore2);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI, ScriptType.P2WSH, wallet.getKeystores(), 2)); wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI, ScriptType.P2WSH, wallet.getKeystores(), 2));
Assert.assertEquals("bc1q20e4vm656h5lvmngz9ztz6hjzftvh39yzngqhuqzk8qzj7tqnzaqgclrwc", wallet.getAddress(KeyPurpose.RECEIVE, 0).toString()); Assert.assertEquals("bc1q20e4vm656h5lvmngz9ztz6hjzftvh39yzngqhuqzk8qzj7tqnzaqgclrwc", wallet.getAddress(new WalletNode(KeyPurpose.RECEIVE, 0)).toString());
Assert.assertEquals("bc1q2epdx7dplwaas2jucfrzmxm8350rqh68hs6vqreysku80ye44mfqla85f2", wallet.getAddress(KeyPurpose.CHANGE, 1).toString()); Assert.assertEquals("bc1q2epdx7dplwaas2jucfrzmxm8350rqh68hs6vqreysku80ye44mfqla85f2", wallet.getAddress(new WalletNode(KeyPurpose.CHANGE, 1)).toString());
} }
@Test @Test
@ -227,6 +227,6 @@ public class WalletTest {
List<ChildNumber> derivation = List.of(keystore.getExtendedPublicKey().getKeyChildNumber(), new ChildNumber(0)); List<ChildNumber> derivation = List.of(keystore.getExtendedPublicKey().getKeyChildNumber(), new ChildNumber(0));
Assert.assertEquals("027ecc656f4b91b92881b6f07cf876cd2e42b20df7acc4df54fc3315fbb2d13e1c", Utils.bytesToHex(extendedKey.getKey(derivation).getPubKey())); Assert.assertEquals("027ecc656f4b91b92881b6f07cf876cd2e42b20df7acc4df54fc3315fbb2d13e1c", Utils.bytesToHex(extendedKey.getKey(derivation).getPubKey()));
Assert.assertEquals("bc1qarzeu6ncapyvjzdeayjq8vnzp6uvcn4eaeuuqq", wallet.getAddress(KeyPurpose.RECEIVE, 0).toString()); Assert.assertEquals("bc1qarzeu6ncapyvjzdeayjq8vnzp6uvcn4eaeuuqq", wallet.getAddress(new WalletNode(KeyPurpose.RECEIVE, 0)).toString());
} }
} }