From abb598d3b041a9d0b3d0ba41b5fb9785e2100193 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Mon, 14 Apr 2025 15:49:02 +0200 Subject: [PATCH] add pay to anchor script and address type --- .../sparrowwallet/drongo/address/Address.java | 2 + .../drongo/address/P2AAddress.java | 31 +++++ .../sparrowwallet/drongo/protocol/Script.java | 4 + .../drongo/protocol/ScriptType.java | 128 +++++++++++++++++- .../sparrowwallet/drongo/wallet/Payment.java | 7 +- 5 files changed, 167 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/sparrowwallet/drongo/address/P2AAddress.java diff --git a/src/main/java/com/sparrowwallet/drongo/address/Address.java b/src/main/java/com/sparrowwallet/drongo/address/Address.java index c44fa86..2ad8c47 100644 --- a/src/main/java/com/sparrowwallet/drongo/address/Address.java +++ b/src/main/java/com/sparrowwallet/drongo/address/Address.java @@ -134,6 +134,8 @@ public abstract class Address { byte[] witnessProgram = Bech32.convertBits(convertedProgram, 0, convertedProgram.length, 5, 8, false); if(witnessProgram.length == 32) { return new P2TRAddress(witnessProgram); + } else if(Arrays.equals(witnessProgram, ScriptType.ANCHOR_WITNESS_PROGRAM)) { + return new P2AAddress(witnessProgram); } } } diff --git a/src/main/java/com/sparrowwallet/drongo/address/P2AAddress.java b/src/main/java/com/sparrowwallet/drongo/address/P2AAddress.java new file mode 100644 index 0000000..82c037c --- /dev/null +++ b/src/main/java/com/sparrowwallet/drongo/address/P2AAddress.java @@ -0,0 +1,31 @@ +package com.sparrowwallet.drongo.address; + +import com.sparrowwallet.drongo.Network; +import com.sparrowwallet.drongo.protocol.Bech32; +import com.sparrowwallet.drongo.protocol.ScriptType; + +public class P2AAddress extends Address { + public P2AAddress(byte[] data) { + super(data); + } + + @Override + public int getVersion(Network network) { + return 1; + } + + @Override + public String getAddress(Network network) { + return Bech32.encode(network.getBech32AddressHRP(), getVersion(), data); + } + + @Override + public ScriptType getScriptType() { + return ScriptType.P2A; + } + + @Override + public String getOutputScriptDataType() { + return "Anchor"; + } +} diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/Script.java b/src/main/java/com/sparrowwallet/drongo/protocol/Script.java index b821092..44607fb 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/Script.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/Script.java @@ -212,6 +212,10 @@ public class Script { return addresses.toArray(new Address[addresses.size()]); } + if(P2A.isScriptType(this)) { + return new Address[] { P2A.getAddress(P2A.getDataFromScript(this)) }; + } + throw new NonStandardScriptException("Cannot find addresses in non standard script: " + toString()); } diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/ScriptType.java b/src/main/java/com/sparrowwallet/drongo/protocol/ScriptType.java index 43d0490..c5fc92e 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/ScriptType.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/ScriptType.java @@ -1134,6 +1134,122 @@ public enum ScriptType { public List getAllowedPolicyTypes() { return List.of(SINGLE); } + }, + P2A("P2A", "Anchor (P2A)", "m/86'/0'/0'") { + @Override + public Address getAddress(byte[] data) { + return new P2AAddress(data); + } + + @Override + public Address getAddress(ECKey derivedKey) { + throw new ProtocolException("Cannot create a anchor address with a key"); + } + + @Override + public Address getAddress(Script script) { + throw new ProtocolException("Cannot create a anchor address with a script"); + } + + @Override + public Script getOutputScript(byte[] data) { + List chunks = new ArrayList<>(); + chunks.add(new ScriptChunk(OP_1, null)); + chunks.add(new ScriptChunk(data.length, data)); + + return new Script(chunks); + } + + @Override + public Script getOutputScript(ECKey derivedKey) { + throw new ProtocolException("Cannot create an anchor output script with a key"); + } + + @Override + public Script getOutputScript(Script script) { + throw new ProtocolException("Cannot create an anchor output script with a script"); + } + + @Override + public String getOutputDescriptor(ECKey derivedKey) { + throw new ProtocolException("Cannot create an anchor output descriptor with a key"); + } + + @Override + public String getOutputDescriptor(Script script) { + throw new ProtocolException("Cannot create an anchor output descriptor with a script"); + } + + @Override + public String getDescriptor() { + return "addr("; + } + + @Override + public boolean isScriptType(Script script) { + List 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 (!Arrays.equals(chunk1data, ANCHOR_WITNESS_PROGRAM)) { + return false; + } + return true; + } + + @Override + public byte[] getDataFromScript(Script script) { + return script.chunks.get(1).data; + } + + @Override + public byte[] getHashFromScript(Script script) { + throw new ProtocolException("P2A does not contain a hash"); + } + + @Override + public ECKey getPublicKeyFromScript(Script script) { + throw new ProtocolException("P2A does not contain a key"); + } + + @Override + public Script getScriptSig(Script scriptPubKey, ECKey pubKey, TransactionSignature signature) { + if(!isScriptType(scriptPubKey)) { + throw new ProtocolException("Provided scriptPubKey is not a " + getName() + " script"); + } + + return new Script(new byte[0]); + } + + @Override + public TransactionInput addSpendingInput(Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) { + Script scriptSig = getScriptSig(prevOutput.getScript(), pubKey, signature); + return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig); + } + + @Override + public Script getMultisigScriptSig(Script scriptPubKey, int threshold, Map pubKeySignatures) { + throw new UnsupportedOperationException("Constructing Taproot inputs is not yet supported"); + } + + @Override + public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, Map pubKeySignatures) { + throw new UnsupportedOperationException("Constructing Taproot inputs is not yet supported"); + } + + @Override + public TransactionSignature.Type getSignatureType() { + return TransactionSignature.Type.SCHNORR; + }; + + @Override + public List getAllowedPolicyTypes() { + return Collections.emptyList(); + } }; private final String name; @@ -1250,6 +1366,10 @@ public enum ScriptType { public abstract boolean isScriptType(Script script); + public byte[] getDataFromScript(Script script) { + throw new ProtocolException("Script type " + this + " does not contain data"); + } + public abstract byte[] getHashFromScript(Script script); public Address[] getAddresses(Script script) { @@ -1282,11 +1402,13 @@ public enum ScriptType { 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[] ADDRESSABLE_TYPES = {P2PKH, P2SH, P2SH_P2WPKH, P2SH_P2WSH, P2WPKH, P2WSH, P2TR, P2A}; public static final ScriptType[] NON_WITNESS_TYPES = {P2PK, P2PKH, P2SH}; - public static final ScriptType[] WITNESS_TYPES = {P2SH_P2WPKH, P2SH_P2WSH, P2WPKH, P2WSH, P2TR}; + public static final ScriptType[] WITNESS_TYPES = {P2SH_P2WPKH, P2SH_P2WSH, P2WPKH, P2WSH, P2TR, P2A}; + + public static final byte[] ANCHOR_WITNESS_PROGRAM = new byte[] {78, 115}; public static List getScriptTypesForPolicyType(PolicyType policyType) { return Arrays.stream(values()).filter(scriptType -> scriptType.isAllowed(policyType)).collect(Collectors.toList()); @@ -1364,6 +1486,8 @@ public enum ScriptType { } else if(P2TR.equals(this)) { //Assume a default keypath spend return (32 + 4 + 1 + ((double)66 / WITNESS_SCALE_FACTOR) + 4); + } else if(P2A.equals(this)) { + return 32 + 4 + 1 + 4; } else if(Arrays.asList(WITNESS_TYPES).contains(this)) { //Return length of spending input with 75% discount to script size return (32 + 4 + 1 + ((double)107 / WITNESS_SCALE_FACTOR) + 4); diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/Payment.java b/src/main/java/com/sparrowwallet/drongo/wallet/Payment.java index 2d0c428..716a8f0 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/Payment.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/Payment.java @@ -1,6 +1,7 @@ package com.sparrowwallet.drongo.wallet; import com.sparrowwallet.drongo.address.Address; +import com.sparrowwallet.drongo.address.P2AAddress; public class Payment { private Address address; @@ -15,10 +16,10 @@ public class Payment { public Payment(Address address, String label, long amount, boolean sendMax, Type type) { this.address = address; - this.label = label; + this.label = label == null && address instanceof P2AAddress ? address.getOutputScriptDataType() : label; this.amount = amount; this.sendMax = sendMax; - this.type = type; + this.type = type == Type.DEFAULT && address instanceof P2AAddress ? Type.ANCHOR : type; } public Address getAddress() { @@ -62,6 +63,6 @@ public class Payment { } public enum Type { - DEFAULT, WHIRLPOOL_FEE, FAKE_MIX, MIX; + DEFAULT, WHIRLPOOL_FEE, FAKE_MIX, MIX, ANCHOR; } }