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")
|
||||
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() {
|
||||
}
|
||||
|
||||
|
@ -719,6 +719,13 @@ public class CrossChainResource {
|
||||
if (refundRequest.atAddress == null || !Crypto.isValidAtAddress(refundRequest.atAddress))
|
||||
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
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ATData atData = fetchAtDataWithChecking(repository, refundRequest.atAddress);
|
||||
@ -756,7 +763,7 @@ public class CrossChainResource {
|
||||
|
||||
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);
|
||||
|
||||
if (!wasBroadcast)
|
||||
|
@ -980,7 +980,17 @@ public class TradeBot {
|
||||
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||
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)) {
|
||||
// We couldn't refund P2SH-B at this time
|
||||
LOGGER.debug(() -> String.format("Couldn't broadcast P2SH-B refund transaction?"));
|
||||
@ -1025,7 +1035,16 @@ public class TradeBot {
|
||||
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)) {
|
||||
// We couldn't refund P2SH-A at this time
|
||||
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.UTXOProvider;
|
||||
import org.bitcoinj.core.UTXOProviderException;
|
||||
import org.bitcoinj.crypto.ChildNumber;
|
||||
import org.bitcoinj.crypto.DeterministicHierarchy;
|
||||
import org.bitcoinj.crypto.DeterministicKey;
|
||||
import org.bitcoinj.params.MainNetParams;
|
||||
@ -240,6 +241,89 @@ public class BTC {
|
||||
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
|
||||
|
||||
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
|
||||
btc.spentKeys.remove(key);
|
||||
areAllKeysSpent = false;
|
||||
|
||||
for (UnspentOutput unspentOutput : unspentOutputs) {
|
||||
|
@ -131,9 +131,10 @@ public class BTCP2SH {
|
||||
* @param fundingOutput output from transaction that funded P2SH address
|
||||
* @param redeemScriptBytes the redeemScript itself, in byte[] form
|
||||
* @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
|
||||
*/
|
||||
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) -> {
|
||||
// Build scriptSig with...
|
||||
ScriptBuilder scriptBuilder = new ScriptBuilder();
|
||||
@ -152,7 +153,7 @@ public class BTCP2SH {
|
||||
};
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@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);
|
||||
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();
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user