mirror of
https://github.com/sparrowwallet/drongo.git
synced 2024-11-02 18:26:43 +00:00
add support for sending to taproot addresses
This commit is contained in:
parent
107a165fc1
commit
967a2c2026
10 changed files with 279 additions and 24 deletions
|
@ -108,15 +108,29 @@ public abstract class Address {
|
||||||
Bech32.Bech32Data data = Bech32.decode(address);
|
Bech32.Bech32Data data = Bech32.decode(address);
|
||||||
if(data.hrp.equals(network.getBech32AddressHRP())) {
|
if(data.hrp.equals(network.getBech32AddressHRP())) {
|
||||||
int witnessVersion = data.data[0];
|
int witnessVersion = data.data[0];
|
||||||
if (witnessVersion == 0) {
|
if(witnessVersion == 0) {
|
||||||
|
if(data.encoding != Bech32.Encoding.BECH32) {
|
||||||
|
throw new InvalidAddressException("Invalid address - witness version is 0 but encoding is " + data.encoding);
|
||||||
|
}
|
||||||
|
|
||||||
byte[] convertedProgram = Arrays.copyOfRange(data.data, 1, data.data.length);
|
byte[] convertedProgram = Arrays.copyOfRange(data.data, 1, data.data.length);
|
||||||
byte[] witnessProgram = Bech32.convertBits(convertedProgram, 0, convertedProgram.length, 5, 8, false);
|
byte[] witnessProgram = Bech32.convertBits(convertedProgram, 0, convertedProgram.length, 5, 8, false);
|
||||||
if (witnessProgram.length == 20) {
|
if(witnessProgram.length == 20) {
|
||||||
return new P2WPKHAddress(witnessProgram);
|
return new P2WPKHAddress(witnessProgram);
|
||||||
}
|
}
|
||||||
if (witnessProgram.length == 32) {
|
if(witnessProgram.length == 32) {
|
||||||
return new P2WSHAddress(witnessProgram);
|
return new P2WSHAddress(witnessProgram);
|
||||||
}
|
}
|
||||||
|
} else if(witnessVersion == 1) {
|
||||||
|
if(data.encoding != Bech32.Encoding.BECH32M) {
|
||||||
|
throw new InvalidAddressException("Invalid address - witness version is 1 but encoding is " + data.encoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] convertedProgram = Arrays.copyOfRange(data.data, 1, data.data.length);
|
||||||
|
byte[] witnessProgram = Bech32.convertBits(convertedProgram, 0, convertedProgram.length, 5, 8, false);
|
||||||
|
if(witnessProgram.length == 32) {
|
||||||
|
return new P2TRAddress(witnessProgram);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|
|
@ -6,7 +6,7 @@ import com.sparrowwallet.drongo.protocol.Script;
|
||||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
|
|
||||||
public class P2PKAddress extends Address {
|
public class P2PKAddress extends Address {
|
||||||
private byte[] pubKey;
|
private final byte[] pubKey;
|
||||||
|
|
||||||
public P2PKAddress(byte[] pubKey) {
|
public P2PKAddress(byte[] pubKey) {
|
||||||
super(Utils.sha256hash160(pubKey));
|
super(Utils.sha256hash160(pubKey));
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
package com.sparrowwallet.drongo.address;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.Network;
|
||||||
|
import com.sparrowwallet.drongo.Utils;
|
||||||
|
import com.sparrowwallet.drongo.protocol.Bech32;
|
||||||
|
import com.sparrowwallet.drongo.protocol.Script;
|
||||||
|
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
|
|
||||||
|
public class P2TRAddress extends Address {
|
||||||
|
private final byte[] pubKey;
|
||||||
|
|
||||||
|
public P2TRAddress(byte[] pubKey) {
|
||||||
|
super(Utils.sha256hash160(pubKey));
|
||||||
|
this.pubKey = pubKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getVersion(Network network) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAddress(Network network) {
|
||||||
|
return Bech32.encode(network.getBech32AddressHRP(), getVersion(), pubKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ScriptType getScriptType() {
|
||||||
|
return ScriptType.P2TR;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Script getOutputScript() {
|
||||||
|
return getScriptType().getOutputScript(pubKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getOutputScriptData() {
|
||||||
|
return pubKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getOutputScriptDataType() {
|
||||||
|
return "Taproot";
|
||||||
|
}
|
||||||
|
}
|
|
@ -340,6 +340,13 @@ public class ECKey implements EncryptableItem {
|
||||||
return pub.getEncoded();
|
return pub.getEncoded();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the x coordinate of the raw public key value. This appears in transaction scriptPubKeys for Taproot outputs.
|
||||||
|
*/
|
||||||
|
public byte[] getPubKeyXCoord() {
|
||||||
|
return pub.getEncodedXCoord();
|
||||||
|
}
|
||||||
|
|
||||||
/** Gets the public key in the form of an elliptic curve point object from Bouncy Castle. */
|
/** Gets the public key in the form of an elliptic curve point object from Bouncy Castle. */
|
||||||
public ECPoint getPubKeyPoint() {
|
public ECPoint getPubKeyPoint() {
|
||||||
return pub.get();
|
return pub.get();
|
||||||
|
@ -625,8 +632,10 @@ public class ECKey implements EncryptableItem {
|
||||||
* Returns true if the given pubkey is canonical, i.e. the correct length taking into account compression.
|
* Returns true if the given pubkey is canonical, i.e. the correct length taking into account compression.
|
||||||
*/
|
*/
|
||||||
public static boolean isPubKeyCanonical(byte[] pubkey) {
|
public static boolean isPubKeyCanonical(byte[] pubkey) {
|
||||||
if (pubkey.length < 33)
|
if (pubkey.length < 32)
|
||||||
return false;
|
return false;
|
||||||
|
if (pubkey.length == 32)
|
||||||
|
return true;
|
||||||
if (pubkey[0] == 0x04) {
|
if (pubkey[0] == 0x04) {
|
||||||
// Uncompressed pubkey
|
// Uncompressed pubkey
|
||||||
if (pubkey.length != 65)
|
if (pubkey.length != 65)
|
||||||
|
@ -644,7 +653,7 @@ public class ECKey implements EncryptableItem {
|
||||||
* Returns true if the given pubkey is in its compressed form.
|
* Returns true if the given pubkey is in its compressed form.
|
||||||
*/
|
*/
|
||||||
public static boolean isPubKeyCompressed(byte[] encoded) {
|
public static boolean isPubKeyCompressed(byte[] encoded) {
|
||||||
if (encoded.length == 33 && (encoded[0] == 0x02 || encoded[0] == 0x03))
|
if (encoded.length == 32 || (encoded.length == 33 && (encoded[0] == 0x02 || encoded[0] == 0x03)))
|
||||||
return true;
|
return true;
|
||||||
else if (encoded.length == 65 && encoded[0] == 0x04)
|
else if (encoded.length == 65 && encoded[0] == 0x04)
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -20,7 +20,7 @@ public class LazyECPoint {
|
||||||
|
|
||||||
public LazyECPoint(ECCurve curve, byte[] bits) {
|
public LazyECPoint(ECCurve curve, byte[] bits) {
|
||||||
this.curve = curve;
|
this.curve = curve;
|
||||||
this.bits = bits;
|
this.bits = (bits != null && bits.length == 32 ? addYCoord(bits) : bits);
|
||||||
this.compressed = ECKey.isPubKeyCompressed(bits);
|
this.compressed = ECKey.isPubKeyCompressed(bits);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,6 +61,13 @@ public class LazyECPoint {
|
||||||
return get().getEncoded(compressed);
|
return get().getEncoded(compressed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] getEncodedXCoord() {
|
||||||
|
byte[] compressed = getEncoded(true);
|
||||||
|
byte[] xcoord = new byte[32];
|
||||||
|
System.arraycopy(compressed, 1, xcoord, 0, 32);
|
||||||
|
return xcoord;
|
||||||
|
}
|
||||||
|
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return Hex.toHexString(getEncoded());
|
return Hex.toHexString(getEncoded());
|
||||||
}
|
}
|
||||||
|
@ -80,4 +87,11 @@ public class LazyECPoint {
|
||||||
private byte[] getCanonicalEncoding() {
|
private byte[] getCanonicalEncoding() {
|
||||||
return getEncoded(true);
|
return getEncoded(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static byte[] addYCoord(byte[] xcoord) {
|
||||||
|
byte[] compressed = new byte[33];
|
||||||
|
compressed[0] = 0x02;
|
||||||
|
System.arraycopy(xcoord, 0, compressed, 1, 32);
|
||||||
|
return compressed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,10 +39,18 @@ public class Bech32 {
|
||||||
public static class Bech32Data {
|
public static class Bech32Data {
|
||||||
public final String hrp;
|
public final String hrp;
|
||||||
public final byte[] data;
|
public final byte[] data;
|
||||||
|
public final Encoding encoding;
|
||||||
|
|
||||||
private Bech32Data(final String hrp, final byte[] data) {
|
private Bech32Data(final String hrp, final byte[] data) {
|
||||||
this.hrp = hrp;
|
this.hrp = hrp;
|
||||||
this.data = data;
|
this.data = data;
|
||||||
|
this.encoding = (data[0] == 0x00 ? Encoding.BECH32 : Encoding.BECH32M);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Bech32Data(String hrp, byte[] data, Encoding encoding) {
|
||||||
|
this.hrp = hrp;
|
||||||
|
this.data = data;
|
||||||
|
this.encoding = encoding;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,7 +72,7 @@ public class Bech32 {
|
||||||
/** Expand a HRP for use in checksum computation. */
|
/** Expand a HRP for use in checksum computation. */
|
||||||
private static byte[] expandHrp(final String hrp) {
|
private static byte[] expandHrp(final String hrp) {
|
||||||
int hrpLength = hrp.length();
|
int hrpLength = hrp.length();
|
||||||
byte ret[] = new byte[hrpLength * 2 + 1];
|
byte[] ret = new byte[hrpLength * 2 + 1];
|
||||||
for (int i = 0; i < hrpLength; ++i) {
|
for (int i = 0; i < hrpLength; ++i) {
|
||||||
int c = hrp.charAt(i) & 0x7f; // Limit to standard 7-bit ASCII
|
int c = hrp.charAt(i) & 0x7f; // Limit to standard 7-bit ASCII
|
||||||
ret[i] = (byte) ((c >>> 5) & 0x07);
|
ret[i] = (byte) ((c >>> 5) & 0x07);
|
||||||
|
@ -75,21 +83,29 @@ public class Bech32 {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Verify a checksum. */
|
/** Verify a checksum. */
|
||||||
private static boolean verifyChecksum(final String hrp, final byte[] values) {
|
private static Encoding verifyChecksum(final String hrp, final byte[] values) {
|
||||||
byte[] hrpExpanded = expandHrp(hrp);
|
byte[] hrpExpanded = expandHrp(hrp);
|
||||||
byte[] combined = new byte[hrpExpanded.length + values.length];
|
byte[] combined = new byte[hrpExpanded.length + values.length];
|
||||||
System.arraycopy(hrpExpanded, 0, combined, 0, hrpExpanded.length);
|
System.arraycopy(hrpExpanded, 0, combined, 0, hrpExpanded.length);
|
||||||
System.arraycopy(values, 0, combined, hrpExpanded.length, values.length);
|
System.arraycopy(values, 0, combined, hrpExpanded.length, values.length);
|
||||||
return polymod(combined) == 1;
|
|
||||||
|
int check = polymod(combined);
|
||||||
|
for(Encoding encoding : Encoding.values()) {
|
||||||
|
if(check == encoding.checksumConstant) {
|
||||||
|
return encoding;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Create a checksum. */
|
/** Create a checksum. */
|
||||||
private static byte[] createChecksum(final String hrp, final byte[] values) {
|
private static byte[] createChecksum(final String hrp, Encoding encoding, final byte[] values) {
|
||||||
byte[] hrpExpanded = expandHrp(hrp);
|
byte[] hrpExpanded = expandHrp(hrp);
|
||||||
byte[] enc = new byte[hrpExpanded.length + values.length + 6];
|
byte[] enc = new byte[hrpExpanded.length + values.length + 6];
|
||||||
System.arraycopy(hrpExpanded, 0, enc, 0, hrpExpanded.length);
|
System.arraycopy(hrpExpanded, 0, enc, 0, hrpExpanded.length);
|
||||||
System.arraycopy(values, 0, enc, hrpExpanded.length, values.length);
|
System.arraycopy(values, 0, enc, hrpExpanded.length, values.length);
|
||||||
int mod = polymod(enc) ^ 1;
|
int mod = polymod(enc) ^ encoding.checksumConstant;
|
||||||
byte[] ret = new byte[6];
|
byte[] ret = new byte[6];
|
||||||
for (int i = 0; i < 6; ++i) {
|
for (int i = 0; i < 6; ++i) {
|
||||||
ret[i] = (byte) ((mod >>> (5 * (5 - i))) & 31);
|
ret[i] = (byte) ((mod >>> (5 * (5 - i))) & 31);
|
||||||
|
@ -99,16 +115,17 @@ public class Bech32 {
|
||||||
|
|
||||||
/** Encode a Bech32 string. */
|
/** Encode a Bech32 string. */
|
||||||
public static String encode(final Bech32Data bech32) {
|
public static String encode(final Bech32Data bech32) {
|
||||||
return encode(bech32.hrp, bech32.data);
|
return encode(bech32.hrp, bech32.encoding, bech32.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Encode a Bech32 string. */
|
/** Encode a Bech32 string. */
|
||||||
public static String encode(String hrp, int version, final byte[] values) {
|
public static String encode(String hrp, int version, final byte[] values) {
|
||||||
return encode(hrp, encode(0, values));
|
Encoding encoding = (version == 0 ? Encoding.BECH32 : Encoding.BECH32M);
|
||||||
|
return encode(hrp, encoding, encode(version, values));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Encode a Bech32 string. */
|
/** Encode a Bech32 string. */
|
||||||
public static String encode(String hrp, final byte[] values) {
|
public static String encode(String hrp, Encoding encoding, final byte[] values) {
|
||||||
if(hrp.length() < 1) {
|
if(hrp.length() < 1) {
|
||||||
throw new ProtocolException("Human-readable part is too short");
|
throw new ProtocolException("Human-readable part is too short");
|
||||||
}
|
}
|
||||||
|
@ -118,7 +135,7 @@ public class Bech32 {
|
||||||
}
|
}
|
||||||
|
|
||||||
hrp = hrp.toLowerCase(Locale.ROOT);
|
hrp = hrp.toLowerCase(Locale.ROOT);
|
||||||
byte[] checksum = createChecksum(hrp, values);
|
byte[] checksum = createChecksum(hrp, encoding, values);
|
||||||
byte[] combined = new byte[values.length + checksum.length];
|
byte[] combined = new byte[values.length + checksum.length];
|
||||||
System.arraycopy(values, 0, combined, 0, values.length);
|
System.arraycopy(values, 0, combined, 0, values.length);
|
||||||
System.arraycopy(checksum, 0, combined, values.length, checksum.length);
|
System.arraycopy(checksum, 0, combined, values.length, checksum.length);
|
||||||
|
@ -163,14 +180,18 @@ public class Bech32 {
|
||||||
values[i] = CHARSET_REV[c];
|
values[i] = CHARSET_REV[c];
|
||||||
}
|
}
|
||||||
String hrp = str.substring(0, pos).toLowerCase(Locale.ROOT);
|
String hrp = str.substring(0, pos).toLowerCase(Locale.ROOT);
|
||||||
if (!verifyChecksum(hrp, values)) throw new ProtocolException("Invalid checksum");
|
Encoding encoding = verifyChecksum(hrp, values);
|
||||||
return new Bech32Data(hrp, Arrays.copyOfRange(values, 0, values.length - 6));
|
if(encoding == null) {
|
||||||
|
throw new ProtocolException("Invalid checksum");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Bech32Data(hrp, Arrays.copyOfRange(values, 0, values.length - 6), encoding);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] encode(int witnessVersion, byte[] witnessProgram) {
|
private static byte[] encode(int witnessVersion, byte[] witnessProgram) {
|
||||||
byte[] convertedProgram = convertBits(witnessProgram, 0, witnessProgram.length, 8, 5, true);
|
byte[] convertedProgram = convertBits(witnessProgram, 0, witnessProgram.length, 8, 5, true);
|
||||||
byte[] bytes = new byte[1 + convertedProgram.length];
|
byte[] bytes = new byte[1 + convertedProgram.length];
|
||||||
bytes[0] = (byte) (Script.encodeToOpN(witnessVersion) & 0xff);
|
bytes[0] = (byte)(witnessVersion & 0xff);
|
||||||
System.arraycopy(convertedProgram, 0, bytes, 1, convertedProgram.length);
|
System.arraycopy(convertedProgram, 0, bytes, 1, convertedProgram.length);
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
@ -206,4 +227,14 @@ public class Bech32 {
|
||||||
}
|
}
|
||||||
return out.toByteArray();
|
return out.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum Encoding {
|
||||||
|
BECH32(1), BECH32M(0x2bc830a3);
|
||||||
|
|
||||||
|
private final int checksumConstant;
|
||||||
|
|
||||||
|
Encoding(int checksumConstant) {
|
||||||
|
this.checksumConstant = checksumConstant;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -135,6 +135,21 @@ public class Script {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>If the program somehow pays to a pubkey, returns the pubkey.</p>
|
||||||
|
*
|
||||||
|
* <p>Otherwise this method throws a ScriptException.</p>
|
||||||
|
*/
|
||||||
|
public ECKey getPubKey() throws ProtocolException {
|
||||||
|
for(ScriptType scriptType : SINGLE_KEY_TYPES) {
|
||||||
|
if(scriptType.isScriptType(this)) {
|
||||||
|
return scriptType.getPublicKeyFromScript(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ProtocolException("Script not a standard form that contains a single key");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>If the program somehow pays to a hash, returns the hash.</p>
|
* <p>If the program somehow pays to a hash, returns the hash.</p>
|
||||||
*
|
*
|
||||||
|
@ -160,8 +175,10 @@ public class Script {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(P2PK.isScriptType(this)) {
|
for(ScriptType scriptType : SINGLE_KEY_TYPES) {
|
||||||
return new Address[] { P2PK.getAddress(P2PK.getPublicKeyFromScript(this).getPubKey()) };
|
if(scriptType.isScriptType(this)) {
|
||||||
|
return new Address[] { scriptType.getAddress(scriptType.getPublicKeyFromScript(this)) };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(MULTISIG.isScriptType(this)) {
|
if(MULTISIG.isScriptType(this)) {
|
||||||
|
|
|
@ -974,6 +974,106 @@ public enum ScriptType {
|
||||||
public List<PolicyType> getAllowedPolicyTypes() {
|
public List<PolicyType> getAllowedPolicyTypes() {
|
||||||
return List.of(MULTI, CUSTOM);
|
return List.of(MULTI, CUSTOM);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
P2TR("P2TR", "Taproot (P2TR)", "m/6789'/0'/0'") {
|
||||||
|
@Override
|
||||||
|
public Address getAddress(byte[] pubKey) {
|
||||||
|
return new P2TRAddress(pubKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Address getAddress(ECKey key) {
|
||||||
|
return getAddress(key.getPubKeyXCoord());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Address getAddress(Script script) {
|
||||||
|
throw new ProtocolException("Cannot create a taproot address without a keypath");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Script getOutputScript(byte[] pubKey) {
|
||||||
|
List<ScriptChunk> chunks = new ArrayList<>();
|
||||||
|
chunks.add(new ScriptChunk(OP_1, null));
|
||||||
|
chunks.add(new ScriptChunk(pubKey.length, pubKey));
|
||||||
|
|
||||||
|
return new Script(chunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Script getOutputScript(ECKey key) {
|
||||||
|
return getOutputScript(key.getPubKeyXCoord());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Script getOutputScript(Script script) {
|
||||||
|
throw new ProtocolException("Cannot create a taproot output script without a keypath");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getOutputDescriptor(ECKey key) {
|
||||||
|
return getDescriptor() + Utils.bytesToHex(key.getPubKeyXCoord()) + getCloseDescriptor();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getOutputDescriptor(Script script) {
|
||||||
|
throw new ProtocolException("Cannot create a taproot output descriptor without a keypath");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescriptor() {
|
||||||
|
return "tr(";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isScriptType(Script script) {
|
||||||
|
List<ScriptChunk> chunks = script.chunks;
|
||||||
|
if (chunks.size() != 2)
|
||||||
|
return false;
|
||||||
|
if (!chunks.get(0).equalsOpCode(OP_1))
|
||||||
|
return false;
|
||||||
|
byte[] chunk1data = chunks.get(1).data;
|
||||||
|
if (chunk1data == null)
|
||||||
|
return false;
|
||||||
|
if (chunk1data.length != 32)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getHashFromScript(Script script) {
|
||||||
|
throw new ProtocolException("P2TR script does not contain a hash, use getPublicKeyFromScript(script) to retrieve public key");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ECKey getPublicKeyFromScript(Script script) {
|
||||||
|
return ECKey.fromPublicOnly(script.chunks.get(1).data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Script getScriptSig(Script scriptPubKey, ECKey pubKey, TransactionSignature signature) {
|
||||||
|
throw new UnsupportedOperationException("Constructing Taproot inputs is not yet supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TransactionInput addSpendingInput(Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
|
||||||
|
throw new UnsupportedOperationException("Constructing Taproot inputs is not yet supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Script getMultisigScriptSig(Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||||
|
throw new UnsupportedOperationException("Constructing Taproot inputs is not yet supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
|
||||||
|
throw new UnsupportedOperationException("Constructing Taproot inputs is not yet supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<PolicyType> getAllowedPolicyTypes() {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private final String name;
|
private final String name;
|
||||||
|
@ -1087,18 +1187,22 @@ public enum ScriptType {
|
||||||
|
|
||||||
public abstract TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures);
|
public abstract TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures);
|
||||||
|
|
||||||
|
public static final ScriptType[] SINGLE_KEY_TYPES = {P2PK, P2TR};
|
||||||
|
|
||||||
public static final ScriptType[] SINGLE_HASH_TYPES = {P2PKH, P2SH, P2SH_P2WPKH, P2SH_P2WSH, P2WPKH, P2WSH};
|
public static final ScriptType[] SINGLE_HASH_TYPES = {P2PKH, P2SH, P2SH_P2WPKH, P2SH_P2WSH, P2WPKH, P2WSH};
|
||||||
|
|
||||||
|
public static final ScriptType[] ADDRESSABLE_TYPES = {P2PKH, P2SH, P2SH_P2WPKH, P2SH_P2WSH, P2WPKH, P2WSH, P2TR};
|
||||||
|
|
||||||
public static final ScriptType[] NON_WITNESS_TYPES = {P2PK, P2PKH, P2SH};
|
public static final ScriptType[] NON_WITNESS_TYPES = {P2PK, P2PKH, P2SH};
|
||||||
|
|
||||||
public static final ScriptType[] WITNESS_TYPES = {P2SH_P2WPKH, P2SH_P2WSH, P2WPKH, P2WSH};
|
public static final ScriptType[] WITNESS_TYPES = {P2SH_P2WPKH, P2SH_P2WSH, P2WPKH, P2WSH, P2TR};
|
||||||
|
|
||||||
public static List<ScriptType> getScriptTypesForPolicyType(PolicyType policyType) {
|
public static List<ScriptType> getScriptTypesForPolicyType(PolicyType policyType) {
|
||||||
return Arrays.stream(values()).filter(scriptType -> scriptType.isAllowed(policyType)).collect(Collectors.toList());
|
return Arrays.stream(values()).filter(scriptType -> scriptType.isAllowed(policyType)).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<ScriptType> getAddressableScriptTypes(PolicyType policyType) {
|
public static List<ScriptType> getAddressableScriptTypes(PolicyType policyType) {
|
||||||
return Arrays.stream(values()).filter(scriptType -> scriptType.isAllowed(policyType) && Arrays.asList(SINGLE_HASH_TYPES).contains(scriptType)).collect(Collectors.toList());
|
return Arrays.stream(ADDRESSABLE_TYPES).filter(scriptType -> scriptType.isAllowed(policyType)).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ScriptType getType(Script script) {
|
public static ScriptType getType(Script script) {
|
||||||
|
@ -1166,6 +1270,9 @@ public enum ScriptType {
|
||||||
return (32 + 4 + 1 + 13 + (107 / WITNESS_SCALE_FACTOR) + 4);
|
return (32 + 4 + 1 + 13 + (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 + (107 / WITNESS_SCALE_FACTOR) + 4);
|
||||||
|
} else if(P2TR.equals(this)) {
|
||||||
|
//Assume a default keypath spend
|
||||||
|
return (32 + 4 + 1 + (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 + (107 / WITNESS_SCALE_FACTOR) + 4);
|
||||||
|
|
|
@ -443,7 +443,7 @@ public class Wallet extends Persistable {
|
||||||
public int getNoInputsWeightUnits(List<Payment> payments) {
|
public int getNoInputsWeightUnits(List<Payment> payments) {
|
||||||
Transaction transaction = new Transaction();
|
Transaction transaction = new Transaction();
|
||||||
if(Arrays.asList(ScriptType.WITNESS_TYPES).contains(getScriptType())) {
|
if(Arrays.asList(ScriptType.WITNESS_TYPES).contains(getScriptType())) {
|
||||||
transaction.setSegwitVersion(0);
|
transaction.setSegwitVersion(1);
|
||||||
}
|
}
|
||||||
for(Payment payment : payments) {
|
for(Payment payment : payments) {
|
||||||
transaction.addOutput(payment.getAmount(), payment.getAddress());
|
transaction.addOutput(payment.getAmount(), payment.getAddress());
|
||||||
|
|
|
@ -49,6 +49,14 @@ public class AddressTest {
|
||||||
Address address10 = Address.fromString(Network.SIGNET, "2NCZUtUt6gzXyBiPEQi5yQyrgR6f6F6Ki6A");
|
Address address10 = Address.fromString(Network.SIGNET, "2NCZUtUt6gzXyBiPEQi5yQyrgR6f6F6Ki6A");
|
||||||
Assert.assertTrue(address10 instanceof P2SHAddress);
|
Assert.assertTrue(address10 instanceof P2SHAddress);
|
||||||
Assert.assertEquals("2NCZUtUt6gzXyBiPEQi5yQyrgR6f6F6Ki6A", address10.toString(Network.SIGNET));
|
Assert.assertEquals("2NCZUtUt6gzXyBiPEQi5yQyrgR6f6F6Ki6A", address10.toString(Network.SIGNET));
|
||||||
|
|
||||||
|
Address address11 = Address.fromString("bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0");
|
||||||
|
Assert.assertTrue(address11 instanceof P2TRAddress);
|
||||||
|
Assert.assertEquals("bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0", address11.toString());
|
||||||
|
|
||||||
|
Address address12 = Address.fromString(Network.TESTNET, "tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c");
|
||||||
|
Assert.assertTrue(address12 instanceof P2TRAddress);
|
||||||
|
Assert.assertEquals("tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c", address12.toString(Network.TESTNET));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -74,6 +82,10 @@ public class AddressTest {
|
||||||
Address address9 = Address.fromString("2NCZUtUt6gzXyBiPEQi5yQyrgR6f6F6Ki6A");
|
Address address9 = Address.fromString("2NCZUtUt6gzXyBiPEQi5yQyrgR6f6F6Ki6A");
|
||||||
Assert.assertTrue(address9 instanceof P2SHAddress);
|
Assert.assertTrue(address9 instanceof P2SHAddress);
|
||||||
Assert.assertEquals("2NCZUtUt6gzXyBiPEQi5yQyrgR6f6F6Ki6A", address9.toString());
|
Assert.assertEquals("2NCZUtUt6gzXyBiPEQi5yQyrgR6f6F6Ki6A", address9.toString());
|
||||||
|
|
||||||
|
Address address12 = Address.fromString("tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c");
|
||||||
|
Assert.assertTrue(address12 instanceof P2TRAddress);
|
||||||
|
Assert.assertEquals("tb1pqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesf3hn0c", address12.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -119,6 +131,11 @@ public class AddressTest {
|
||||||
Address address1 = Address.fromString("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmb3");
|
Address address1 = Address.fromString("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmb3");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test(expected = InvalidAddressException.class)
|
||||||
|
public void invalidEncodingAddressTest() throws InvalidAddressException {
|
||||||
|
Address address1 = Address.fromString("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kemeawh");
|
||||||
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
public void tearDown() throws Exception {
|
public void tearDown() throws Exception {
|
||||||
Network.set(null);
|
Network.set(null);
|
||||||
|
|
Loading…
Reference in a new issue