Merge pull request #159 from kennycud/master

Added Core API endpoint to repair LTC wallets
This commit is contained in:
AlphaX-Projects 2023-12-30 19:31:04 +01:00 committed by GitHub
commit 903fa0346a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 191 additions and 4 deletions

View File

@ -265,4 +265,43 @@ public class CrossChainLitecoinResource {
return CrossChainUtils.buildServerConfigurationInfo(Litecoin.getInstance());
}
}
@POST
@Path("/repair")
@Operation(
summary = "Sends all coins in wallet to primary receive address",
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "BIP32 'm' private/public key in base58",
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", description = "transaction hash"))
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
@SecurityRequirement(name = "apiKey")
public String repairOldWallet(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
Security.checkApiCallAllowed(request);
Litecoin litecoin = Litecoin.getInstance();
if (!litecoin.isValidDeterministicKey(key58))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
try {
return litecoin.repairOldWallet(key58);
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
}

View File

@ -505,7 +505,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
List<String> candidates = this.getSpendingCandidateAddresses(key58);
for(DeterministicKey key : getWalletKeys(key58)) {
for(DeterministicKey key : getOldWalletKeys(key58)) {
infos.add(buildAddressInfo(key, candidates));
}
@ -592,11 +592,23 @@ public abstract class Bitcoiny implements ForeignBlockchain {
}
}
private List<DeterministicKey> getWalletKeys(String key58) throws ForeignBlockchainException {
/**
* Get Old Wallet Keys
*
* Get wallet keys using the old key generation algorithm. This is used for diagnosing and repairing wallets
* created before 2024.
*
* @param masterPrivateKey
*
* @return the keys
*
* @throws ForeignBlockchainException
*/
private List<DeterministicKey> getOldWalletKeys(String masterPrivateKey) throws ForeignBlockchainException {
synchronized (this) {
Context.propagate(bitcoinjContext);
Wallet wallet = walletFromDeterministicKey58(key58);
Wallet wallet = walletFromDeterministicKey58(masterPrivateKey);
DeterministicKeyChain keyChain = wallet.getActiveKeyChain();
keyChain.setLookaheadSize(Bitcoiny.WALLET_KEY_LOOKAHEAD_INCREMENT);
@ -998,4 +1010,52 @@ public abstract class Bitcoiny implements ForeignBlockchain {
return Wallet.fromWatchingKeyB58(this.params, key58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS);
}
/**
* Repair Wallet
*
* Repair wallets generated before 2024 by moving all the address balances to the first address.
*
* @param privateMasterKey
*
* @return the transaction Id of the spend operation that moves the balances or the exception name if an exception
* is thrown
*
* @throws ForeignBlockchainException
*/
public String repairOldWallet(String privateMasterKey) throws ForeignBlockchainException {
// create a deterministic wallet to satisfy the bitcoinj API
Wallet wallet = Wallet.createDeterministic(this.bitcoinjContext, ScriptType.P2PKH);
// use the blockchain resources of this instance for UTXO provision
wallet.setUTXOProvider(new BitcoinyUTXOProvider( this ));
// import in each that is generated using the old key generation algorithm
List<DeterministicKey> walletKeys = getOldWalletKeys(privateMasterKey);
for( DeterministicKey key : walletKeys) {
wallet.importKey(ECKey.fromPrivate(key.getPrivKey()));
}
// get the primary receive address
Address firstAddress = Address.fromKey(this.params, walletKeys.get(0), ScriptType.P2PKH);
// send all the imported coins to the primary receive address
SendRequest sendRequest = SendRequest.emptyWallet(firstAddress);
sendRequest.feePerKb = this.getFeePerKb();
try {
// allow the wallet to build the send request transaction and broadcast
wallet.completeTx(sendRequest);
broadcastTransaction(sendRequest.tx);
// return the transaction Id
return sendRequest.tx.getTxId().toString();
}
catch( Exception e ) {
// log error and return exception name
LOGGER.error(e.getMessage(), e);
return e.getClass().getSimpleName();
}
}
}

View File

@ -0,0 +1,80 @@
package org.qortal.crosschain;
import org.bitcoinj.core.*;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import java.util.ArrayList;
import java.util.List;
/**
* Class BitcoinyUTXOProvider
*
* Uses Bitcoiny resources for UTXO provision.
*/
public class BitcoinyUTXOProvider implements UTXOProvider {
private Bitcoiny bitcoiny;
public BitcoinyUTXOProvider(Bitcoiny bitcoiny) {
this.bitcoiny = bitcoiny;
}
@Override
public List<UTXO> getOpenTransactionOutputs(List<ECKey> keys) throws UTXOProviderException {
try {
List<UTXO> utxos = new ArrayList<>();
for( ECKey key : keys) {
Address address = Address.fromKey(this.bitcoiny.params, key, Script.ScriptType.P2PKH);
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
// collection UTXO's for all confirmed unspent outputs
for (UnspentOutput output : this.bitcoiny.blockchainProvider.getUnspentOutputs(script, false)) {
utxos.add(toUTXO(output));
}
}
return utxos;
} catch (ForeignBlockchainException e) {
throw new UTXOProviderException(e);
}
}
/**
* Convert Unspent Output to a UTXO
*
* @param unspentOutput
*
* @return the UTXO
*
* @throws ForeignBlockchainException
*/
private UTXO toUTXO(UnspentOutput unspentOutput) throws ForeignBlockchainException {
List<TransactionOutput> transactionOutputs = this.bitcoiny.getOutputs(unspentOutput.hash);
TransactionOutput transactionOutput = transactionOutputs.get(unspentOutput.index);
return new UTXO(
Sha256Hash.wrap(unspentOutput.hash),
unspentOutput.index,
Coin.valueOf(unspentOutput.value),
unspentOutput.height,
false,
transactionOutput.getScriptPubKey()
);
}
@Override
public int getChainHeadHeight() throws UTXOProviderException {
try {
return this.bitcoiny.blockchainProvider.getCurrentHeight();
} catch (ForeignBlockchainException e) {
throw new UTXOProviderException(e);
}
}
@Override
public NetworkParameters getParams() {
return this.bitcoiny.params;
}
}

View File

@ -99,6 +99,14 @@ public abstract class BitcoinyTests extends Common {
transaction = bitcoiny.buildSpend(xprv58, recipient, amount);
assertNotNull(transaction);
}
@Test
public void testRepair() throws ForeignBlockchainException {
String xprv58 = getDeterministicKey58();
String transaction = bitcoiny.repairOldWallet(xprv58);
assertNotNull(transaction);
}
@Test
public void testGetWalletBalance() throws ForeignBlockchainException {