Skip to content

Commit

Permalink
Added Address info support for Litecoin
Browse files Browse the repository at this point in the history
  • Loading branch information
kennycud committed Nov 4, 2023
1 parent 5febfaf commit d4ef175
Show file tree
Hide file tree
Showing 7 changed files with 335 additions and 0 deletions.
17 changes: 17 additions & 0 deletions src/main/java/org/qortal/api/model/crosschain/AddressRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.qortal.api.model.crosschain;

import io.swagger.v3.oas.annotations.media.Schema;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;

@XmlAccessorType(XmlAccessType.FIELD)
public class AddressRequest {

@Schema(description = "Litecoin BIP32 extended public key", example = "tpub___________________________________________________________________________________________________________")
public String xpub58;

public AddressRequest() {
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
import org.qortal.api.ApiErrors;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.Security;
import org.qortal.api.model.crosschain.AddressRequest;
import org.qortal.api.model.crosschain.LitecoinSendRequest;
import org.qortal.crosschain.AddressInfo;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.Litecoin;
import org.qortal.crosschain.SimpleTransaction;
Expand Down Expand Up @@ -150,6 +152,44 @@ public List<SimpleTransaction> getLitecoinWalletTransactions(@HeaderParam(Securi
}
}

@POST
@Path("/addressinfos")
@Operation(
summary = "Returns information for each address for a hierarchical, deterministic BIP32 wallet",
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.APPLICATION_JSON,
schema = @Schema(
implementation = AddressRequest.class
)
)
),
responses = {
@ApiResponse(
content = @Content(array = @ArraySchema( schema = @Schema( implementation = AddressInfo.class ) ) )
)
}

)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
@SecurityRequirement(name = "apiKey")
public List<AddressInfo> getLitecoinAddressInfos(@HeaderParam(Security.API_KEY_HEADER) String apiKey, AddressRequest addressRequest) {
Security.checkApiCallAllowed(request);

Litecoin litecoin = Litecoin.getInstance();

if (!litecoin.isValidDeterministicKey(addressRequest.xpub58))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);

try {
return litecoin.getWalletAddressInfos(addressRequest.xpub58);
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}

@POST
@Path("/unusedaddress")
@Operation(
Expand Down
78 changes: 78 additions & 0 deletions src/main/java/org/qortal/crosschain/AddressInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package org.qortal.crosschain;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import java.util.List;
import java.util.Objects;

/**
* Class AddressInfo
*/
@XmlAccessorType(XmlAccessType.FIELD)
public class AddressInfo {

private String address;

private List<Integer> path;

private long value;

private String pathAsString;

private int transactionCount;

public AddressInfo() {
}

public AddressInfo(String address, List<Integer> path, long value, String pathAsString, int transactionCount) {
this.address = address;
this.path = path;
this.value = value;
this.pathAsString = pathAsString;
this.transactionCount = transactionCount;
}

public String getAddress() {
return address;
}

public List<Integer> getPath() {
return path;
}

public long getValue() {
return value;
}

public String getPathAsString() {
return pathAsString;
}

public int getTransactionCount() {
return transactionCount;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AddressInfo that = (AddressInfo) o;
return value == that.value && transactionCount == that.transactionCount && Objects.equals(address, that.address) && Objects.equals(path, that.path) && Objects.equals(pathAsString, that.pathAsString);
}

@Override
public int hashCode() {
return Objects.hash(address, path, value, pathAsString, transactionCount);
}

@Override
public String toString() {
return "AddressInfo{" +
"address='" + address + '\'' +
", path=" + path +
", value=" + value +
", pathAsString='" + pathAsString + '\'' +
", transactionCount=" + transactionCount +
'}';
}
}
94 changes: 94 additions & 0 deletions src/main/java/org/qortal/crosschain/Bitcoiny.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.util.*;
import java.util.stream.Collectors;

import com.google.common.collect.ImmutableList;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bitcoinj.core.Address;
Expand Down Expand Up @@ -488,6 +489,37 @@ public List<SimpleTransaction> getWalletTransactions(String key58) throws Foreig
}
}

public List<AddressInfo> getWalletAddressInfos(String key58) throws ForeignBlockchainException {
List<AddressInfo> infos = new ArrayList<>();

for(DeterministicKey key : getWalletKeys(key58)) {
infos.add(buildAddressInfo(key));
}

return infos.stream()
.sorted(new PathComparator(1))
.collect(Collectors.toList());
}

public AddressInfo buildAddressInfo(DeterministicKey key) throws ForeignBlockchainException {

Address address = Address.fromKey(this.params, key, ScriptType.P2PKH);

int transactionCount = getAddressTransactions(ScriptBuilder.createOutputScript(address).getProgram(), true).size();

return new AddressInfo(
address.toString(),
toIntegerList( key.getPath()),
summingUnspentOutputs(address.toString()),
key.getPathAsString(),
transactionCount);
}

private static List<Integer> toIntegerList(ImmutableList<ChildNumber> path) {

return path.stream().map(ChildNumber::num).collect(Collectors.toList());
}

public Set<String> getWalletAddresses(String key58) throws ForeignBlockchainException {
synchronized (this) {
Context.propagate(bitcoinjContext);
Expand Down Expand Up @@ -546,6 +578,61 @@ public Set<String> getWalletAddresses(String key58) throws ForeignBlockchainExce
}
}

private List<DeterministicKey> getWalletKeys(String key58) throws ForeignBlockchainException {
synchronized (this) {
Context.propagate(bitcoinjContext);

Wallet wallet = walletFromDeterministicKey58(key58);
DeterministicKeyChain keyChain = wallet.getActiveKeyChain();

keyChain.setLookaheadSize(Bitcoiny.WALLET_KEY_LOOKAHEAD_INCREMENT);
keyChain.maybeLookAhead();

List<DeterministicKey> keys = new ArrayList<>(keyChain.getLeafKeys());

int unusedCounter = 0;
int ki = 0;
do {
boolean areAllKeysUnused = true;

for (; ki < keys.size(); ++ki) {
DeterministicKey dKey = keys.get(ki);

// Check for transactions
Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH);
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();

// Ask for transaction history - if it's empty then key has never been used
List<TransactionHash> historicTransactionHashes = this.getAddressTransactions(script, false);

if (!historicTransactionHashes.isEmpty()) {
areAllKeysUnused = false;
}
}

if (areAllKeysUnused) {
// No transactions
if (unusedCounter >= Settings.getInstance().getGapLimit()) {
// ... and we've hit our search limit
break;
}
// We haven't hit our search limit yet so increment the counter and keep looking
unusedCounter += WALLET_KEY_LOOKAHEAD_INCREMENT;
} else {
// Some keys in this batch were used, so reset the counter
unusedCounter = 0;
}

// Generate some more keys
keys.addAll(generateMoreKeys(keyChain));

// Process new keys
} while (true);

return keys;
}
}

protected SimpleTransaction convertToSimpleTransaction(BitcoinyTransaction t, Set<String> keySet) {
long amount = 0;
long total = 0L;
Expand Down Expand Up @@ -818,6 +905,13 @@ public NetworkParameters getParams() {
}
}

private Long summingUnspentOutputs(String walletAddress) throws ForeignBlockchainException {
return this.getUnspentOutputs(walletAddress).stream()
.map(TransactionOutput::getValue)
.mapToLong(Coin::longValue)
.sum();
}

// Utility methods for others

public static List<SimpleForeignTransaction> simplifyWalletTransactions(List<BitcoinyTransaction> transactions) {
Expand Down
29 changes: 29 additions & 0 deletions src/main/java/org/qortal/crosschain/PathComparator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.qortal.crosschain;

/**
* Class PathComparator
*/
public class PathComparator implements java.util.Comparator<AddressInfo> {

private int max;

public PathComparator(int max) {
this.max = max;
}

@Override
public int compare(AddressInfo info1, AddressInfo info2) {
return compareAtLevel(info1, info2, 0);
}

private int compareAtLevel(AddressInfo info1, AddressInfo info2, int level) {

if( level < 0 ) return 0;

int compareTo = info1.getPath().get(level).compareTo(info2.getPath().get(level));

if(compareTo != 0 || level == max) return compareTo;

return compareAtLevel(info1, info2,level + 1);
}
}
35 changes: 35 additions & 0 deletions src/test/java/org/qortal/test/crosschain/BitcoinyTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.qortal.crosschain.AddressInfo;
import org.qortal.crosschain.Bitcoiny;
import org.qortal.crosschain.BitcoinyHTLC;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;

import java.util.Arrays;
import java.util.List;
import java.util.Set;

import static org.junit.Assert.*;

Expand Down Expand Up @@ -127,4 +130,36 @@ public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
System.out.println(address);
}

@Test
public void testGenerateRootKeyForTesting() {

String rootKey = BitcoinyTestsUtils.generateBip32RootKey( this.bitcoiny.getNetworkParameters() );

System.out.println(String.format(getCoinName() + " generated BIP32 Root Key: " + rootKey));

}

@Test
public void testGetWalletAddresses() throws ForeignBlockchainException {

String xprv58 = getDeterministicKey58();

Set<String> addresses = this.bitcoiny.getWalletAddresses(xprv58);

System.out.println( "root key = " + xprv58 );
System.out.println( "keys ...");
addresses.stream().forEach(System.out::println);
}

@Test
public void testWalletAddressInfos() throws ForeignBlockchainException {

String xprv58 = getDeterministicKey58();

List<AddressInfo> addressInfos = this.bitcoiny.getWalletAddressInfos(xprv58);

System.out.println("address count = " + addressInfos.size() );
System.out.println( "address infos ..." );
addressInfos.forEach( System.out::println );
}
}
Loading

0 comments on commit d4ef175

Please sign in to comment.