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