mirror of
https://github.com/sparrowwallet/drongo.git
synced 2024-12-26 01:56:44 +00:00
add bip47 keystores
This commit is contained in:
parent
f73cabad3c
commit
7bb07ab39e
21 changed files with 1116 additions and 96 deletions
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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()) {
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
104
src/main/java/com/sparrowwallet/drongo/bip47/PaymentAddress.java
Normal file
104
src/main/java/com/sparrowwallet/drongo/bip47/PaymentAddress.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
380
src/main/java/com/sparrowwallet/drongo/bip47/PaymentCode.java
Normal file
380
src/main/java/com/sparrowwallet/drongo/bip47/PaymentCode.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
if(utxoSelector.shuffleInputs()) {
|
||||||
Collections.shuffle(shuffledInputs);
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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=";
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue