forked from Qortal/qortal
Send BTCACCT refunds to first unused received address instead of address derived from tradePrivateKey.
Added BTC.getUnusedReceiveAddress() to support above.
This commit is contained in:
parent
76485010ad
commit
9393689037
@ -22,6 +22,9 @@ public class CrossChainBitcoinRefundRequest {
|
|||||||
@Schema(description = "Bitcoin miner fee", example = "0.00001000")
|
@Schema(description = "Bitcoin miner fee", example = "0.00001000")
|
||||||
public BigDecimal bitcoinMinerFee;
|
public BigDecimal bitcoinMinerFee;
|
||||||
|
|
||||||
|
@Schema(description = "Bitcoin HASH160(public key) for receiving funds, or omit to derive from private key", example = "u17kBVKkKSp12oUzaxFwNnq1JZf")
|
||||||
|
public byte[] receivingAccountInfo;
|
||||||
|
|
||||||
public CrossChainBitcoinRefundRequest() {
|
public CrossChainBitcoinRefundRequest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -719,6 +719,13 @@ public class CrossChainResource {
|
|||||||
if (refundRequest.atAddress == null || !Crypto.isValidAtAddress(refundRequest.atAddress))
|
if (refundRequest.atAddress == null || !Crypto.isValidAtAddress(refundRequest.atAddress))
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||||
|
|
||||||
|
if (refundRequest.receivingAccountInfo == null)
|
||||||
|
refundRequest.receivingAccountInfo = refundKey.getPubKeyHash();
|
||||||
|
|
||||||
|
if (refundRequest.receivingAccountInfo.length != 20)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
|
||||||
|
|
||||||
|
|
||||||
// Extract data from cross-chain trading AT
|
// Extract data from cross-chain trading AT
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
ATData atData = fetchAtDataWithChecking(repository, refundRequest.atAddress);
|
ATData atData = fetchAtDataWithChecking(repository, refundRequest.atAddress);
|
||||||
@ -756,7 +763,7 @@ public class CrossChainResource {
|
|||||||
|
|
||||||
Coin refundAmount = Coin.valueOf(p2shBalance - refundRequest.bitcoinMinerFee.unscaledValue().longValue());
|
Coin refundAmount = Coin.valueOf(p2shBalance - refundRequest.bitcoinMinerFee.unscaledValue().longValue());
|
||||||
|
|
||||||
org.bitcoinj.core.Transaction refundTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime);
|
org.bitcoinj.core.Transaction refundTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime, refundRequest.receivingAccountInfo);
|
||||||
boolean wasBroadcast = BTC.getInstance().broadcastTransaction(refundTransaction);
|
boolean wasBroadcast = BTC.getInstance().broadcastTransaction(refundTransaction);
|
||||||
|
|
||||||
if (!wasBroadcast)
|
if (!wasBroadcast)
|
||||||
|
@ -980,7 +980,17 @@ public class TradeBot {
|
|||||||
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||||
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress);
|
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress);
|
||||||
|
|
||||||
Transaction p2shRefundTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, crossChainTradeData.lockTimeB);
|
|
||||||
|
// Determine receive address for refund
|
||||||
|
String receiveAddress = BTC.getInstance().getUnusedReceiveAddress(tradeBotData.getXprv58());
|
||||||
|
if (receiveAddress == null) {
|
||||||
|
LOGGER.debug(() -> String.format("Couldn't determine a receive address for P2SH-B refund?"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Address receiving = Address.fromString(BTC.getInstance().getNetworkParameters(), receiveAddress);
|
||||||
|
|
||||||
|
Transaction p2shRefundTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, crossChainTradeData.lockTimeB, receiving.getHash());
|
||||||
if (!BTC.getInstance().broadcastTransaction(p2shRefundTransaction)) {
|
if (!BTC.getInstance().broadcastTransaction(p2shRefundTransaction)) {
|
||||||
// We couldn't refund P2SH-B at this time
|
// We couldn't refund P2SH-B at this time
|
||||||
LOGGER.debug(() -> String.format("Couldn't broadcast P2SH-B refund transaction?"));
|
LOGGER.debug(() -> String.format("Couldn't broadcast P2SH-B refund transaction?"));
|
||||||
@ -1025,7 +1035,16 @@ public class TradeBot {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Transaction p2shRefundTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, tradeBotData.getLockTimeA());
|
// Determine receive address for refund
|
||||||
|
String receiveAddress = BTC.getInstance().getUnusedReceiveAddress(tradeBotData.getXprv58());
|
||||||
|
if (receiveAddress == null) {
|
||||||
|
LOGGER.debug(() -> String.format("Couldn't determine a receive address for P2SH-A refund?"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Address receiving = Address.fromString(BTC.getInstance().getNetworkParameters(), receiveAddress);
|
||||||
|
|
||||||
|
Transaction p2shRefundTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, tradeBotData.getLockTimeA(), receiving.getHash());
|
||||||
if (!BTC.getInstance().broadcastTransaction(p2shRefundTransaction)) {
|
if (!BTC.getInstance().broadcastTransaction(p2shRefundTransaction)) {
|
||||||
// We couldn't refund P2SH-A at this time
|
// We couldn't refund P2SH-A at this time
|
||||||
LOGGER.debug(() -> String.format("Couldn't broadcast P2SH-A refund transaction?"));
|
LOGGER.debug(() -> String.format("Couldn't broadcast P2SH-A refund transaction?"));
|
||||||
|
@ -20,6 +20,7 @@ import org.bitcoinj.core.TransactionOutput;
|
|||||||
import org.bitcoinj.core.UTXO;
|
import org.bitcoinj.core.UTXO;
|
||||||
import org.bitcoinj.core.UTXOProvider;
|
import org.bitcoinj.core.UTXOProvider;
|
||||||
import org.bitcoinj.core.UTXOProviderException;
|
import org.bitcoinj.core.UTXOProviderException;
|
||||||
|
import org.bitcoinj.crypto.ChildNumber;
|
||||||
import org.bitcoinj.crypto.DeterministicHierarchy;
|
import org.bitcoinj.crypto.DeterministicHierarchy;
|
||||||
import org.bitcoinj.crypto.DeterministicKey;
|
import org.bitcoinj.crypto.DeterministicKey;
|
||||||
import org.bitcoinj.params.MainNetParams;
|
import org.bitcoinj.params.MainNetParams;
|
||||||
@ -240,6 +241,89 @@ public class BTC {
|
|||||||
return balance.value;
|
return balance.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns first unused receive address given 'm' BIP32 key.
|
||||||
|
*
|
||||||
|
* @param xprv58 BIP32 extended Bitcoin private key
|
||||||
|
* @return Bitcoin P2PKH address, or null if something went wrong
|
||||||
|
*/
|
||||||
|
public String getUnusedReceiveAddress(String xprv58) {
|
||||||
|
Wallet wallet = Wallet.fromSpendingKeyB58(this.params, xprv58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS);
|
||||||
|
DeterministicKeyChain keyChain = wallet.getActiveKeyChain();
|
||||||
|
|
||||||
|
keyChain.setLookaheadSize(WalletAwareUTXOProvider.LOOKAHEAD_INCREMENT);
|
||||||
|
keyChain.maybeLookAhead();
|
||||||
|
|
||||||
|
final int keyChainPathSize = keyChain.getAccountPath().size();
|
||||||
|
List<DeterministicKey> keys = new ArrayList<>(keyChain.getLeafKeys());
|
||||||
|
|
||||||
|
int ki = 0;
|
||||||
|
do {
|
||||||
|
for (; ki < keys.size(); ++ki) {
|
||||||
|
DeterministicKey dKey = keys.get(ki);
|
||||||
|
List<ChildNumber> dKeyPath = dKey.getPath();
|
||||||
|
|
||||||
|
// If keyChain is based on 'm', then make sure dKey is m/0/ki
|
||||||
|
if (dKeyPath.size() != keyChainPathSize + 2 || dKeyPath.get(dKeyPath.size() - 2) != ChildNumber.ZERO)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Check unspent
|
||||||
|
Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH);
|
||||||
|
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
|
||||||
|
|
||||||
|
List<UnspentOutput> unspentOutputs = this.electrumX.getUnspentOutputs(script);
|
||||||
|
if (unspentOutputs == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If there are no unspent outputs then either:
|
||||||
|
* a) all the outputs have been spent
|
||||||
|
* b) address has never been used
|
||||||
|
*
|
||||||
|
* For case (a) we want to remember not to check this address (key) again.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (unspentOutputs.isEmpty()) {
|
||||||
|
// If this is a known key that has been spent before, then we can skip asking for transaction history
|
||||||
|
if (this.spentKeys.contains(dKey)) {
|
||||||
|
wallet.getActiveKeyChain().markKeyAsUsed((DeterministicKey) dKey);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask for transaction history - if it's empty then key has never been used
|
||||||
|
List<byte[]> historicTransactionHashes = this.electrumX.getAddressTransactions(script);
|
||||||
|
if (historicTransactionHashes == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (!historicTransactionHashes.isEmpty()) {
|
||||||
|
// Fully spent key - case (a)
|
||||||
|
this.spentKeys.add(dKey);
|
||||||
|
wallet.getActiveKeyChain().markKeyAsUsed(dKey);
|
||||||
|
} else {
|
||||||
|
// Key never been used - case (b)
|
||||||
|
return address.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key has unspent outputs, hence used, so no good to us
|
||||||
|
this.spentKeys.remove(dKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate some more keys
|
||||||
|
keyChain.setLookaheadSize(keyChain.getLookaheadSize() + WalletAwareUTXOProvider.LOOKAHEAD_INCREMENT);
|
||||||
|
keyChain.maybeLookAhead();
|
||||||
|
|
||||||
|
// This returns all keys, including those already in 'keys'
|
||||||
|
List<DeterministicKey> allLeafKeys = keyChain.getLeafKeys();
|
||||||
|
// Add only new keys onto our list of keys to search
|
||||||
|
List<DeterministicKey> newKeys = allLeafKeys.subList(ki, allLeafKeys.size());
|
||||||
|
keys.addAll(newKeys);
|
||||||
|
// Fall-through to checking more keys as now 'ki' is smaller than 'keys.size()' again
|
||||||
|
|
||||||
|
// Process new keys
|
||||||
|
} while (true);
|
||||||
|
}
|
||||||
|
|
||||||
// UTXOProvider support
|
// UTXOProvider support
|
||||||
|
|
||||||
static class WalletAwareUTXOProvider implements UTXOProvider {
|
static class WalletAwareUTXOProvider implements UTXOProvider {
|
||||||
@ -320,6 +404,7 @@ public class BTC {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If we reach here, then there's definitely at least one unspent key
|
// If we reach here, then there's definitely at least one unspent key
|
||||||
|
btc.spentKeys.remove(key);
|
||||||
areAllKeysSpent = false;
|
areAllKeysSpent = false;
|
||||||
|
|
||||||
for (UnspentOutput unspentOutput : unspentOutputs) {
|
for (UnspentOutput unspentOutput : unspentOutputs) {
|
||||||
|
@ -131,9 +131,10 @@ public class BTCP2SH {
|
|||||||
* @param fundingOutput output from transaction that funded P2SH address
|
* @param fundingOutput output from transaction that funded P2SH address
|
||||||
* @param redeemScriptBytes the redeemScript itself, in byte[] form
|
* @param redeemScriptBytes the redeemScript itself, in byte[] form
|
||||||
* @param lockTime transaction nLockTime - must be at least locktime used in redeemScript
|
* @param lockTime transaction nLockTime - must be at least locktime used in redeemScript
|
||||||
|
* @param receivingAccountInfo Bitcoin PKH used for output
|
||||||
* @return Signed Bitcoin transaction for refunding P2SH
|
* @return Signed Bitcoin transaction for refunding P2SH
|
||||||
*/
|
*/
|
||||||
public static Transaction buildRefundTransaction(Coin refundAmount, ECKey refundKey, List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, long lockTime) {
|
public static Transaction buildRefundTransaction(Coin refundAmount, ECKey refundKey, List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, long lockTime, byte[] receivingAccountInfo) {
|
||||||
Function<byte[], Script> refundSigScriptBuilder = (txSigBytes) -> {
|
Function<byte[], Script> refundSigScriptBuilder = (txSigBytes) -> {
|
||||||
// Build scriptSig with...
|
// Build scriptSig with...
|
||||||
ScriptBuilder scriptBuilder = new ScriptBuilder();
|
ScriptBuilder scriptBuilder = new ScriptBuilder();
|
||||||
@ -152,7 +153,7 @@ public class BTCP2SH {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Send funds back to funding address
|
// Send funds back to funding address
|
||||||
return buildP2shTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime, refundSigScriptBuilder, refundKey.getPubKeyHash());
|
return buildP2shTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime, refundSigScriptBuilder, receivingAccountInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -104,4 +104,17 @@ public class BtcTests extends Common {
|
|||||||
assertEquals(balance, repeatBalance);
|
assertEquals(balance, repeatBalance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetUnusedReceiveAddress() {
|
||||||
|
BTC btc = BTC.getInstance();
|
||||||
|
|
||||||
|
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
|
||||||
|
|
||||||
|
String address = btc.getUnusedReceiveAddress(xprv58);
|
||||||
|
|
||||||
|
assertNotNull(address);
|
||||||
|
|
||||||
|
System.out.println(address);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -186,7 +186,7 @@ public class Refund {
|
|||||||
Coin refundAmount = Coin.valueOf(p2shBalance).subtract(bitcoinFee);
|
Coin refundAmount = Coin.valueOf(p2shBalance).subtract(bitcoinFee);
|
||||||
System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.format(refundAmount), BTC.format(bitcoinFee)));
|
System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.format(refundAmount), BTC.format(bitcoinFee)));
|
||||||
|
|
||||||
Transaction redeemTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime);
|
Transaction redeemTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime, refundKey.getPubKeyHash());
|
||||||
|
|
||||||
byte[] redeemBytes = redeemTransaction.bitcoinSerialize();
|
byte[] redeemBytes = redeemTransaction.bitcoinSerialize();
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user