Browse Source

Add BTC.getWalletBalance(xprv) and add API call to access that.

Also improved BTC.WalletAwareUTXOProvider to derive more keys itself
instead of throwing and relying on caller to do the work.
Added benefit of cleaning up caller code and being more efficient.
Needed because not all receiving/change addresses were being picked up.
split-DB
catbref 4 years ago
parent
commit
cd07240ce7
  1. 36
      src/main/java/org/qortal/api/resource/CrossChainResource.java
  2. 188
      src/main/java/org/qortal/crosschain/BTC.java
  3. 13
      src/test/java/org/qortal/test/btcacct/BtcTests.java

36
src/main/java/org/qortal/api/resource/CrossChainResource.java

@ -911,6 +911,42 @@ public class CrossChainResource {
}
}
@POST
@Path("/btc/walletbalance")
@Operation(
summary = "Returns BTC balance for BIP32 wallet",
description = "Supply BIP32 'm' private key in base58, starting with 'xprv' for mainnet, 'tprv' for testnet",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "BIP32 'm' private key in base58",
example = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TbTVGajEB55L1HYLg2aQMecZLXLre5YJcawpdFG66STVAWPJ"
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY})
public String getBitcoinWalletBalance(String xprv58) {
Security.checkApiCallAllowed(request);
if (!BTC.getInstance().isValidXprv(xprv58))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
Long balance = BTC.getInstance().getWalletBalance(xprv58);
if (balance == null)
return "null";
return balance.toString();
}
@GET
@Path("/tradebot")
@Operation(

188
src/main/java/org/qortal/crosschain/BTC.java

@ -206,10 +206,7 @@ public class BTC {
*/
public Transaction buildSpend(String xprv58, String recipient, long amount) {
Wallet wallet = Wallet.fromSpendingKeyB58(this.params, xprv58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS);
wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet));
DeterministicKeyChain activeKeyChain = wallet.getActiveKeyChain();
activeKeyChain.setLookaheadSize(3);
wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet, WalletAwareUTXOProvider.KeySearchMode.REQUEST_MORE_IF_ALL_SPENT));
Address destination = Address.fromString(this.params, recipient);
SendRequest sendRequest = SendRequest.to(destination, Coin.valueOf(amount));
@ -218,111 +215,144 @@ public class BTC {
// Much smaller fee for TestNet3
sendRequest.feePerKb = Coin.valueOf(2000L);
do {
activeKeyChain.maybeLookAhead();
try {
wallet.completeTx(sendRequest);
return sendRequest.tx;
} catch (InsufficientMoneyException e) {
return null;
}
}
try {
wallet.completeTx(sendRequest);
break;
} catch (InsufficientMoneyException e) {
return null;
} catch (WalletAwareUTXOProvider.AllKeysSpentException e) {
// loop again and use maybeLookAhead() to generate more keys to check
}
} while (true);
/**
* Returns unspent Bitcoin balance given 'm' BIP32 key.
*
* @param xprv58 BIP32 extended Bitcoin private key
* @return unspent BTC balance, or null if unable to determine balance
*/
public Long getWalletBalance(String xprv58) {
Wallet wallet = Wallet.fromSpendingKeyB58(this.params, xprv58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS);
wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet, WalletAwareUTXOProvider.KeySearchMode.REQUEST_MORE_IF_ANY_SPENT));
Coin balance = wallet.getBalance();
if (balance == null)
return null;
return sendRequest.tx;
return balance.value;
}
// UTXOProvider support
static class WalletAwareUTXOProvider implements UTXOProvider {
private final Wallet wallet;
private static final int LOOKAHEAD_INCREMENT = 3;
private final BTC btc;
private final Wallet wallet;
// We extend RuntimeException for unchecked-ness so it will bubble up to caller.
// We can't use UTXOProviderException as it will be wrapped in RuntimeException anyway.
@SuppressWarnings("serial")
public static class AllKeysSpentException extends RuntimeException {
public AllKeysSpentException() {
super();
}
enum KeySearchMode {
REQUEST_MORE_IF_ALL_SPENT, REQUEST_MORE_IF_ANY_SPENT;
}
private final KeySearchMode keySearchMode;
private final DeterministicKeyChain keyChain;
public WalletAwareUTXOProvider(BTC btc, Wallet wallet) {
public WalletAwareUTXOProvider(BTC btc, Wallet wallet, KeySearchMode keySearchMode) {
this.btc = btc;
this.wallet = wallet;
this.keySearchMode = keySearchMode;
this.keyChain = this.wallet.getActiveKeyChain();
// Set up wallet's key chain
this.keyChain.setLookaheadSize(LOOKAHEAD_INCREMENT);
this.keyChain.maybeLookAhead();
}
public List<UTXO> getOpenTransactionOutputs(List<ECKey> keys) throws UTXOProviderException {
List<UTXO> allUnspentOutputs = new ArrayList<>();
final boolean coinbase = false;
boolean areAllKeysSpent = true;
for (ECKey key : keys) {
if (btc.spentKeys.contains(key)) {
wallet.getActiveKeyChain().markKeyAsUsed((DeterministicKey) key);
continue;
}
int ki = 0;
do {
boolean areAllKeysUnspent = true;
boolean areAllKeysSpent = true;
for (; ki < keys.size(); ++ki) {
ECKey key = keys.get(ki);
Address address = Address.fromKey(btc.params, key, ScriptType.P2PKH);
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
List<UnspentOutput> unspentOutputs = btc.electrumX.getUnspentOutputs(script);
if (unspentOutputs == null)
throw new UTXOProviderException(String.format("Unable to fetch unspent outputs for %s", address));
/*
* 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 all passed keys are spent then we need to signal caller that they might want to
* generate more keys to check.
*/
if (unspentOutputs.isEmpty()) {
// Ask for transaction history - if it's empty then key has never been used
List<byte[]> historicTransactionHashes = btc.electrumX.getAddressTransactions(script);
if (historicTransactionHashes == null)
throw new UTXOProviderException(
String.format("Unable to fetch transaction history for %s", address));
if (!historicTransactionHashes.isEmpty()) {
// Fully spent key - case (a)
btc.spentKeys.add(key);
if (btc.spentKeys.contains(key)) {
wallet.getActiveKeyChain().markKeyAsUsed((DeterministicKey) key);
} else {
// Key never been used - case (b)
areAllKeysSpent = false;
areAllKeysUnspent = false;
continue;
}
continue;
}
Address address = Address.fromKey(btc.params, key, ScriptType.P2PKH);
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
List<UnspentOutput> unspentOutputs = btc.electrumX.getUnspentOutputs(script);
if (unspentOutputs == null)
throw new UTXOProviderException(String.format("Unable to fetch unspent outputs for %s", address));
/*
* 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()) {
// Ask for transaction history - if it's empty then key has never been used
List<byte[]> historicTransactionHashes = btc.electrumX.getAddressTransactions(script);
if (historicTransactionHashes == null)
throw new UTXOProviderException(
String.format("Unable to fetch transaction history for %s", address));
if (!historicTransactionHashes.isEmpty()) {
// Fully spent key - case (a)
btc.spentKeys.add(key);
wallet.getActiveKeyChain().markKeyAsUsed((DeterministicKey) key);
areAllKeysUnspent = false;
} else {
// Key never been used - case (b)
areAllKeysSpent = false;
}
continue;
}
// If we reach here, then there's definitely at least one unspent key
areAllKeysSpent = false;
// If we reach here, then there's definitely at least one unspent key
areAllKeysSpent = false;
for (UnspentOutput unspentOutput : unspentOutputs) {
List<TransactionOutput> transactionOutputs = btc.getOutputs(unspentOutput.hash);
if (transactionOutputs == null)
throw new UTXOProviderException(String.format("Unable to fetch outputs for TX %s",
HashCode.fromBytes(unspentOutput.hash)));
for (UnspentOutput unspentOutput : unspentOutputs) {
List<TransactionOutput> transactionOutputs = btc.getOutputs(unspentOutput.hash);
if (transactionOutputs == null)
throw new UTXOProviderException(String.format("Unable to fetch outputs for TX %s",
HashCode.fromBytes(unspentOutput.hash)));
TransactionOutput transactionOutput = transactionOutputs.get(unspentOutput.index);
TransactionOutput transactionOutput = transactionOutputs.get(unspentOutput.index);
UTXO utxo = new UTXO(Sha256Hash.wrap(unspentOutput.hash), unspentOutput.index,
Coin.valueOf(unspentOutput.value), unspentOutput.height, coinbase,
transactionOutput.getScriptPubKey());
UTXO utxo = new UTXO(Sha256Hash.wrap(unspentOutput.hash), unspentOutput.index,
Coin.valueOf(unspentOutput.value), unspentOutput.height, coinbase,
transactionOutput.getScriptPubKey());
allUnspentOutputs.add(utxo);
}
}
allUnspentOutputs.add(utxo);
if ((this.keySearchMode == KeySearchMode.REQUEST_MORE_IF_ALL_SPENT && areAllKeysSpent)
|| (this.keySearchMode == KeySearchMode.REQUEST_MORE_IF_ANY_SPENT && !areAllKeysUnspent)) {
// Generate some more keys
this.keyChain.setLookaheadSize(this.keyChain.getLookaheadSize() + LOOKAHEAD_INCREMENT);
this.keyChain.maybeLookAhead();
// This returns all keys, including those already in 'keys'
List<DeterministicKey> allLeafKeys = this.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
}
}
if (areAllKeysSpent)
// Notify caller that they need to check more keys
throw new AllKeysSpentException();
// If we have processed all keys, then we're done
} while (ki < keys.size());
return allUnspentOutputs;
}

13
src/test/java/org/qortal/test/btcacct/BtcTests.java

@ -73,4 +73,17 @@ public class BtcTests extends Common {
btc.buildSpend(xprv58, recipient, amount);
}
@Test
public void testGetWalletBalance() {
BTC btc = BTC.getInstance();
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
Long balance = btc.getWalletBalance(xprv58);
assertNotNull(balance);
System.out.println(BTC.format(balance));
}
}

Loading…
Cancel
Save