mirror of
https://github.com/sparrowwallet/drongo.git
synced 2025-11-05 11:56:38 +00:00
Merge branch 'master' into update-dec-2022
This commit is contained in:
commit
2a68d8eec0
39 changed files with 740 additions and 164 deletions
|
|
@ -59,6 +59,7 @@ dependencies {
|
||||||
exclude group: 'org.hamcrest', module: 'hamcrest-core'
|
exclude group: 'org.hamcrest', module: 'hamcrest-core'
|
||||||
}
|
}
|
||||||
testImplementation group: 'org.hamcrest', name: 'hamcrest-core', version: '2.2'
|
testImplementation group: 'org.hamcrest', name: 'hamcrest-core', version: '2.2'
|
||||||
|
testImplementation 'junit:junit:4.13.1'
|
||||||
}
|
}
|
||||||
|
|
||||||
processResources {
|
processResources {
|
||||||
|
|
|
||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
3
gradle/wrapper/gradle-wrapper.properties
vendored
3
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -1,5 +1,6 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|
|
||||||
12
gradlew
vendored
12
gradlew
vendored
|
|
@ -55,7 +55,7 @@
|
||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (3) This script is generated from the Groovy template
|
||||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
# within the Gradle project.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
|
@ -80,10 +80,10 @@ do
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
APP_NAME="Gradle"
|
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
|
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||||
|
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
@ -143,12 +143,16 @@ fi
|
||||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
case $MAX_FD in #(
|
case $MAX_FD in #(
|
||||||
max*)
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC3045
|
||||||
MAX_FD=$( ulimit -H -n ) ||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
warn "Could not query maximum file descriptor limit"
|
warn "Could not query maximum file descriptor limit"
|
||||||
esac
|
esac
|
||||||
case $MAX_FD in #(
|
case $MAX_FD in #(
|
||||||
'' | soft) :;; #(
|
'' | soft) :;; #(
|
||||||
*)
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC3045
|
||||||
ulimit -n "$MAX_FD" ||
|
ulimit -n "$MAX_FD" ||
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
esac
|
esac
|
||||||
|
|
|
||||||
1
gradlew.bat
vendored
1
gradlew.bat
vendored
|
|
@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
set DIRNAME=%~dp0
|
||||||
if "%DIRNAME%"=="" set DIRNAME=.
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
set APP_BASE_NAME=%~n0
|
set APP_BASE_NAME=%~n0
|
||||||
set APP_HOME=%DIRNAME%
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -90,8 +90,8 @@ public class ExtendedKey {
|
||||||
ChildNumber childNumber;
|
ChildNumber childNumber;
|
||||||
List<ChildNumber> path;
|
List<ChildNumber> path;
|
||||||
|
|
||||||
if(depth == 0) {
|
if(depth == 0 && !header.isPrivateKey()) {
|
||||||
//Poorly formatted extended key, add first child path element
|
//Poorly formatted public extended key, add first child path element
|
||||||
childNumber = new ChildNumber(0, false);
|
childNumber = new ChildNumber(0, false);
|
||||||
} else if ((i & ChildNumber.HARDENED_BIT) != 0) {
|
} else if ((i & ChildNumber.HARDENED_BIT) != 0) {
|
||||||
childNumber = new ChildNumber(i ^ ChildNumber.HARDENED_BIT, true); //already hardened
|
childNumber = new ChildNumber(i ^ ChildNumber.HARDENED_BIT, true); //already hardened
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ public class OutputDescriptor {
|
||||||
private static final String INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
|
private static final String INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
|
||||||
private static final String CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
private static final String CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
||||||
|
|
||||||
private static final Pattern XPUB_PATTERN = Pattern.compile("(\\[[^\\]]+\\])?(.pub[^/\\,)]{100,112})(/[/\\d*'hH<>;]+)?");
|
private static final Pattern XPUB_PATTERN = Pattern.compile("(\\[[^\\]]+\\])?(.(?:pub|prv)[^/\\,)]{100,112})(/[/\\d*'hH<>;]+)?");
|
||||||
private static final Pattern PUBKEY_PATTERN = Pattern.compile("(\\[[^\\]]+\\])?(0[23][0-9a-fA-F]{32})");
|
private static final Pattern PUBKEY_PATTERN = Pattern.compile("(\\[[^\\]]+\\])?(0[23][0-9a-fA-F]{32})");
|
||||||
private static final Pattern MULTI_PATTERN = Pattern.compile("multi\\(([\\d+])");
|
private static final Pattern MULTI_PATTERN = Pattern.compile("multi\\(([\\d+])");
|
||||||
private static final Pattern KEY_ORIGIN_PATTERN = Pattern.compile("\\[([A-Fa-f0-9]{8})([/\\d'hH]+)\\]");
|
private static final Pattern KEY_ORIGIN_PATTERN = Pattern.compile("\\[([A-Fa-f0-9]{8})([/\\d'hH]+)\\]");
|
||||||
|
|
@ -379,6 +379,13 @@ public class OutputDescriptor {
|
||||||
KeyDerivation keyDerivation = new KeyDerivation(masterFingerprint, keyDerivationPath);
|
KeyDerivation keyDerivation = new KeyDerivation(masterFingerprint, keyDerivationPath);
|
||||||
try {
|
try {
|
||||||
ExtendedKey extendedPublicKey = ExtendedKey.fromDescriptor(extPubKey);
|
ExtendedKey extendedPublicKey = ExtendedKey.fromDescriptor(extPubKey);
|
||||||
|
if(extendedPublicKey.getKey().hasPrivKey()) {
|
||||||
|
List<ChildNumber> derivation = keyDerivation.getDerivation();
|
||||||
|
int depth = derivation.size() == 0 ? scriptType.getDefaultDerivation().size() : derivation.size();
|
||||||
|
DeterministicKey prvKey = extendedPublicKey.getKey();
|
||||||
|
DeterministicKey pubKey = new DeterministicKey(prvKey.getPath(), prvKey.getChainCode(), prvKey.getPubKey(), depth, extendedPublicKey.getParentFingerprint());
|
||||||
|
extendedPublicKey = new ExtendedKey(pubKey, pubKey.getParentFingerprint(), extendedPublicKey.getKeyChildNumber());
|
||||||
|
}
|
||||||
keyDerivationMap.put(extendedPublicKey, keyDerivation);
|
keyDerivationMap.put(extendedPublicKey, keyDerivation);
|
||||||
keyChildDerivationMap.put(extendedPublicKey, childDerivationPath);
|
keyChildDerivationMap.put(extendedPublicKey, childDerivationPath);
|
||||||
} catch(ProtocolException e) {
|
} catch(ProtocolException e) {
|
||||||
|
|
@ -479,6 +486,10 @@ public class OutputDescriptor {
|
||||||
}
|
}
|
||||||
|
|
||||||
public String toString(boolean addKeyOrigin, boolean addChecksum) {
|
public String toString(boolean addKeyOrigin, boolean addChecksum) {
|
||||||
|
return toString(addKeyOrigin, true, addChecksum);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString(boolean addKeyOrigin, boolean addKey, boolean addChecksum) {
|
||||||
StringBuilder builder = new StringBuilder();
|
StringBuilder builder = new StringBuilder();
|
||||||
builder.append(scriptType.getDescriptor());
|
builder.append(scriptType.getDescriptor());
|
||||||
|
|
||||||
|
|
@ -487,14 +498,14 @@ public class OutputDescriptor {
|
||||||
StringJoiner joiner = new StringJoiner(",");
|
StringJoiner joiner = new StringJoiner(",");
|
||||||
joiner.add(Integer.toString(multisigThreshold));
|
joiner.add(Integer.toString(multisigThreshold));
|
||||||
for(ExtendedKey pubKey : sortExtendedPubKeys(extendedPublicKeys.keySet())) {
|
for(ExtendedKey pubKey : sortExtendedPubKeys(extendedPublicKeys.keySet())) {
|
||||||
String extKeyString = toString(pubKey, addKeyOrigin);
|
String extKeyString = toString(pubKey, addKeyOrigin, addKey);
|
||||||
joiner.add(extKeyString);
|
joiner.add(extKeyString);
|
||||||
}
|
}
|
||||||
builder.append(joiner.toString());
|
builder.append(joiner.toString());
|
||||||
builder.append(ScriptType.MULTISIG.getCloseDescriptor());
|
builder.append(ScriptType.MULTISIG.getCloseDescriptor());
|
||||||
} else {
|
} else {
|
||||||
ExtendedKey extendedPublicKey = getSingletonExtendedPublicKey();
|
ExtendedKey extendedPublicKey = getSingletonExtendedPublicKey();
|
||||||
builder.append(toString(extendedPublicKey, addKeyOrigin));
|
builder.append(toString(extendedPublicKey, addKeyOrigin, addKey));
|
||||||
}
|
}
|
||||||
builder.append(scriptType.getCloseDescriptor());
|
builder.append(scriptType.getCloseDescriptor());
|
||||||
|
|
||||||
|
|
@ -548,7 +559,7 @@ public class OutputDescriptor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String toString(ExtendedKey pubKey, boolean addKeyOrigin) {
|
private String toString(ExtendedKey pubKey, boolean addKeyOrigin, boolean addKey) {
|
||||||
StringBuilder keyBuilder = new StringBuilder();
|
StringBuilder keyBuilder = new StringBuilder();
|
||||||
KeyDerivation keyDerivation = extendedPublicKeys.get(pubKey);
|
KeyDerivation keyDerivation = extendedPublicKeys.get(pubKey);
|
||||||
if(keyDerivation != null && keyDerivation.getDerivationPath() != null && addKeyOrigin) {
|
if(keyDerivation != null && keyDerivation.getDerivationPath() != null && addKeyOrigin) {
|
||||||
|
|
@ -561,6 +572,7 @@ public class OutputDescriptor {
|
||||||
keyBuilder.append("]");
|
keyBuilder.append("]");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(addKey) {
|
||||||
if(pubKey != null) {
|
if(pubKey != null) {
|
||||||
keyBuilder.append(pubKey.toString());
|
keyBuilder.append(pubKey.toString());
|
||||||
}
|
}
|
||||||
|
|
@ -573,6 +585,7 @@ public class OutputDescriptor {
|
||||||
|
|
||||||
keyBuilder.append(childDerivation);
|
keyBuilder.append(childDerivation);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return keyBuilder.toString();
|
return keyBuilder.toString();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.CharsetDecoder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
|
@ -22,6 +23,7 @@ public class Utils {
|
||||||
|
|
||||||
public static final String HEX_REGEX = "^[0-9A-Fa-f]+$";
|
public static final String HEX_REGEX = "^[0-9A-Fa-f]+$";
|
||||||
public static final String BASE64_REGEX = "^[0-9A-Za-z\\\\+=/]+$";
|
public static final String BASE64_REGEX = "^[0-9A-Za-z\\\\+=/]+$";
|
||||||
|
public static final String NUMERIC_REGEX = "^-?\\d+(\\.\\d+)?$";
|
||||||
|
|
||||||
public static boolean isHex(String s) {
|
public static boolean isHex(String s) {
|
||||||
return s.matches(HEX_REGEX);
|
return s.matches(HEX_REGEX);
|
||||||
|
|
@ -31,6 +33,20 @@ public class Utils {
|
||||||
return s.matches(BASE64_REGEX);
|
return s.matches(BASE64_REGEX);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isNumber(String s) {
|
||||||
|
return s.matches(NUMERIC_REGEX);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isUtf8(byte[] bytes) {
|
||||||
|
try {
|
||||||
|
CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();
|
||||||
|
decoder.decode(java.nio.ByteBuffer.wrap(bytes));
|
||||||
|
return true;
|
||||||
|
} catch(Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static String bytesToHex(byte[] bytes) {
|
public static String bytesToHex(byte[] bytes) {
|
||||||
char[] hexChars = new char[bytes.length * 2];
|
char[] hexChars = new char[bytes.length * 2];
|
||||||
for ( int j = 0; j < bytes.length; j++ ) {
|
for ( int j = 0; j < bytes.length; j++ ) {
|
||||||
|
|
@ -128,6 +144,20 @@ public class Utils {
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static byte[] xor(byte[] a, byte[] b) {
|
||||||
|
if(a.length != b.length) {
|
||||||
|
throw new IllegalArgumentException("Invalid length for xor: " + a.length + " vs " + b.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
/** Parse 4 bytes from the byte array (starting at the offset) as unsigned 32-bit integer in little endian format. */
|
/** Parse 4 bytes from the byte array (starting at the offset) as unsigned 32-bit integer in little endian format. */
|
||||||
public static long readUint32(byte[] bytes, int offset) {
|
public static long readUint32(byte[] bytes, int offset) {
|
||||||
return (bytes[offset] & 0xffl) |
|
return (bytes[offset] & 0xffl) |
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ public class PaymentAddress {
|
||||||
private BigInteger addSecp256k1(BigInteger b1, BigInteger b2) {
|
private BigInteger addSecp256k1(BigInteger b1, BigInteger b2) {
|
||||||
BigInteger ret = b1.add(b2);
|
BigInteger ret = b1.add(b2);
|
||||||
|
|
||||||
if(ret.bitLength() > CURVE.getN().bitLength()) {
|
if(ret.compareTo(CURVE.getN()) > 0) {
|
||||||
return ret.mod(CURVE.getN());
|
return ret.mod(CURVE.getN());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,7 +83,7 @@ public class PaymentAddress {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isSecp256k1(BigInteger b) {
|
private boolean isSecp256k1(BigInteger b) {
|
||||||
return b.compareTo(BigInteger.ONE) > 0 && b.bitLength() <= CURVE.getN().bitLength();
|
return (b.compareTo(BigInteger.ONE) > 0) && (b.compareTo(CURVE.getN()) < 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private BigInteger secretPoint() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException, NotSecp256k1Exception {
|
private BigInteger secretPoint() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException, NotSecp256k1Exception {
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static com.sparrowwallet.drongo.Utils.xor;
|
||||||
|
|
||||||
public class PaymentCode {
|
public class PaymentCode {
|
||||||
private static final Logger log = LoggerFactory.getLogger(PaymentCode.class);
|
private static final Logger log = LoggerFactory.getLogger(PaymentCode.class);
|
||||||
|
|
||||||
|
|
@ -313,21 +315,6 @@ public class PaymentCode {
|
||||||
return HDKeyDerivation.createMasterPubKeyFromBytes(pubkey, chain);
|
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() {
|
public boolean isValid() {
|
||||||
try {
|
try {
|
||||||
byte[] pcodeBytes = Base58.decodeChecked(strPaymentCode);
|
byte[] pcodeBytes = Base58.decodeChecked(strPaymentCode);
|
||||||
|
|
|
||||||
170
src/main/java/com/sparrowwallet/drongo/crypto/Bip322.java
Normal file
170
src/main/java/com/sparrowwallet/drongo/crypto/Bip322.java
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
package com.sparrowwallet.drongo.crypto;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.Utils;
|
||||||
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
|
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||||
|
import com.sparrowwallet.drongo.protocol.*;
|
||||||
|
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||||
|
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
||||||
|
import com.sparrowwallet.drongo.psbt.PSBTInputSigner;
|
||||||
|
import com.sparrowwallet.drongo.psbt.PSBTSignatureException;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.SignatureException;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import static com.sparrowwallet.drongo.protocol.ScriptType.P2TR;
|
||||||
|
|
||||||
|
public class Bip322 {
|
||||||
|
public static String signMessageBip322(ScriptType scriptType, String message, ECKey privKey) {
|
||||||
|
checkScriptType(scriptType);
|
||||||
|
|
||||||
|
ECKey pubKey = ECKey.fromPublicOnly(privKey);
|
||||||
|
Address address = scriptType.getAddress(pubKey);
|
||||||
|
|
||||||
|
Transaction toSpend = getBip322ToSpend(address, message);
|
||||||
|
Transaction toSign = getBip322ToSign(toSpend);
|
||||||
|
|
||||||
|
TransactionOutput utxoOutput = toSpend.getOutputs().get(0);
|
||||||
|
|
||||||
|
PSBT psbt = new PSBT(toSign);
|
||||||
|
PSBTInput psbtInput = psbt.getPsbtInputs().get(0);
|
||||||
|
psbtInput.setWitnessUtxo(utxoOutput);
|
||||||
|
psbtInput.setSigHash(SigHash.ALL);
|
||||||
|
psbtInput.sign(scriptType.getOutputKey(privKey));
|
||||||
|
|
||||||
|
TransactionSignature signature = psbtInput.isTaproot() ? psbtInput.getTapKeyPathSignature() : psbtInput.getPartialSignature(pubKey);
|
||||||
|
|
||||||
|
Transaction finalizeTransaction = new Transaction();
|
||||||
|
TransactionInput finalizedTxInput = scriptType.addSpendingInput(finalizeTransaction, utxoOutput, pubKey, signature);
|
||||||
|
|
||||||
|
return Base64.getEncoder().encodeToString(finalizedTxInput.getWitness().toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean verifyMessageBip322(ScriptType scriptType, Address address, String message, String signatureBase64) throws SignatureException {
|
||||||
|
checkScriptType(scriptType);
|
||||||
|
|
||||||
|
if(signatureBase64.trim().isEmpty()) {
|
||||||
|
throw new SignatureException("Provided signature is empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] signatureEncoded;
|
||||||
|
try {
|
||||||
|
signatureEncoded = Base64.getDecoder().decode(signatureBase64);
|
||||||
|
} catch(IllegalArgumentException e) {
|
||||||
|
throw new SignatureException("Could not decode base64 signature", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionWitness witness;
|
||||||
|
try {
|
||||||
|
witness = new TransactionWitness(null, signatureEncoded, 0);
|
||||||
|
} catch(Exception e) {
|
||||||
|
throw new SignatureException("Provided signature is not a BIP322 simple signature.", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionSignature signature;
|
||||||
|
ECKey pubKey;
|
||||||
|
|
||||||
|
if(witness.getWitnessScript() != null) {
|
||||||
|
throw new IllegalArgumentException("Multisig signatures are not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(witness.getSignatures().isEmpty()) {
|
||||||
|
throw new SignatureException("BIP322 simple signature contains no transaction signatures.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(scriptType == ScriptType.P2WPKH) {
|
||||||
|
signature = witness.getSignatures().get(0);
|
||||||
|
if(witness.getPushes().size() <= 1) {
|
||||||
|
throw new SignatureException("BIP322 simple signature for P2WPKH script type does not contain a pubkey.");
|
||||||
|
}
|
||||||
|
pubKey = ECKey.fromPublicOnly(witness.getPushes().get(1));
|
||||||
|
|
||||||
|
if(!address.equals(scriptType.getAddress(pubKey))) {
|
||||||
|
throw new SignatureException("Provided address does not match pubkey in signature");
|
||||||
|
}
|
||||||
|
} else if(scriptType == ScriptType.P2TR) {
|
||||||
|
signature = witness.getSignatures().get(0);
|
||||||
|
pubKey = P2TR.getPublicKeyFromScript(address.getOutputScript());
|
||||||
|
} else {
|
||||||
|
throw new SignatureException(scriptType + " addresses are not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
Transaction toSpend = getBip322ToSpend(address, message);
|
||||||
|
Transaction toSign = getBip322ToSign(toSpend);
|
||||||
|
|
||||||
|
PSBT psbt = new PSBT(toSign);
|
||||||
|
PSBTInput psbtInput = psbt.getPsbtInputs().get(0);
|
||||||
|
psbtInput.setWitnessUtxo(toSpend.getOutputs().get(0));
|
||||||
|
psbtInput.setSigHash(SigHash.ALL);
|
||||||
|
|
||||||
|
if(scriptType == ScriptType.P2TR) {
|
||||||
|
psbtInput.setTapKeyPathSignature(signature);
|
||||||
|
} else {
|
||||||
|
psbtInput.getPartialSignatures().put(pubKey, signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
psbt.verifySignatures();
|
||||||
|
} catch(PSBTSignatureException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void checkScriptType(ScriptType scriptType) {
|
||||||
|
if(!scriptType.isAllowed(PolicyType.SINGLE)) {
|
||||||
|
throw new UnsupportedOperationException("Only singlesig addresses are currently supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!Arrays.asList(ScriptType.WITNESS_TYPES).contains(scriptType)) {
|
||||||
|
throw new UnsupportedOperationException("Legacy addresses are not supported for BIP322 simple signatures");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(scriptType == ScriptType.P2SH_P2WPKH) {
|
||||||
|
throw new UnsupportedOperationException("The P2SH-P2WPKH script type is not currently supported");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isSupported(ScriptType scriptType) {
|
||||||
|
return scriptType == ScriptType.P2WPKH || scriptType == P2TR;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Transaction getBip322ToSpend(Address address, String message) {
|
||||||
|
Transaction toSpend = new Transaction();
|
||||||
|
toSpend.setVersion(0);
|
||||||
|
toSpend.setLocktime(0);
|
||||||
|
|
||||||
|
List<ScriptChunk> scriptSigChunks = new ArrayList<>();
|
||||||
|
scriptSigChunks.add(ScriptChunk.fromOpcode(ScriptOpCodes.OP_0));
|
||||||
|
scriptSigChunks.add(ScriptChunk.fromData(getBip322MessageHash(message)));
|
||||||
|
Script scriptSig = new Script(scriptSigChunks);
|
||||||
|
toSpend.addInput(Sha256Hash.ZERO_HASH, 0xFFFFFFFFL, scriptSig, new TransactionWitness(toSpend, Collections.emptyList()));
|
||||||
|
toSpend.getInputs().get(0).setSequenceNumber(0L);
|
||||||
|
toSpend.addOutput(0L, address.getOutputScript());
|
||||||
|
|
||||||
|
return toSpend;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Transaction getBip322ToSign(Transaction toSpend) {
|
||||||
|
Transaction toSign = new Transaction();
|
||||||
|
toSign.setVersion(0);
|
||||||
|
toSign.setLocktime(0);
|
||||||
|
|
||||||
|
TransactionWitness witness = new TransactionWitness(toSign);
|
||||||
|
toSign.addInput(toSpend.getTxId(), 0L, new Script(new byte[0]), witness);
|
||||||
|
toSign.getInputs().get(0).setSequenceNumber(0L);
|
||||||
|
toSign.addOutput(0, new Script(List.of(ScriptChunk.fromOpcode(ScriptOpCodes.OP_RETURN))));
|
||||||
|
|
||||||
|
return toSign;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] getBip322MessageHash(String message) {
|
||||||
|
if(message == null) {
|
||||||
|
throw new IllegalArgumentException("Message cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Utils.taggedHash("BIP0322-signed-message", message.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -576,9 +576,13 @@ public class ECKey {
|
||||||
* @throws IllegalStateException if this ECKey does not have the private part.
|
* @throws IllegalStateException if this ECKey does not have the private part.
|
||||||
*/
|
*/
|
||||||
public String signMessage(String message, ScriptType scriptType) {
|
public String signMessage(String message, ScriptType scriptType) {
|
||||||
|
return signMessage(message, scriptType, this::signEcdsa);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String signMessage(String message, ScriptType scriptType, ECDSAHashSigner ecdsaHashSigner) {
|
||||||
byte[] data = formatMessageForSigning(message);
|
byte[] data = formatMessageForSigning(message);
|
||||||
Sha256Hash hash = Sha256Hash.of(data);
|
Sha256Hash hash = Sha256Hash.of(data);
|
||||||
ECDSASignature sig = signEcdsa(hash);
|
ECDSASignature sig = ecdsaHashSigner.sign(hash);
|
||||||
byte recId = findRecoveryId(hash, sig);
|
byte recId = findRecoveryId(hash, sig);
|
||||||
int headerByte = recId + getSigningTypeConstant(scriptType);
|
int headerByte = recId + getSigningTypeConstant(scriptType);
|
||||||
byte[] sigData = new byte[65]; // 1 header + 32 bytes for R + 32 bytes for S
|
byte[] sigData = new byte[65]; // 1 header + 32 bytes for R + 32 bytes for S
|
||||||
|
|
@ -627,6 +631,14 @@ public class ECKey {
|
||||||
// This is what you get back from Bouncy Castle if base64 doesn't decode :(
|
// This is what you get back from Bouncy Castle if base64 doesn't decode :(
|
||||||
throw new SignatureException("Could not decode base64", e);
|
throw new SignatureException("Could not decode base64", e);
|
||||||
}
|
}
|
||||||
|
byte[] messageBytes = formatMessageForSigning(message);
|
||||||
|
// Note that the C++ code doesn't actually seem to specify any character encoding. Presumably it's whatever
|
||||||
|
// JSON-SPIRIT hands back. Assume UTF-8 for now.
|
||||||
|
Sha256Hash messageHash = Sha256Hash.twiceOf(messageBytes);
|
||||||
|
return signedHashToKey(messageHash, signatureEncoded, electrumFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ECKey signedHashToKey(Sha256Hash messageHash, byte[] signatureEncoded, boolean electrumFormat) throws SignatureException {
|
||||||
// Parse the signature bytes into r/s and the selector value.
|
// Parse the signature bytes into r/s and the selector value.
|
||||||
if(signatureEncoded.length < 65) {
|
if(signatureEncoded.length < 65) {
|
||||||
throw new SignatureException("Signature truncated, expected 65 bytes and got " + signatureEncoded.length);
|
throw new SignatureException("Signature truncated, expected 65 bytes and got " + signatureEncoded.length);
|
||||||
|
|
@ -640,10 +652,7 @@ public class ECKey {
|
||||||
BigInteger r = new BigInteger(1, Arrays.copyOfRange(signatureEncoded, 1, 33));
|
BigInteger r = new BigInteger(1, Arrays.copyOfRange(signatureEncoded, 1, 33));
|
||||||
BigInteger s = new BigInteger(1, Arrays.copyOfRange(signatureEncoded, 33, 65));
|
BigInteger s = new BigInteger(1, Arrays.copyOfRange(signatureEncoded, 33, 65));
|
||||||
ECDSASignature sig = new ECDSASignature(r, s);
|
ECDSASignature sig = new ECDSASignature(r, s);
|
||||||
byte[] messageBytes = formatMessageForSigning(message);
|
|
||||||
// Note that the C++ code doesn't actually seem to specify any character encoding. Presumably it's whatever
|
|
||||||
// JSON-SPIRIT hands back. Assume UTF-8 for now.
|
|
||||||
Sha256Hash messageHash = Sha256Hash.of(messageBytes);
|
|
||||||
boolean compressed = false;
|
boolean compressed = false;
|
||||||
if(header >= 39) { // this is a bech32 signature
|
if(header >= 39) { // this is a bech32 signature
|
||||||
header -= 12;
|
header -= 12;
|
||||||
|
|
@ -863,4 +872,8 @@ public class ECKey {
|
||||||
throw new RuntimeException(e); // Cannot happen.
|
throw new RuntimeException(e); // Cannot happen.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface ECDSAHashSigner {
|
||||||
|
ECDSASignature sign(Sha256Hash hash);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,25 +7,36 @@ import org.bouncycastle.crypto.params.KeyParameter;
|
||||||
|
|
||||||
public class Pbkdf2KeyDeriver implements KeyDeriver, AsymmetricKeyDeriver {
|
public class Pbkdf2KeyDeriver implements KeyDeriver, AsymmetricKeyDeriver {
|
||||||
public static final int DEFAULT_ITERATION_COUNT = 1024;
|
public static final int DEFAULT_ITERATION_COUNT = 1024;
|
||||||
|
public static final int DEFAULT_KEY_SIZE = 512;
|
||||||
|
|
||||||
private final byte[] salt;
|
private final byte[] salt;
|
||||||
private final int iterationCount;
|
private final int iterationCount;
|
||||||
|
private final int keySize;
|
||||||
|
|
||||||
public static final Pbkdf2KeyDeriver DEFAULT_INSTANCE = new Pbkdf2KeyDeriver();
|
public static final Pbkdf2KeyDeriver DEFAULT_INSTANCE = new Pbkdf2KeyDeriver();
|
||||||
|
|
||||||
public Pbkdf2KeyDeriver() {
|
public Pbkdf2KeyDeriver() {
|
||||||
this.salt = new byte[0];
|
this.salt = new byte[0];
|
||||||
this.iterationCount = DEFAULT_ITERATION_COUNT;
|
this.iterationCount = DEFAULT_ITERATION_COUNT;
|
||||||
|
this.keySize = DEFAULT_KEY_SIZE;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Pbkdf2KeyDeriver(byte[] salt) {
|
public Pbkdf2KeyDeriver(byte[] salt) {
|
||||||
this.salt = salt;
|
this.salt = salt;
|
||||||
this.iterationCount = DEFAULT_ITERATION_COUNT;
|
this.iterationCount = DEFAULT_ITERATION_COUNT;
|
||||||
|
this.keySize = DEFAULT_KEY_SIZE;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Pbkdf2KeyDeriver(byte[] salt, int iterationCount) {
|
public Pbkdf2KeyDeriver(byte[] salt, int iterationCount) {
|
||||||
this.salt = salt;
|
this.salt = salt;
|
||||||
this.iterationCount = iterationCount;
|
this.iterationCount = iterationCount;
|
||||||
|
this.keySize = DEFAULT_KEY_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pbkdf2KeyDeriver(byte[] salt, int iterationCount, int keySize) {
|
||||||
|
this.salt = salt;
|
||||||
|
this.iterationCount = iterationCount;
|
||||||
|
this.keySize = keySize;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -37,7 +48,7 @@ public class Pbkdf2KeyDeriver implements KeyDeriver, AsymmetricKeyDeriver {
|
||||||
public Key deriveKey(CharSequence password) throws KeyCrypterException {
|
public Key deriveKey(CharSequence password) throws KeyCrypterException {
|
||||||
PKCS5S2ParametersGenerator gen = new PKCS5S2ParametersGenerator(new SHA512Digest());
|
PKCS5S2ParametersGenerator gen = new PKCS5S2ParametersGenerator(new SHA512Digest());
|
||||||
gen.init(SecureString.toBytesUTF8(password), salt, iterationCount);
|
gen.init(SecureString.toBytesUTF8(password), salt, iterationCount);
|
||||||
byte[] keyBytes = ((KeyParameter)gen.generateDerivedParameters(512)).getKey();
|
byte[] keyBytes = ((KeyParameter)gen.generateDerivedParameters(keySize)).getKey();
|
||||||
return new Key(keyBytes, salt, getDeriverType());
|
return new Key(keyBytes, salt, getDeriverType());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import java.util.regex.Pattern;
|
||||||
public class Miniscript {
|
public class Miniscript {
|
||||||
private static final Pattern SINGLE_PATTERN = Pattern.compile("pkh?\\(");
|
private static final Pattern SINGLE_PATTERN = Pattern.compile("pkh?\\(");
|
||||||
private static final Pattern TAPROOT_PATTERN = Pattern.compile("tr\\(");
|
private static final Pattern TAPROOT_PATTERN = Pattern.compile("tr\\(");
|
||||||
private static final Pattern MULTI_PATTERN = Pattern.compile("multi\\(([\\d+])");
|
private static final Pattern MULTI_PATTERN = Pattern.compile("multi\\((\\d+)");
|
||||||
|
|
||||||
private String script;
|
private String script;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import com.sparrowwallet.drongo.Utils;
|
||||||
import com.sparrowwallet.drongo.address.*;
|
import com.sparrowwallet.drongo.address.*;
|
||||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||||
import org.bouncycastle.util.encoders.Hex;
|
import org.bouncycastle.util.encoders.Hex;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
|
@ -18,6 +20,8 @@ import static com.sparrowwallet.drongo.protocol.ScriptType.*;
|
||||||
import static com.sparrowwallet.drongo.protocol.ScriptOpCodes.*;
|
import static com.sparrowwallet.drongo.protocol.ScriptOpCodes.*;
|
||||||
|
|
||||||
public class Script {
|
public class Script {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(Script.class);
|
||||||
|
|
||||||
public static final long MAX_SCRIPT_ELEMENT_SIZE = 520;
|
public static final long MAX_SCRIPT_ELEMENT_SIZE = 520;
|
||||||
|
|
||||||
// The program is a set of chunks where each element is either [opcode] or [data, data, data ...]
|
// The program is a set of chunks where each element is either [opcode] or [data, data, data ...]
|
||||||
|
|
@ -32,7 +36,11 @@ public class Script {
|
||||||
Script(byte[] programBytes, boolean parse) {
|
Script(byte[] programBytes, boolean parse) {
|
||||||
program = programBytes;
|
program = programBytes;
|
||||||
if(parse) {
|
if(parse) {
|
||||||
|
try {
|
||||||
parse();
|
parse();
|
||||||
|
} catch(ProtocolException e) {
|
||||||
|
log.warn("Invalid script, continuing with already parsed chunks", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,16 +67,16 @@ public class Script {
|
||||||
// Read some bytes of data, where how many is the opcode value itself.
|
// Read some bytes of data, where how many is the opcode value itself.
|
||||||
dataToRead = opcode;
|
dataToRead = opcode;
|
||||||
} else if (opcode == OP_PUSHDATA1) {
|
} else if (opcode == OP_PUSHDATA1) {
|
||||||
if (bis.available() < 1) throw new ProtocolException("Unexpected end of script");
|
if (bis.available() < 1) throw new ProtocolException("Unexpected end of script - OP_PUSHDATA1 was followed by " + bis.available() + " bytes");
|
||||||
dataToRead = bis.read();
|
dataToRead = bis.read();
|
||||||
} else if (opcode == OP_PUSHDATA2) {
|
} else if (opcode == OP_PUSHDATA2) {
|
||||||
// Read a short, then read that many bytes of data.
|
// Read a short, then read that many bytes of data.
|
||||||
if (bis.available() < 2) throw new ProtocolException("Unexpected end of script");
|
if (bis.available() < 2) throw new ProtocolException("Unexpected end of script - OP_PUSHDATA2 was followed by only " + bis.available() + " bytes");
|
||||||
dataToRead = Utils.readUint16FromStream(bis);
|
dataToRead = Utils.readUint16FromStream(bis);
|
||||||
} else if (opcode == OP_PUSHDATA4) {
|
} else if (opcode == OP_PUSHDATA4) {
|
||||||
// Read a uint32, then read that many bytes of data.
|
// Read a uint32, then read that many bytes of data.
|
||||||
// Though this is allowed, because its value cannot be > 520, it should never actually be used
|
// Though this is allowed, because its value cannot be > 520, it should never actually be used
|
||||||
if (bis.available() < 4) throw new ProtocolException("Unexpected end of script");
|
if (bis.available() < 4) throw new ProtocolException("Unexpected end of script - OP_PUSHDATA4 was followed by only " + bis.available() + " bytes");
|
||||||
dataToRead = Utils.readUint32FromStream(bis);
|
dataToRead = Utils.readUint32FromStream(bis);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import org.bouncycastle.util.encoders.Hex;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
|
|
@ -28,7 +30,11 @@ public class ScriptChunk {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ScriptChunk fromOpcode(int opcode) {
|
public static ScriptChunk fromOpcode(int opcode) {
|
||||||
return new ScriptChunk(opcode, null);
|
return new ScriptChunk(opcode, opcode == ScriptOpCodes.OP_0 ? new byte[0] : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ScriptChunk fromString(String strData, Charset charset) {
|
||||||
|
return fromData(strData.getBytes(charset));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ScriptChunk fromData(byte[] data) {
|
public static ScriptChunk fromData(byte[] data) {
|
||||||
|
|
@ -68,7 +74,7 @@ public class ScriptChunk {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void write(OutputStream stream) throws IOException {
|
public void write(OutputStream stream) throws IOException {
|
||||||
if (isOpCode()) {
|
if (isOpCode() && opcode != ScriptOpCodes.OP_0) {
|
||||||
if(data != null) throw new IllegalStateException("Data must be null for opcode chunk");
|
if(data != null) throw new IllegalStateException("Data must be null for opcode chunk");
|
||||||
stream.write(opcode);
|
stream.write(opcode);
|
||||||
} else if (data != null) {
|
} else if (data != null) {
|
||||||
|
|
@ -126,6 +132,18 @@ public class ScriptChunk {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isString() {
|
||||||
|
if(data == null || data.length == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isSignature() || isPubKey()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Utils.isUtf8(data);
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isScript() {
|
public boolean isScript() {
|
||||||
if(data == null || data.length == 0) {
|
if(data == null || data.length == 0) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -213,6 +231,9 @@ public class ScriptChunk {
|
||||||
if (data.length == 0) {
|
if (data.length == 0) {
|
||||||
return "OP_0";
|
return "OP_0";
|
||||||
}
|
}
|
||||||
|
if(Utils.isUtf8(data)) {
|
||||||
|
return new String(data, StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
return Hex.toHexString(data);
|
return Hex.toHexString(data);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1320,7 +1320,7 @@ public enum ScriptType {
|
||||||
//Start with length of output
|
//Start with length of output
|
||||||
int outputVbytes = output.getLength();
|
int outputVbytes = output.getLength();
|
||||||
//Add length of spending input (with or without discount depending on script type)
|
//Add length of spending input (with or without discount depending on script type)
|
||||||
int inputVbytes = getInputVbytes();
|
double inputVbytes = getInputVbytes();
|
||||||
|
|
||||||
//Return fee rate in sats/vByte multiplied by the calculated output and input vByte lengths
|
//Return fee rate in sats/vByte multiplied by the calculated output and input vByte lengths
|
||||||
return (long)(feeRate * outputVbytes + longTermFeeRate * inputVbytes);
|
return (long)(feeRate * outputVbytes + longTermFeeRate * inputVbytes);
|
||||||
|
|
@ -1333,17 +1333,17 @@ public enum ScriptType {
|
||||||
*
|
*
|
||||||
* @return The number of vBytes required for an input of this script type
|
* @return The number of vBytes required for an input of this script type
|
||||||
*/
|
*/
|
||||||
public int getInputVbytes() {
|
public double getInputVbytes() {
|
||||||
if(P2SH_P2WPKH.equals(this)) {
|
if(P2SH_P2WPKH.equals(this)) {
|
||||||
return (32 + 4 + 1 + 13 + (107 / WITNESS_SCALE_FACTOR) + 4);
|
return (32 + 4 + 1 + 13 + ((double)107 / WITNESS_SCALE_FACTOR) + 4);
|
||||||
} else if(P2SH_P2WSH.equals(this)) {
|
} else if(P2SH_P2WSH.equals(this)) {
|
||||||
return (32 + 4 + 1 + 35 + (107 / WITNESS_SCALE_FACTOR) + 4);
|
return (32 + 4 + 1 + 35 + ((double)107 / WITNESS_SCALE_FACTOR) + 4);
|
||||||
} else if(P2TR.equals(this)) {
|
} else if(P2TR.equals(this)) {
|
||||||
//Assume a default keypath spend
|
//Assume a default keypath spend
|
||||||
return (32 + 4 + 1 + (66 / WITNESS_SCALE_FACTOR) + 4);
|
return (32 + 4 + 1 + ((double)66 / WITNESS_SCALE_FACTOR) + 4);
|
||||||
} else if(Arrays.asList(WITNESS_TYPES).contains(this)) {
|
} else if(Arrays.asList(WITNESS_TYPES).contains(this)) {
|
||||||
//Return length of spending input with 75% discount to script size
|
//Return length of spending input with 75% discount to script size
|
||||||
return (32 + 4 + 1 + (107 / WITNESS_SCALE_FACTOR) + 4);
|
return (32 + 4 + 1 + ((double)107 / WITNESS_SCALE_FACTOR) + 4);
|
||||||
} else if(Arrays.asList(NON_WITNESS_TYPES).contains(this)) {
|
} else if(Arrays.asList(NON_WITNESS_TYPES).contains(this)) {
|
||||||
//Return length of spending input with no discount
|
//Return length of spending input with no discount
|
||||||
return (32 + 4 + 1 + 107 + 4);
|
return (32 + 4 + 1 + 107 + 4);
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,18 @@ public class Sha256Hash implements Comparable<Sha256Hash> {
|
||||||
return wrap(Utils.hexToBytes(hexString));
|
return wrap(Utils.hexToBytes(hexString));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duplicate method with default name for deserialization mappers.
|
||||||
|
*
|
||||||
|
* @param hexString a hash value represented as a hex string
|
||||||
|
* @return a new instance
|
||||||
|
* @throws IllegalArgumentException if the given string is not a valid
|
||||||
|
* hex string, or if it does not represent exactly 32 bytes
|
||||||
|
*/
|
||||||
|
public static Sha256Hash fromString(String hexString) {
|
||||||
|
return wrap(hexString);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new instance that wraps the given hash value, but with byte order reversed.
|
* Creates a new instance that wraps the given hash value, but with byte order reversed.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,7 @@ import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static com.sparrowwallet.drongo.Utils.uint32ToByteStreamLE;
|
import static com.sparrowwallet.drongo.Utils.uint32ToByteStreamLE;
|
||||||
import static com.sparrowwallet.drongo.Utils.uint64ToByteStreamLE;
|
import static com.sparrowwallet.drongo.Utils.uint64ToByteStreamLE;
|
||||||
|
|
@ -634,6 +631,9 @@ public class Transaction extends ChildMessage {
|
||||||
if(spentUtxos.size() != getInputs().size()) {
|
if(spentUtxos.size() != getInputs().size()) {
|
||||||
throw new IllegalArgumentException("Provided spent UTXOs length does not equal the number of transaction inputs");
|
throw new IllegalArgumentException("Provided spent UTXOs length does not equal the number of transaction inputs");
|
||||||
}
|
}
|
||||||
|
if(spentUtxos.stream().anyMatch(Objects::isNull)) {
|
||||||
|
throw new IllegalArgumentException("Not all spent UTXOs are provided");
|
||||||
|
}
|
||||||
if(inputIndex >= getInputs().size()) {
|
if(inputIndex >= getInputs().size()) {
|
||||||
throw new IllegalArgumentException("Input index is greater than the number of transaction inputs");
|
throw new IllegalArgumentException("Input index is greater than the number of transaction inputs");
|
||||||
}
|
}
|
||||||
|
|
@ -685,7 +685,7 @@ public class Transaction extends ChildMessage {
|
||||||
|
|
||||||
if(anyoneCanPay) {
|
if(anyoneCanPay) {
|
||||||
getInputs().get(inputIndex).getOutpoint().bitcoinSerializeToStream(bos);
|
getInputs().get(inputIndex).getOutpoint().bitcoinSerializeToStream(bos);
|
||||||
Utils.uint32ToByteStreamLE(spentUtxos.get(inputIndex).getValue(), bos);
|
Utils.int64ToByteStreamLE(spentUtxos.get(inputIndex).getValue(), bos);
|
||||||
byteArraySerialize(spentUtxos.get(inputIndex).getScriptBytes(), bos);
|
byteArraySerialize(spentUtxos.get(inputIndex).getScriptBytes(), bos);
|
||||||
Utils.uint32ToByteStreamLE(getInputs().get(inputIndex).getSequenceNumber(), bos);
|
Utils.uint32ToByteStreamLE(getInputs().get(inputIndex).getSequenceNumber(), bos);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ public class TransactionSignature {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TransactionSignature decodeFromBitcoin(byte[] bytes, boolean requireCanonicalEncoding) throws SignatureDecodeException {
|
public static TransactionSignature decodeFromBitcoin(byte[] bytes, boolean requireCanonicalEncoding) throws SignatureDecodeException {
|
||||||
if(bytes.length == 64) {
|
if(bytes.length == 64 || bytes.length == 65) {
|
||||||
return decodeFromBitcoin(Type.SCHNORR, bytes, requireCanonicalEncoding);
|
return decodeFromBitcoin(Type.SCHNORR, bytes, requireCanonicalEncoding);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -461,10 +461,10 @@ public class PSBT {
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] serialize() {
|
public byte[] serialize() {
|
||||||
return serialize(true);
|
return serialize(true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] serialize(boolean includeXpubs) {
|
public byte[] serialize(boolean includeXpubs, boolean includeNonWitnessUtxos) {
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
|
||||||
baos.writeBytes(Utils.hexToBytes(PSBT_MAGIC_HEX));
|
baos.writeBytes(Utils.hexToBytes(PSBT_MAGIC_HEX));
|
||||||
|
|
@ -481,8 +481,9 @@ public class PSBT {
|
||||||
for(PSBTInput psbtInput : getPsbtInputs()) {
|
for(PSBTInput psbtInput : getPsbtInputs()) {
|
||||||
List<PSBTEntry> inputEntries = psbtInput.getInputEntries();
|
List<PSBTEntry> inputEntries = psbtInput.getInputEntries();
|
||||||
for(PSBTEntry entry : inputEntries) {
|
for(PSBTEntry entry : inputEntries) {
|
||||||
if(includeXpubs || (entry.getKeyType() != PSBT_IN_BIP32_DERIVATION && entry.getKeyType() != PSBT_IN_PROPRIETARY
|
if((includeXpubs || (entry.getKeyType() != PSBT_IN_BIP32_DERIVATION && entry.getKeyType() != PSBT_IN_PROPRIETARY
|
||||||
&& entry.getKeyType() != PSBT_IN_TAP_INTERNAL_KEY && entry.getKeyType() != PSBT_IN_TAP_BIP32_DERIVATION)) {
|
&& entry.getKeyType() != PSBT_IN_TAP_INTERNAL_KEY && entry.getKeyType() != PSBT_IN_TAP_BIP32_DERIVATION))
|
||||||
|
&& (includeNonWitnessUtxos || entry.getKeyType() != PSBT_IN_NON_WITNESS_UTXO)) {
|
||||||
entry.serializeToStream(baos);
|
entry.serializeToStream(baos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -630,7 +631,7 @@ public class PSBT {
|
||||||
}
|
}
|
||||||
|
|
||||||
public String toBase64String(boolean includeXpubs) {
|
public String toBase64String(boolean includeXpubs) {
|
||||||
return Base64.toBase64String(serialize(includeXpubs));
|
return Base64.toBase64String(serialize(includeXpubs, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isPSBT(byte[] b) {
|
public static boolean isPSBT(byte[] b) {
|
||||||
|
|
|
||||||
|
|
@ -188,7 +188,7 @@ public class PSBTEntry {
|
||||||
psbtByteBuffer.get(buf);
|
psbtByteBuffer.get(buf);
|
||||||
ByteBuffer byteBuffer = ByteBuffer.wrap(buf);
|
ByteBuffer byteBuffer = ByteBuffer.wrap(buf);
|
||||||
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
return byteBuffer.getShort();
|
return Short.toUnsignedInt(byteBuffer.getShort());
|
||||||
}
|
}
|
||||||
case (byte) 0xfe: {
|
case (byte) 0xfe: {
|
||||||
byte[] buf = new byte[4];
|
byte[] buf = new byte[4];
|
||||||
|
|
|
||||||
|
|
@ -519,6 +519,20 @@ public class PSBTInput {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean sign(ECKey privKey) {
|
public boolean sign(ECKey privKey) {
|
||||||
|
return sign(new PSBTInputSigner() {
|
||||||
|
@Override
|
||||||
|
public TransactionSignature sign(Sha256Hash hash, SigHash sigHash, TransactionSignature.Type signatureType) {
|
||||||
|
return privKey.sign(hash, sigHash, signatureType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ECKey getPubKey() {
|
||||||
|
return ECKey.fromPublicOnly(privKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean sign(PSBTInputSigner psbtInputSigner) {
|
||||||
SigHash localSigHash = getSigHash();
|
SigHash localSigHash = getSigHash();
|
||||||
if(localSigHash == null) {
|
if(localSigHash == null) {
|
||||||
localSigHash = getDefaultSigHash();
|
localSigHash = getDefaultSigHash();
|
||||||
|
|
@ -529,12 +543,12 @@ public class PSBTInput {
|
||||||
if(signingScript != null) {
|
if(signingScript != null) {
|
||||||
Sha256Hash hash = getHashForSignature(signingScript, localSigHash);
|
Sha256Hash hash = getHashForSignature(signingScript, localSigHash);
|
||||||
TransactionSignature.Type type = isTaproot() ? SCHNORR : ECDSA;
|
TransactionSignature.Type type = isTaproot() ? SCHNORR : ECDSA;
|
||||||
TransactionSignature transactionSignature = privKey.sign(hash, localSigHash, type);
|
TransactionSignature transactionSignature = psbtInputSigner.sign(hash, localSigHash, type);
|
||||||
|
|
||||||
if(type == SCHNORR) {
|
if(type == SCHNORR) {
|
||||||
tapKeyPathSignature = transactionSignature;
|
tapKeyPathSignature = transactionSignature;
|
||||||
} else {
|
} else {
|
||||||
ECKey pubKey = ECKey.fromPublicOnly(privKey);
|
ECKey pubKey = psbtInputSigner.getPubKey();
|
||||||
getPartialSignatures().put(pubKey, transactionSignature);
|
getPartialSignatures().put(pubKey, transactionSignature);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.sparrowwallet.drongo.psbt;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||||
|
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||||
|
import com.sparrowwallet.drongo.protocol.SigHash;
|
||||||
|
import com.sparrowwallet.drongo.protocol.TransactionSignature;
|
||||||
|
|
||||||
|
public interface PSBTInputSigner {
|
||||||
|
TransactionSignature sign(Sha256Hash hash, SigHash sigHash, TransactionSignature.Type signatureType);
|
||||||
|
ECKey getPubKey();
|
||||||
|
}
|
||||||
|
|
@ -229,4 +229,85 @@ public class Bip39MnemonicCode {
|
||||||
bits[(i * 8) + j] = (data[i] & (1 << (7 - j))) != 0;
|
bits[(i * 8) + j] = (data[i] & (1 << (7 - j))) != 0;
|
||||||
return bits;
|
return bits;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<String> getPossibleLastWords(List<String> previousWords) throws MnemonicException.MnemonicLengthException, MnemonicException.MnemonicWordException {
|
||||||
|
if((previousWords.size() + 1) % 3 > 0) {
|
||||||
|
throw new MnemonicException.MnemonicLengthException("Previous word list size must be multiple of three words, less one.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up all the words in the list and construct the
|
||||||
|
// concatenation of the original entropy and the checksum.
|
||||||
|
//
|
||||||
|
int concatLenBits = previousWords.size() * 11;
|
||||||
|
boolean[] concatBits = new boolean[concatLenBits];
|
||||||
|
int wordindex = 0;
|
||||||
|
for (String word : previousWords) {
|
||||||
|
// Find the words index in the wordlist.
|
||||||
|
int ndx = Collections.binarySearch(this.wordList, word);
|
||||||
|
if (ndx < 0) {
|
||||||
|
throw new MnemonicException.MnemonicWordException(word);
|
||||||
|
}
|
||||||
|
// Set the next 11 bits to the value of the index.
|
||||||
|
for (int ii = 0; ii < 11; ++ii) {
|
||||||
|
concatBits[(wordindex * 11) + ii] = (ndx & (1 << (10 - ii))) != 0;
|
||||||
|
}
|
||||||
|
++wordindex;
|
||||||
|
}
|
||||||
|
|
||||||
|
int checksumLengthBits = (concatLenBits + 11) / 33;
|
||||||
|
int entropyLengthBits = (concatLenBits + 11) - checksumLengthBits;
|
||||||
|
int varyingLengthBits = entropyLengthBits - concatLenBits;
|
||||||
|
|
||||||
|
boolean[][] bitPermutations = getBitPermutations(varyingLengthBits);
|
||||||
|
|
||||||
|
ArrayList<String> possibleWords = new ArrayList<>();
|
||||||
|
for(boolean[] bitPermutation : bitPermutations) {
|
||||||
|
boolean[] entropyBits = new boolean[concatLenBits + varyingLengthBits];
|
||||||
|
System.arraycopy(concatBits, 0, entropyBits, 0, concatBits.length);
|
||||||
|
System.arraycopy(bitPermutation, 0, entropyBits, concatBits.length, varyingLengthBits);
|
||||||
|
|
||||||
|
byte[] entropy = new byte[entropyLengthBits / 8];
|
||||||
|
for(int ii = 0; ii < entropy.length; ++ii) {
|
||||||
|
for(int jj = 0; jj < 8; ++jj) {
|
||||||
|
if(entropyBits[(ii * 8) + jj]) {
|
||||||
|
entropy[ii] |= 1 << (7 - jj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] hash = Sha256Hash.hash(entropy);
|
||||||
|
boolean[] hashBits = bytesToBits(hash);
|
||||||
|
|
||||||
|
boolean[] wordBits = new boolean[11];
|
||||||
|
System.arraycopy(bitPermutation, 0, wordBits, 0, varyingLengthBits);
|
||||||
|
System.arraycopy(hashBits, 0, wordBits, varyingLengthBits, checksumLengthBits);
|
||||||
|
|
||||||
|
int index = 0;
|
||||||
|
for(int j = 0; j < 11; ++j) {
|
||||||
|
index <<= 1;
|
||||||
|
if(wordBits[j]) {
|
||||||
|
index |= 0x1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
possibleWords.add(this.wordList.get(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
Collections.sort(possibleWords);
|
||||||
|
|
||||||
|
return possibleWords;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean[][] getBitPermutations(int length) {
|
||||||
|
int numPermutations = (int) Math.pow(2, length);
|
||||||
|
boolean[][] permutations = new boolean[numPermutations][length];
|
||||||
|
|
||||||
|
for (int i = 0; i < numPermutations; i++) {
|
||||||
|
for (int j = 0; j < length; j++) {
|
||||||
|
permutations[i][j] = ((i >> j) & 1) == 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return permutations;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@ package com.sparrowwallet.drongo.wallet;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
|
|
||||||
public class CoinbaseUtxoFilter implements UtxoFilter {
|
public class CoinbaseTxoFilter implements TxoFilter {
|
||||||
private final Wallet wallet;
|
private final Wallet wallet;
|
||||||
|
|
||||||
public CoinbaseUtxoFilter(Wallet wallet) {
|
public CoinbaseTxoFilter(Wallet wallet) {
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package com.sparrowwallet.drongo.wallet;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
public class ExcludeTxoFilter implements TxoFilter {
|
||||||
|
private final Collection<BlockTransactionHashIndex> excludedTxos;
|
||||||
|
|
||||||
|
public ExcludeTxoFilter() {
|
||||||
|
this.excludedTxos = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExcludeTxoFilter(Collection<BlockTransactionHashIndex> excludedTxos) {
|
||||||
|
this.excludedTxos = new ArrayList<>(excludedTxos);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEligible(BlockTransactionHashIndex candidate) {
|
||||||
|
for(BlockTransactionHashIndex excludedTxo : excludedTxos) {
|
||||||
|
if(candidate.getHash().equals(excludedTxo.getHash()) && candidate.getIndex() == excludedTxo.getIndex()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<BlockTransactionHashIndex> getExcludedTxos() {
|
||||||
|
return excludedTxos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
package com.sparrowwallet.drongo.wallet;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
|
|
||||||
public class ExcludeUtxoFilter implements UtxoFilter {
|
|
||||||
private final Collection<BlockTransactionHashIndex> excludedUtxos;
|
|
||||||
|
|
||||||
public ExcludeUtxoFilter() {
|
|
||||||
this.excludedUtxos = new ArrayList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ExcludeUtxoFilter(Collection<BlockTransactionHashIndex> excludedUtxos) {
|
|
||||||
this.excludedUtxos = new ArrayList<>(excludedUtxos);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isEligible(BlockTransactionHashIndex candidate) {
|
|
||||||
for(BlockTransactionHashIndex excludedUtxo : excludedUtxos) {
|
|
||||||
if(candidate.getHash().equals(excludedUtxo.getHash()) && candidate.getIndex() == excludedUtxo.getIndex()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Collection<BlockTransactionHashIndex> getExcludedUtxos() {
|
|
||||||
return excludedUtxos;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package com.sparrowwallet.drongo.wallet;
|
package com.sparrowwallet.drongo.wallet;
|
||||||
|
|
||||||
public class FrozenUtxoFilter implements UtxoFilter {
|
public class FrozenTxoFilter implements TxoFilter {
|
||||||
@Override
|
@Override
|
||||||
public boolean isEligible(BlockTransactionHashIndex candidate) {
|
public boolean isEligible(BlockTransactionHashIndex candidate) {
|
||||||
return candidate.getStatus() == null || candidate.getStatus() != Status.FROZEN;
|
return candidate.getStatus() == null || candidate.getStatus() != Status.FROZEN;
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package com.sparrowwallet.drongo.wallet;
|
package com.sparrowwallet.drongo.wallet;
|
||||||
|
|
||||||
public class InsufficientFundsException extends Exception {
|
public class InsufficientFundsException extends Exception {
|
||||||
|
private Long targetValue;
|
||||||
|
|
||||||
public InsufficientFundsException() {
|
public InsufficientFundsException() {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
@ -8,4 +10,13 @@ public class InsufficientFundsException extends Exception {
|
||||||
public InsufficientFundsException(String msg) {
|
public InsufficientFundsException(String msg) {
|
||||||
super(msg);
|
super(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public InsufficientFundsException(String message, Long targetValue) {
|
||||||
|
super(message);
|
||||||
|
this.targetValue = targetValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getTargetValue() {
|
||||||
|
return targetValue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,30 @@
|
||||||
package com.sparrowwallet.drongo.wallet;
|
package com.sparrowwallet.drongo.wallet;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
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 Collection<BlockTransactionHashIndex> excludedUtxos;
|
||||||
private final boolean maintainOrder;
|
private final boolean maintainOrder;
|
||||||
|
private final boolean requireAll;
|
||||||
|
|
||||||
public PresetUtxoSelector(Collection<BlockTransactionHashIndex> presetUtxos) {
|
public PresetUtxoSelector(Collection<BlockTransactionHashIndex> presetUtxos) {
|
||||||
this.presetUtxos = presetUtxos;
|
this(presetUtxos, new ArrayList<>());
|
||||||
this.maintainOrder = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public PresetUtxoSelector(Collection<BlockTransactionHashIndex> presetUtxos, boolean maintainOrder) {
|
public PresetUtxoSelector(Collection<BlockTransactionHashIndex> presetUtxos, Collection<BlockTransactionHashIndex> excludedUtxos) {
|
||||||
this.presetUtxos = presetUtxos;
|
this.presetUtxos = presetUtxos;
|
||||||
|
this.excludedUtxos = excludedUtxos;
|
||||||
|
this.maintainOrder = false;
|
||||||
|
this.requireAll = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PresetUtxoSelector(Collection<BlockTransactionHashIndex> presetUtxos, boolean maintainOrder, boolean requireAll) {
|
||||||
|
this.presetUtxos = presetUtxos;
|
||||||
|
this.excludedUtxos = new ArrayList<>();
|
||||||
this.maintainOrder = maintainOrder;
|
this.maintainOrder = maintainOrder;
|
||||||
|
this.requireAll = requireAll;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -33,8 +41,11 @@ public class PresetUtxoSelector extends SingleSetUtxoSelector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(maintainOrder && utxos.containsAll(presetUtxos)) {
|
Set<BlockTransactionHashIndex> utxosSet = new HashSet<>(utxos);
|
||||||
|
if(maintainOrder && utxosSet.containsAll(presetUtxos)) {
|
||||||
return presetUtxos;
|
return presetUtxos;
|
||||||
|
} else if(requireAll && !utxosSet.containsAll(presetUtxos)) {
|
||||||
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
return utxos;
|
return utxos;
|
||||||
|
|
@ -44,6 +55,17 @@ public class PresetUtxoSelector extends SingleSetUtxoSelector {
|
||||||
return presetUtxos;
|
return presetUtxos;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Collection<BlockTransactionHashIndex> getExcludedUtxos() {
|
||||||
|
return excludedUtxos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TxoFilter asExcludeTxoFilter() {
|
||||||
|
List<BlockTransactionHashIndex> utxos = new ArrayList<>();
|
||||||
|
utxos.addAll(presetUtxos);
|
||||||
|
utxos.addAll(excludedUtxos);
|
||||||
|
return new ExcludeTxoFilter(utxos);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean shuffleInputs() {
|
public boolean shuffleInputs() {
|
||||||
return !maintainOrder;
|
return !maintainOrder;
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,13 @@ public class SeedQR {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static DeterministicSeed getSeed(byte[] compactSeedQr) {
|
public static DeterministicSeed getSeed(byte[] compactSeedQr) {
|
||||||
|
byte[] seed;
|
||||||
|
|
||||||
|
if(compactSeedQr.length == 16 || compactSeedQr.length == 32) {
|
||||||
|
//Assume scan contains seed only
|
||||||
|
seed = compactSeedQr;
|
||||||
|
} else {
|
||||||
|
//Assume scan contains header, seed and EC bytes
|
||||||
if(compactSeedQr[0] != 0x41 && compactSeedQr[0] != 0x42) {
|
if(compactSeedQr[0] != 0x41 && compactSeedQr[0] != 0x42) {
|
||||||
throw new IllegalArgumentException("Invalid CompactSeedQR header");
|
throw new IllegalArgumentException("Invalid CompactSeedQR header");
|
||||||
}
|
}
|
||||||
|
|
@ -41,13 +48,16 @@ public class SeedQR {
|
||||||
|
|
||||||
String qrHex = Utils.bytesToHex(compactSeedQr);
|
String qrHex = Utils.bytesToHex(compactSeedQr);
|
||||||
String seedHex;
|
String seedHex;
|
||||||
if(qrHex.endsWith("0ec")) {
|
if(qrHex.endsWith("0ec11ec11")) {
|
||||||
seedHex = qrHex.substring(3, qrHex.length() - 3);
|
seedHex = qrHex.substring(3, qrHex.length() - 9); //12 word, high EC
|
||||||
|
} else if(qrHex.endsWith("0ec")) {
|
||||||
|
seedHex = qrHex.substring(3, qrHex.length() - 3); //12 word, low EC
|
||||||
} else {
|
} else {
|
||||||
seedHex = qrHex.substring(3, qrHex.length() - 1);
|
seedHex = qrHex.substring(3, qrHex.length() - 1); //24 word
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] seed = Utils.hexToBytes(seedHex);
|
seed = Utils.hexToBytes(seedHex);
|
||||||
|
}
|
||||||
|
|
||||||
if(seed.length < 16 || seed.length > 32 || seed.length % 4 > 0) {
|
if(seed.length < 16 || seed.length > 32 || seed.length % 4 > 0) {
|
||||||
throw new IllegalArgumentException("Invalid CompactSeedQR length: " + compactSeedQr.length);
|
throw new IllegalArgumentException("Invalid CompactSeedQR length: " + compactSeedQr.length);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.sparrowwallet.drongo.wallet;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||||
|
|
||||||
|
public class SpentTxoFilter implements TxoFilter {
|
||||||
|
private final Sha256Hash replacedTxid;
|
||||||
|
|
||||||
|
public SpentTxoFilter() {
|
||||||
|
replacedTxid = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SpentTxoFilter(Sha256Hash replacedTxid) {
|
||||||
|
this.replacedTxid = replacedTxid;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEligible(BlockTransactionHashIndex candidate) {
|
||||||
|
return !isSpentOrReplaced(candidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isSpentOrReplaced(BlockTransactionHashIndex candidate) {
|
||||||
|
return candidate.getHash().equals(replacedTxid) || (candidate.isSpent() && !candidate.getSpentBy().getHash().equals(replacedTxid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
package com.sparrowwallet.drongo.wallet;
|
package com.sparrowwallet.drongo.wallet;
|
||||||
|
|
||||||
public interface UtxoFilter {
|
public interface TxoFilter {
|
||||||
boolean isEligible(BlockTransactionHashIndex candidate);
|
boolean isEligible(BlockTransactionHashIndex candidate);
|
||||||
}
|
}
|
||||||
|
|
@ -799,30 +799,38 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<BlockTransactionHashIndex, WalletNode> getWalletUtxos() {
|
public Map<BlockTransactionHashIndex, WalletNode> getWalletUtxos() {
|
||||||
return getWalletUtxos(false);
|
return getWalletTxos(List.of(new SpentTxoFilter()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<BlockTransactionHashIndex, WalletNode> getWalletUtxos(boolean includeSpentMempoolOutputs) {
|
public Map<BlockTransactionHashIndex, WalletNode> getSpendableUtxos() {
|
||||||
Map<BlockTransactionHashIndex, WalletNode> walletUtxos = new TreeMap<>();
|
return getWalletTxos(List.of(new SpentTxoFilter(), new FrozenTxoFilter(), new CoinbaseTxoFilter(this)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<BlockTransactionHashIndex, WalletNode> getSpendableUtxos(BlockTransaction replacedTransaction) {
|
||||||
|
return getWalletTxos(List.of(new SpentTxoFilter(replacedTransaction == null ? null : replacedTransaction.getHash()), new FrozenTxoFilter(), new CoinbaseTxoFilter(this)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<BlockTransactionHashIndex, WalletNode> getWalletTxos(Collection<TxoFilter> txoFilters) {
|
||||||
|
Map<BlockTransactionHashIndex, WalletNode> walletTxos = new TreeMap<>();
|
||||||
for(KeyPurpose keyPurpose : getWalletKeyPurposes()) {
|
for(KeyPurpose keyPurpose : getWalletKeyPurposes()) {
|
||||||
getWalletUtxos(walletUtxos, getNode(keyPurpose), includeSpentMempoolOutputs);
|
getWalletTxos(walletTxos, getNode(keyPurpose), txoFilters);
|
||||||
}
|
}
|
||||||
|
|
||||||
for(Wallet childWallet : getChildWallets()) {
|
for(Wallet childWallet : getChildWallets()) {
|
||||||
if(childWallet.isNested()) {
|
if(childWallet.isNested()) {
|
||||||
for(KeyPurpose keyPurpose : childWallet.getWalletKeyPurposes()) {
|
for(KeyPurpose keyPurpose : childWallet.getWalletKeyPurposes()) {
|
||||||
getWalletUtxos(walletUtxos, childWallet.getNode(keyPurpose), includeSpentMempoolOutputs);
|
getWalletTxos(walletTxos, childWallet.getNode(keyPurpose), txoFilters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return walletUtxos;
|
return walletTxos;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void getWalletUtxos(Map<BlockTransactionHashIndex, WalletNode> walletUtxos, WalletNode purposeNode, boolean includeSpentMempoolOutputs) {
|
private void getWalletTxos(Map<BlockTransactionHashIndex, WalletNode> walletTxos, WalletNode purposeNode, Collection<TxoFilter> txoFilters) {
|
||||||
for(WalletNode addressNode : purposeNode.getChildren()) {
|
for(WalletNode addressNode : purposeNode.getChildren()) {
|
||||||
for(BlockTransactionHashIndex utxo : addressNode.getUnspentTransactionOutputs(includeSpentMempoolOutputs)) {
|
for(BlockTransactionHashIndex utxo : addressNode.getTransactionOutputs(txoFilters)) {
|
||||||
walletUtxos.put(utxo, addressNode);
|
walletTxos.put(utxo, addressNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -981,29 +989,30 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
||||||
return getFee(changeOutput, feeRate, longTermFeeRate);
|
return getFee(changeOutput, feeRate, longTermFeeRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
public WalletTransaction createWalletTransaction(List<UtxoSelector> utxoSelectors, List<UtxoFilter> utxoFilters, List<Payment> payments, List<byte[]> opReturns, Set<WalletNode> excludedChangeNodes, double feeRate, double longTermFeeRate, Long fee, Integer currentBlockHeight, boolean groupByAddress, boolean includeMempoolOutputs, boolean includeSpentMempoolOutputs) throws InsufficientFundsException {
|
public WalletTransaction createWalletTransaction(List<UtxoSelector> utxoSelectors, List<TxoFilter> txoFilters, List<Payment> payments, List<byte[]> opReturns, Set<WalletNode> excludedChangeNodes, double feeRate, double longTermFeeRate, Long fee, Integer currentBlockHeight, boolean groupByAddress, boolean includeMempoolOutputs) throws InsufficientFundsException {
|
||||||
boolean sendMax = payments.stream().anyMatch(Payment::isSendMax);
|
boolean sendMax = payments.stream().anyMatch(Payment::isSendMax);
|
||||||
long totalPaymentAmount = payments.stream().map(Payment::getAmount).mapToLong(v -> v).sum();
|
long totalPaymentAmount = payments.stream().map(Payment::getAmount).mapToLong(v -> v).sum();
|
||||||
long totalUtxoValue = getWalletUtxos().keySet().stream().mapToLong(BlockTransactionHashIndex::getValue).sum();
|
Map<BlockTransactionHashIndex, WalletNode> availableTxos = getWalletTxos(txoFilters);
|
||||||
|
long totalAvailableValue = availableTxos.keySet().stream().mapToLong(BlockTransactionHashIndex::getValue).sum();
|
||||||
|
|
||||||
if(fee != null && feeRate != Transaction.DEFAULT_MIN_RELAY_FEE) {
|
if(fee != null && feeRate != Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||||
throw new IllegalArgumentException("Use an input fee rate of 1 sat/vB when using a defined fee amount so UTXO selectors overestimate effective value");
|
throw new IllegalArgumentException("Use an input fee rate of 1 sat/vB when using a defined fee amount so UTXO selectors overestimate effective value");
|
||||||
}
|
}
|
||||||
|
|
||||||
long maxSpendableAmt = getMaxSpendable(payments.stream().map(Payment::getAddress).collect(Collectors.toList()), feeRate, includeSpentMempoolOutputs);
|
long maxSpendableAmt = getMaxSpendable(payments.stream().map(Payment::getAddress).collect(Collectors.toList()), feeRate, availableTxos);
|
||||||
if(maxSpendableAmt < 0) {
|
if(maxSpendableAmt < 0) {
|
||||||
throw new InsufficientFundsException("Not enough combined value in all available UTXOs to send a transaction to the provided addresses at this fee rate");
|
throw new InsufficientFundsException("Not enough combined value in all available UTXOs to send a transaction to the provided addresses at this fee rate");
|
||||||
}
|
}
|
||||||
|
|
||||||
//When a user fee is set, we can calculate the fees to spend all UTXOs because we assume all UTXOs are spendable at a fee rate of 1 sat/vB
|
//When a user fee is set, we can calculate the fees to spend all UTXOs because we assume all UTXOs are spendable at a fee rate of 1 sat/vB
|
||||||
//We can then add the user set fee less this amount as a "phantom payment amount" to the value required to find (which cannot include transaction fees)
|
//We can then add the user set fee less this amount as a "phantom payment amount" to the value required to find (which cannot include transaction fees)
|
||||||
long valueRequiredAmt = totalPaymentAmount + (fee != null ? fee - (totalUtxoValue - maxSpendableAmt) : 0);
|
long valueRequiredAmt = totalPaymentAmount + (fee != null ? fee - (totalAvailableValue - maxSpendableAmt) : 0);
|
||||||
if(maxSpendableAmt < valueRequiredAmt) {
|
if(maxSpendableAmt < valueRequiredAmt) {
|
||||||
throw new InsufficientFundsException("Not enough combined value in all available UTXOs to send a transaction to send the provided payments at the user set fee" + (fee == null ? " rate" : ""));
|
throw new InsufficientFundsException("Not enough combined value in all available UTXOs to send a transaction to send the provided payments at the user set fee" + (fee == null ? " rate" : ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
while(true) {
|
while(true) {
|
||||||
List<Map<BlockTransactionHashIndex, WalletNode>> selectedUtxoSets = selectInputSets(utxoSelectors, utxoFilters, valueRequiredAmt, feeRate, longTermFeeRate, groupByAddress, includeMempoolOutputs, includeSpentMempoolOutputs, sendMax);
|
List<Map<BlockTransactionHashIndex, WalletNode>> selectedUtxoSets = selectInputSets(availableTxos, utxoSelectors, txoFilters, valueRequiredAmt, feeRate, longTermFeeRate, groupByAddress, includeMempoolOutputs, sendMax);
|
||||||
Map<BlockTransactionHashIndex, WalletNode> selectedUtxos = new LinkedHashMap<>();
|
Map<BlockTransactionHashIndex, WalletNode> selectedUtxos = new LinkedHashMap<>();
|
||||||
selectedUtxoSets.forEach(selectedUtxos::putAll);
|
selectedUtxoSets.forEach(selectedUtxos::putAll);
|
||||||
long totalSelectedAmt = selectedUtxos.keySet().stream().mapToLong(BlockTransactionHashIndex::getValue).sum();
|
long totalSelectedAmt = selectedUtxos.keySet().stream().mapToLong(BlockTransactionHashIndex::getValue).sum();
|
||||||
|
|
@ -1076,7 +1085,7 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
||||||
if(differenceAmt < noChangeFeeRequiredAmt) {
|
if(differenceAmt < noChangeFeeRequiredAmt) {
|
||||||
valueRequiredAmt = totalSelectedAmt + 1;
|
valueRequiredAmt = totalSelectedAmt + 1;
|
||||||
//If we haven't selected all UTXOs yet, don't require more than the max spendable amount
|
//If we haven't selected all UTXOs yet, don't require more than the max spendable amount
|
||||||
if(valueRequiredAmt > maxSpendableAmt && transaction.getInputs().size() < getWalletUtxos().size()) {
|
if(valueRequiredAmt > maxSpendableAmt && transaction.getInputs().size() < availableTxos.size()) {
|
||||||
valueRequiredAmt = maxSpendableAmt;
|
valueRequiredAmt = maxSpendableAmt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1180,8 +1189,8 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Map<BlockTransactionHashIndex, WalletNode>> selectInputSets(List<UtxoSelector> utxoSelectors, List<UtxoFilter> utxoFilters, Long targetValue, double feeRate, double longTermFeeRate, boolean groupByAddress, boolean includeMempoolOutputs, boolean includeSpentMempoolOutputs, boolean sendMax) throws InsufficientFundsException {
|
private List<Map<BlockTransactionHashIndex, WalletNode>> selectInputSets(Map<BlockTransactionHashIndex, WalletNode> availableTxos, List<UtxoSelector> utxoSelectors, List<TxoFilter> txoFilters, Long targetValue, double feeRate, double longTermFeeRate, boolean groupByAddress, boolean includeMempoolOutputs, boolean sendMax) throws InsufficientFundsException {
|
||||||
List<OutputGroup> utxoPool = getGroupedUtxos(utxoFilters, feeRate, longTermFeeRate, groupByAddress, includeSpentMempoolOutputs);
|
List<OutputGroup> utxoPool = getGroupedUtxos(txoFilters, feeRate, longTermFeeRate, groupByAddress);
|
||||||
|
|
||||||
List<OutputGroup.Filter> filters = new ArrayList<>();
|
List<OutputGroup.Filter> filters = new ArrayList<>();
|
||||||
filters.add(new OutputGroup.Filter(1, 6, false));
|
filters.add(new OutputGroup.Filter(1, 6, false));
|
||||||
|
|
@ -1204,7 +1213,6 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
||||||
List<Collection<BlockTransactionHashIndex>> selectedInputSets = utxoSelector.selectSets(targetValue, filteredPool);
|
List<Collection<BlockTransactionHashIndex>> selectedInputSets = utxoSelector.selectSets(targetValue, filteredPool);
|
||||||
List<Map<BlockTransactionHashIndex, WalletNode>> selectedInputSetsList = new ArrayList<>();
|
List<Map<BlockTransactionHashIndex, WalletNode>> selectedInputSetsList = new ArrayList<>();
|
||||||
long total = 0;
|
long total = 0;
|
||||||
Map<BlockTransactionHashIndex, WalletNode> utxos = getWalletUtxos(includeSpentMempoolOutputs);
|
|
||||||
for(Collection<BlockTransactionHashIndex> selectedInputs : selectedInputSets) {
|
for(Collection<BlockTransactionHashIndex> selectedInputs : selectedInputSets) {
|
||||||
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<>();
|
||||||
|
|
@ -1213,7 +1221,7 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
||||||
Collections.shuffle(shuffledInputs);
|
Collections.shuffle(shuffledInputs);
|
||||||
}
|
}
|
||||||
for(BlockTransactionHashIndex shuffledInput : shuffledInputs) {
|
for(BlockTransactionHashIndex shuffledInput : shuffledInputs) {
|
||||||
selectedInputsMap.put(shuffledInput, utxos.get(shuffledInput));
|
selectedInputsMap.put(shuffledInput, availableTxos.get(shuffledInput));
|
||||||
}
|
}
|
||||||
selectedInputSetsList.add(selectedInputsMap);
|
selectedInputSetsList.add(selectedInputsMap);
|
||||||
}
|
}
|
||||||
|
|
@ -1224,21 +1232,21 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new InsufficientFundsException("Not enough combined value in UTXOs for output value " + targetValue);
|
throw new InsufficientFundsException("Not enough combined value in UTXOs for output value " + targetValue, targetValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<OutputGroup> getGroupedUtxos(List<UtxoFilter> utxoFilters, double feeRate, double longTermFeeRate, boolean groupByAddress, boolean includeSpentMempoolOutputs) {
|
public List<OutputGroup> getGroupedUtxos(List<TxoFilter> txoFilters, double feeRate, double longTermFeeRate, boolean groupByAddress) {
|
||||||
List<OutputGroup> outputGroups = new ArrayList<>();
|
List<OutputGroup> outputGroups = new ArrayList<>();
|
||||||
Map<Sha256Hash, BlockTransaction> walletTransactions = getWalletTransactions();
|
Map<Sha256Hash, BlockTransaction> walletTransactions = getWalletTransactions();
|
||||||
Map<BlockTransactionHashIndex, WalletNode> walletTxos = getWalletTxos();
|
Map<BlockTransactionHashIndex, WalletNode> walletTxos = getWalletTxos();
|
||||||
for(KeyPurpose keyPurpose : getWalletKeyPurposes()) {
|
for(KeyPurpose keyPurpose : getWalletKeyPurposes()) {
|
||||||
getGroupedUtxos(outputGroups, getNode(keyPurpose), utxoFilters, walletTransactions, walletTxos, feeRate, longTermFeeRate, groupByAddress, includeSpentMempoolOutputs);
|
getGroupedUtxos(outputGroups, getNode(keyPurpose), txoFilters, walletTransactions, walletTxos, feeRate, longTermFeeRate, groupByAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
for(Wallet childWallet : getChildWallets()) {
|
for(Wallet childWallet : getChildWallets()) {
|
||||||
if(childWallet.isNested()) {
|
if(childWallet.isNested()) {
|
||||||
for(KeyPurpose keyPurpose : childWallet.getWalletKeyPurposes()) {
|
for(KeyPurpose keyPurpose : childWallet.getWalletKeyPurposes()) {
|
||||||
childWallet.getGroupedUtxos(outputGroups, childWallet.getNode(keyPurpose), utxoFilters, walletTransactions, walletTxos, feeRate, longTermFeeRate, groupByAddress, includeSpentMempoolOutputs);
|
childWallet.getGroupedUtxos(outputGroups, childWallet.getNode(keyPurpose), txoFilters, walletTransactions, walletTxos, feeRate, longTermFeeRate, groupByAddress);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1246,16 +1254,11 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
||||||
return outputGroups;
|
return outputGroups;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void getGroupedUtxos(List<OutputGroup> outputGroups, WalletNode purposeNode, List<UtxoFilter> utxoFilters, Map<Sha256Hash, BlockTransaction> walletTransactions, Map<BlockTransactionHashIndex, WalletNode> walletTxos, double feeRate, double longTermFeeRate, boolean groupByAddress, boolean includeSpentMempoolOutputs) {
|
private void getGroupedUtxos(List<OutputGroup> outputGroups, WalletNode purposeNode, List<TxoFilter> txoFilters, Map<Sha256Hash, BlockTransaction> walletTransactions, Map<BlockTransactionHashIndex, WalletNode> walletTxos, double feeRate, double longTermFeeRate, boolean groupByAddress) {
|
||||||
int inputWeightUnits = getInputWeightUnits();
|
int inputWeightUnits = getInputWeightUnits();
|
||||||
for(WalletNode addressNode : purposeNode.getChildren()) {
|
for(WalletNode addressNode : purposeNode.getChildren()) {
|
||||||
OutputGroup outputGroup = null;
|
OutputGroup outputGroup = null;
|
||||||
for(BlockTransactionHashIndex utxo : addressNode.getUnspentTransactionOutputs(includeSpentMempoolOutputs)) {
|
for(BlockTransactionHashIndex utxo : addressNode.getTransactionOutputs(txoFilters)) {
|
||||||
Optional<UtxoFilter> matchedFilter = utxoFilters.stream().filter(utxoFilter -> !utxoFilter.isEligible(utxo)).findAny();
|
|
||||||
if(matchedFilter.isPresent()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(outputGroup == null || !groupByAddress) {
|
if(outputGroup == null || !groupByAddress) {
|
||||||
outputGroup = new OutputGroup(addressNode.getWallet().getScriptType(), getStoredBlockHeight(), inputWeightUnits, feeRate, longTermFeeRate);
|
outputGroup = new OutputGroup(addressNode.getWallet().getScriptType(), getStoredBlockHeight(), inputWeightUnits, feeRate, longTermFeeRate);
|
||||||
outputGroups.add(outputGroup);
|
outputGroups.add(outputGroup);
|
||||||
|
|
@ -1323,12 +1326,12 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
||||||
* @param feeRate the fee rate in sats/vB
|
* @param feeRate the fee rate in sats/vB
|
||||||
* @return the maximum spendable amount (can be negative if the fee is higher than the combined UTXO value)
|
* @return the maximum spendable amount (can be negative if the fee is higher than the combined UTXO value)
|
||||||
*/
|
*/
|
||||||
public long getMaxSpendable(List<Address> paymentAddresses, double feeRate, boolean includeSpentMempoolOutputs) {
|
public long getMaxSpendable(List<Address> paymentAddresses, double feeRate, Map<BlockTransactionHashIndex, WalletNode> availableTxos) {
|
||||||
long maxInputValue = 0;
|
long maxInputValue = 0;
|
||||||
|
|
||||||
Map<Wallet, Integer> cachedInputWeightUnits = new HashMap<>();
|
Map<Wallet, Integer> cachedInputWeightUnits = new HashMap<>();
|
||||||
Transaction transaction = new Transaction();
|
Transaction transaction = new Transaction();
|
||||||
for(Map.Entry<BlockTransactionHashIndex, WalletNode> utxo : getWalletUtxos(includeSpentMempoolOutputs).entrySet()) {
|
for(Map.Entry<BlockTransactionHashIndex, WalletNode> utxo : availableTxos.entrySet()) {
|
||||||
int inputWeightUnits = cachedInputWeightUnits.computeIfAbsent(utxo.getValue().getWallet(), Wallet::getInputWeightUnits);
|
int inputWeightUnits = cachedInputWeightUnits.computeIfAbsent(utxo.getValue().getWallet(), Wallet::getInputWeightUnits);
|
||||||
long minInputValue = (long)Math.ceil(feeRate * inputWeightUnits / WITNESS_SCALE_FACTOR);
|
long minInputValue = (long)Math.ceil(feeRate * inputWeightUnits / WITNESS_SCALE_FACTOR);
|
||||||
if(utxo.getKey().getValue() > minInputValue) {
|
if(utxo.getKey().getValue() > minInputValue) {
|
||||||
|
|
@ -1660,6 +1663,10 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
||||||
labels.put(output.getHash().toString() + "<" + output.getIndex(), output.getLabel());
|
labels.put(output.getHash().toString() + "<" + output.getIndex(), output.getLabel());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(output.getStatus() != null) {
|
||||||
|
labels.put(output.getHash().toString() + ":" + output.getIndex(), output.getStatus().toString());
|
||||||
|
}
|
||||||
|
|
||||||
if(output.isSpent() && output.getSpentBy().getLabel() != null && !output.getSpentBy().getLabel().isEmpty()) {
|
if(output.isSpent() && output.getSpentBy().getLabel() != null && !output.getSpentBy().getLabel().isEmpty()) {
|
||||||
labels.put(output.getSpentBy().getHash() + ">" + output.getSpentBy().getIndex(), output.getSpentBy().getLabel());
|
labels.put(output.getSpentBy().getHash() + ">" + output.getSpentBy().getIndex(), output.getSpentBy().getLabel());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ package com.sparrowwallet.drongo.wallet;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
public enum WalletModel {
|
public enum WalletModel {
|
||||||
SEED, SPARROW, BITCOIN_CORE, ELECTRUM, TREZOR_1, TREZOR_T, COLDCARD, LEDGER_NANO_S, LEDGER_NANO_X, DIGITALBITBOX_01, KEEPKEY, SPECTER_DESKTOP, COBO_VAULT, BITBOX_02, SPECTER_DIY, PASSPORT, BLUE_WALLET, KEYSTONE, SEEDSIGNER, CARAVAN, GORDIAN_SEED_TOOL, JADE, LEDGER_NANO_S_PLUS, EPS;
|
SEED, SPARROW, BITCOIN_CORE, ELECTRUM, TREZOR_1, TREZOR_T, COLDCARD, LEDGER_NANO_S, LEDGER_NANO_X, DIGITALBITBOX_01, KEEPKEY, SPECTER_DESKTOP, COBO_VAULT,
|
||||||
|
BITBOX_02, SPECTER_DIY, PASSPORT, BLUE_WALLET, KEYSTONE, SEEDSIGNER, CARAVAN, GORDIAN_SEED_TOOL, JADE, LEDGER_NANO_S_PLUS, EPS, TAPSIGNER, SATSCARD, LABELS, BSMS;
|
||||||
|
|
||||||
public static WalletModel getModel(String model) {
|
public static WalletModel getModel(String model) {
|
||||||
return valueOf(model.toUpperCase(Locale.ROOT));
|
return valueOf(model.toUpperCase(Locale.ROOT));
|
||||||
|
|
@ -50,7 +51,7 @@ public enum WalletModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean alwaysIncludeNonWitnessUtxo() {
|
public boolean alwaysIncludeNonWitnessUtxo() {
|
||||||
if(this == COLDCARD || this == COBO_VAULT || this == PASSPORT || this == KEYSTONE || this == GORDIAN_SEED_TOOL) {
|
if(this == COLDCARD || this == COBO_VAULT || this == PASSPORT || this == KEYSTONE || this == GORDIAN_SEED_TOOL || this == SEEDSIGNER) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,6 +66,10 @@ public enum WalletModel {
|
||||||
return (this == TREZOR_1 || this == KEEPKEY);
|
return (this == TREZOR_1 || this == KEEPKEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isCard() {
|
||||||
|
return (this == TAPSIGNER || this == SATSCARD);
|
||||||
|
}
|
||||||
|
|
||||||
public static WalletModel fromType(String type) {
|
public static WalletModel fromType(String type) {
|
||||||
for(WalletModel model : values()) {
|
for(WalletModel model : values()) {
|
||||||
if(model.getType().equalsIgnoreCase(type)) {
|
if(model.getType().equalsIgnoreCase(type)) {
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,11 @@ public class WalletNode extends Persistable implements Comparable<WalletNode> {
|
||||||
txo.setLabel(label);
|
txo.setLabel(label);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String status = wallet.getDetachedLabels().remove(txo.getHash().toString() + ":" + txo.getIndex());
|
||||||
|
if(status != null && txo.getStatus() == null) {
|
||||||
|
txo.setStatus(Status.valueOf(status));
|
||||||
|
}
|
||||||
|
|
||||||
if(txo.isSpent()) {
|
if(txo.isSpent()) {
|
||||||
String spentByLabel = wallet.getDetachedLabels().remove(txo.getSpentBy().getHash() + ">" + txo.getSpentBy().getIndex());
|
String spentByLabel = wallet.getDetachedLabels().remove(txo.getSpentBy().getHash() + ">" + txo.getSpentBy().getIndex());
|
||||||
if(spentByLabel != null && (txo.getSpentBy().getLabel() == null || txo.getSpentBy().getLabel().isEmpty())) {
|
if(spentByLabel != null && (txo.getSpentBy().getLabel() == null || txo.getSpentBy().getLabel().isEmpty())) {
|
||||||
|
|
@ -163,16 +168,16 @@ public class WalletNode extends Persistable implements Comparable<WalletNode> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<BlockTransactionHashIndex> getUnspentTransactionOutputs() {
|
public Set<BlockTransactionHashIndex> getUnspentTransactionOutputs() {
|
||||||
return getUnspentTransactionOutputs(false);
|
return getTransactionOutputs(List.of(new SpentTxoFilter()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<BlockTransactionHashIndex> getUnspentTransactionOutputs(boolean includeSpentMempoolOutputs) {
|
public Set<BlockTransactionHashIndex> getTransactionOutputs(Collection<TxoFilter> txoFilters) {
|
||||||
if(transactionOutputs.isEmpty()) {
|
if(transactionOutputs.isEmpty()) {
|
||||||
return Collections.emptySet();
|
return Collections.emptySet();
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<BlockTransactionHashIndex> unspentTXOs = new TreeSet<>(transactionOutputs);
|
Set<BlockTransactionHashIndex> unspentTXOs = new TreeSet<>(transactionOutputs);
|
||||||
unspentTXOs.removeIf(txo -> txo.isSpent() && (!includeSpentMempoolOutputs || txo.getSpentBy().getHeight() > 0));
|
unspentTXOs.removeIf(txo -> !txoFilters.stream().allMatch(txoFilter -> txoFilter.isEligible(txo)));
|
||||||
return unspentTXOs;
|
return unspentTXOs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
113
src/test/java/com/sparrowwallet/drongo/crypto/Bip322Test.java
Normal file
113
src/test/java/com/sparrowwallet/drongo/crypto/Bip322Test.java
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
package com.sparrowwallet.drongo.crypto;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.Utils;
|
||||||
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
|
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||||
|
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.security.SignatureException;
|
||||||
|
|
||||||
|
public class Bip322Test {
|
||||||
|
@Test
|
||||||
|
public void getBip322TaggedHash() {
|
||||||
|
byte[] empty = Bip322.getBip322MessageHash("");
|
||||||
|
Assert.assertArrayEquals(Utils.hexToBytes("c90c269c4f8fcbe6880f72a721ddfbf1914268a794cbb21cfafee13770ae19f1"), empty);
|
||||||
|
|
||||||
|
byte[] hello = Bip322.getBip322MessageHash("Hello World");
|
||||||
|
Assert.assertArrayEquals(Utils.hexToBytes("f0eb03b1a75ac6d9847f55c624a99169b5dccba2a31f5b23bea77ba270de0a7a"), hello);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void signMessageBip322() {
|
||||||
|
ECKey privKey = DumpedPrivateKey.fromBase58("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k").getKey();
|
||||||
|
Address address = ScriptType.P2WPKH.getAddress(privKey);
|
||||||
|
Assert.assertEquals("bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", address.toString());
|
||||||
|
|
||||||
|
String signature = Bip322.signMessageBip322(ScriptType.P2WPKH, "", privKey);
|
||||||
|
Assert.assertEquals("AkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=", signature);
|
||||||
|
|
||||||
|
String signature2 = Bip322.signMessageBip322(ScriptType.P2WPKH, "Hello World", privKey);
|
||||||
|
Assert.assertEquals("AkcwRAIgZRfIY3p7/DoVTty6YZbWS71bc5Vct9p9Fia83eRmw2QCICK/ENGfwLtptFluMGs2KsqoNSk89pO7F29zJLUx9a/sASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=", signature2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void verifyMessageBip322Fail() throws InvalidAddressException, SignatureException {
|
||||||
|
Address address = Address.fromString("bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l");
|
||||||
|
String message1 = "";
|
||||||
|
String signature2 = "AkcwRAIgZRfIY3p7/DoVTty6YZbWS71bc5Vct9p9Fia83eRmw2QCICK/ENGfwLtptFluMGs2KsqoNSk89pO7F29zJLUx9a/sASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=";
|
||||||
|
|
||||||
|
Assert.assertFalse(Bip322.verifyMessageBip322(ScriptType.P2WPKH, address, message1, signature2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void verifyMessageBip322() throws InvalidAddressException, SignatureException {
|
||||||
|
Address address = Address.fromString("bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l");
|
||||||
|
String message1 = "";
|
||||||
|
String signature1 = "AkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=";
|
||||||
|
|
||||||
|
Assert.assertTrue(Bip322.verifyMessageBip322(ScriptType.P2WPKH, address, message1, signature1));
|
||||||
|
|
||||||
|
String message2 = "Hello World";
|
||||||
|
String signature2 = "AkcwRAIgZRfIY3p7/DoVTty6YZbWS71bc5Vct9p9Fia83eRmw2QCICK/ENGfwLtptFluMGs2KsqoNSk89pO7F29zJLUx9a/sASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=";
|
||||||
|
Assert.assertTrue(Bip322.verifyMessageBip322(ScriptType.P2WPKH, address, message2, signature2));
|
||||||
|
|
||||||
|
String signature3 = "AkgwRQIhAOzyynlqt93lOKJr+wmmxIens//zPzl9tqIOua93wO6MAiBi5n5EyAcPScOjf1lAqIUIQtr3zKNeavYabHyR8eGhowEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy";
|
||||||
|
Assert.assertTrue(Bip322.verifyMessageBip322(ScriptType.P2WPKH, address, message2, signature3));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void signMessageBip322Taproot() {
|
||||||
|
ECKey privKey = DumpedPrivateKey.fromBase58("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k").getKey();
|
||||||
|
Address address = ScriptType.P2TR.getAddress(privKey);
|
||||||
|
Assert.assertEquals("bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3", address.toString());
|
||||||
|
|
||||||
|
String signature = Bip322.signMessageBip322(ScriptType.P2TR, "Hello World", privKey);
|
||||||
|
Assert.assertEquals("AUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==", signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void verifyMessageBip322Taproot() throws SignatureException {
|
||||||
|
ECKey privKey = DumpedPrivateKey.fromBase58("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k").getKey();
|
||||||
|
Address address = ScriptType.P2TR.getAddress(privKey);
|
||||||
|
Assert.assertEquals("bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3", address.toString());
|
||||||
|
|
||||||
|
String message1 = "Hello World";
|
||||||
|
String signature1 = "AUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==";
|
||||||
|
|
||||||
|
Assert.assertTrue(Bip322.verifyMessageBip322(ScriptType.P2TR, address, message1, signature1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = UnsupportedOperationException.class)
|
||||||
|
public void signMessageBip322NestedSegwit() {
|
||||||
|
ECKey privKey = DumpedPrivateKey.fromBase58("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k").getKey();
|
||||||
|
Address address = ScriptType.P2SH_P2WPKH.getAddress(privKey);
|
||||||
|
Assert.assertEquals("37qyp7jQAzqb2rCBpMvVtLDuuzKAUCVnJb", address.toString());
|
||||||
|
|
||||||
|
String signature = Bip322.signMessageBip322(ScriptType.P2SH_P2WPKH, "Hello World", privKey);
|
||||||
|
Assert.assertEquals("AkcwRAIgHx821fcP3D4R6RsXHF8kXza4d/SqpKGaGu++AEQjJz0CIH9cN5XGDkgkqqF9OMTbYvhgI7Yp9NoHXEgLstjqDOqDASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=", signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = UnsupportedOperationException.class)
|
||||||
|
public void verifyMessageBip322NestedSegwit() throws SignatureException {
|
||||||
|
ECKey privKey = DumpedPrivateKey.fromBase58("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k").getKey();
|
||||||
|
Address address = ScriptType.P2SH_P2WPKH.getAddress(privKey);
|
||||||
|
Assert.assertEquals("37qyp7jQAzqb2rCBpMvVtLDuuzKAUCVnJb", address.toString());
|
||||||
|
|
||||||
|
String message1 = "Hello World";
|
||||||
|
String signature1 = "AkcwRAIgHx821fcP3D4R6RsXHF8kXza4d/SqpKGaGu++AEQjJz0CIH9cN5XGDkgkqqF9OMTbYvhgI7Yp9NoHXEgLstjqDOqDASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=";
|
||||||
|
|
||||||
|
Bip322.verifyMessageBip322(ScriptType.P2SH_P2WPKH, address, message1, signature1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void verifyMessageBip322Multisig() throws SignatureException, InvalidAddressException {
|
||||||
|
Address address = Address.fromString("bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3");
|
||||||
|
|
||||||
|
String message1 = "This will be a p2wsh 3-of-3 multisig BIP 322 signed message";
|
||||||
|
String signature1 = "BQBIMEUCIQDQoXvGKLH58exuujBOta+7+GN7vi0lKwiQxzBpuNuXuAIgIE0XYQlFDOfxbegGYYzlf+tqegleAKE6SXYIa1U+uCcBRzBEAiATegywVl6GWrG9jJuPpNwtgHKyVYCX2yfuSSDRFATAaQIgTLlU6reLQsSIrQSF21z3PtUO2yAUseUWGZqRUIE7VKoBSDBFAiEAgxtpidsU0Z4u/+5RB9cyeQtoCW5NcreLJmWXZ8kXCZMCIBR1sXoEinhZE4CF9P9STGIcMvCuZjY6F5F0XTVLj9SjAWlTIQP3dyWvTZjUENWJowMWBsQrrXCUs20Gu5YF79CG5Ga0XSEDwqI5GVBOuFkFzQOGH5eTExSAj2Z/LDV/hbcvAPQdlJMhA17FuuJd+4wGuj+ZbVxEsFapTKAOwyhfw9qpch52JKxbU64=";
|
||||||
|
|
||||||
|
Bip322.verifyMessageBip322(ScriptType.P2TR, address, message1, signature1);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue