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.
This commit is contained in:
catbref 2020-08-04 16:37:44 +01:00
parent 91518464c2
commit cd07240ce7
3 changed files with 159 additions and 80 deletions

View File

@ -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(

View File

@ -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,50 +215,71 @@ public class BTC {
// Much smaller fee for TestNet3
sendRequest.feePerKb = Coin.valueOf(2000L);
do {
activeKeyChain.maybeLookAhead();
try {
wallet.completeTx(sendRequest);
break;
return sendRequest.tx;
} catch (InsufficientMoneyException e) {
return null;
} catch (WalletAwareUTXOProvider.AllKeysSpentException e) {
// loop again and use maybeLookAhead() to generate more keys to check
}
} while (true);
}
return sendRequest.tx;
/**
* 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 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;
int ki = 0;
do {
boolean areAllKeysUnspent = true;
boolean areAllKeysSpent = true;
for (ECKey key : keys) {
for (; ki < keys.size(); ++ki) {
ECKey key = keys.get(ki);
if (btc.spentKeys.contains(key)) {
wallet.getActiveKeyChain().markKeyAsUsed((DeterministicKey) key);
areAllKeysUnspent = false;
continue;
}
@ -278,8 +296,6 @@ public class BTC {
* 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()) {
@ -293,6 +309,7 @@ public class BTC {
// 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;
@ -320,9 +337,22 @@ public class BTC {
}
}
if (areAllKeysSpent)
// Notify caller that they need to check more keys
throw new AllKeysSpentException();
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 we have processed all keys, then we're done
} while (ki < keys.size());
return allUnspentOutputs;
}

View File

@ -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));
}
}