Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#482 Generate Account/Wallet from root prv key #486

Merged
merged 4 commits into from
Jan 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* Address class represents Shelley address
*/
public class Address {
public static final String ADDR_VKH_PREFIX = "addr_vkh";
private String prefix;
private byte[] bytes;
private String address;
Expand Down Expand Up @@ -173,4 +174,18 @@ public boolean isStakeKeyHashInDelegationPart() {
public boolean isScriptHashInDelegationPart() {
return AddressProvider.isScriptHashInDelegationPart(this);
}

/**
* Retrieves the Bech32-encoded address verification key hash of the payment credential
* associated with the address, if available.
*
*
* @return An {@link Optional} containing the Bech32-encoded verification key hash
* if the payment credential hash is available, or an empty {@link Optional}
* if the payment credential hash is absent.
*/
public Optional<String> getBech32VerificationKeyHash() {
return getPaymentCredentialHash()
.map(paymentCred -> Bech32.encode(paymentCred, ADDR_VKH_PREFIX));
}
}
254 changes: 197 additions & 57 deletions core/src/main/java/com/bloxbean/cardano/client/account/Account.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.bloxbean.cardano.client.account;

public class AccountException extends RuntimeException {
public AccountException() {
}

public AccountException(String msg) {
super(msg);
}

public AccountException(Throwable cause) {
super(cause);
}

public AccountException(String msg, Throwable cause) {
super(msg, cause);
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public HdKeyPair getAccountKeyPairFromSecretKey(byte[] xprv, DerivationPath deri
return getKeyPairFromSecretKey(xprv, accountPath);
}

private HdKeyPair getKeyPairFromSecretKey(byte[] xprv, String path) {
public HdKeyPair getKeyPairFromSecretKey(byte[] xprv, String path) {
byte[] IL = Arrays.copyOfRange(xprv, 0, 64);
byte[] IR = Arrays.copyOfRange(xprv, 64, 96);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,25 @@ public HdKeyPair getKeyPairFromMnemonic(String mnemonicPhrase, DerivationPath de
}
}

/**
* Generates the root HdKeyPair from the given mnemonic phrase.
*
* @param mnemonicPhrase the mnemonic phrase used to generate the HdKeyPair
* @return the root HdKeyPair derived from the provided mnemonic phrase
* @throws CryptoException if the mnemonic phrase cannot be converted to entropy or if
* the key pair generation fails
*/
public HdKeyPair getRootKeyPairFromMnemonic(String mnemonicPhrase) {
var hdKeyGenerator = new HdKeyGenerator();
try {
byte[] entropy = MnemonicCode.INSTANCE.toEntropy(mnemonicPhrase);

return hdKeyGenerator.getRootKeyPairFromEntropy(entropy);
} catch (Exception ex) {
throw new CryptoException("Mnemonic to KeyPair generation failed", ex);
}
}

/**
* Get HdKeyPair from entropy
* @param entropy entropy
Expand All @@ -42,8 +61,7 @@ public HdKeyPair getKeyPairFromEntropy(byte[] entropy, DerivationPath derivation
HdKeyPair accountKey = hdKeyGenerator.getChildKeyPair(coinTypeKey, derivationPath.getAccount().getValue(), derivationPath.getAccount().isHarden());
HdKeyPair roleKey = hdKeyGenerator.getChildKeyPair(accountKey, derivationPath.getRole().getValue(), derivationPath.getRole().isHarden());

HdKeyPair indexKey = hdKeyGenerator.getChildKeyPair(roleKey, derivationPath.getIndex().getValue(), derivationPath.getIndex().isHarden());
return indexKey;
return hdKeyGenerator.getChildKeyPair(roleKey, derivationPath.getIndex().getValue(), derivationPath.getIndex().isHarden());
}

/**
Expand All @@ -58,8 +76,7 @@ public HdKeyPair getKeyPairFromAccountKey(byte[] accountKey, DerivationPath deri
HdKeyPair accountKeyPair = hdKeyGenerator.getAccountKeyPairFromSecretKey(accountKey, derivationPath);
HdKeyPair roleKey = hdKeyGenerator.getChildKeyPair(accountKeyPair, derivationPath.getRole().getValue(), derivationPath.getRole().isHarden());

HdKeyPair indexKey = hdKeyGenerator.getChildKeyPair(roleKey, derivationPath.getIndex().getValue(), derivationPath.getIndex().isHarden());
return indexKey;
return hdKeyGenerator.getChildKeyPair(roleKey, derivationPath.getIndex().getValue(), derivationPath.getIndex().isHarden());
}

/**
Expand Down Expand Up @@ -106,4 +123,34 @@ public static HdPublicKey getPublicKeyFromAccountPubKey(HdPublicKey accountHdPub
return hdKeyGenerator.getChildPublicKey(roleHdPubKey, index);
}

/**
* Generates an HdKeyPair derived from the given root key and a specified derivation path.
*
* @param rootKey the root key represented as a byte array
* @param derivationPath the hierarchical deterministic (HD) derivation path used to derive the key pair
* @return the HdKeyPair derived from the provided root key and derivation path
*/
public HdKeyPair getKeyPairFromRootKey(byte[] rootKey, DerivationPath derivationPath) {
HdKeyGenerator hdKeyGenerator = new HdKeyGenerator();
HdKeyPair rootKeyPair = hdKeyGenerator.getKeyPairFromSecretKey(rootKey, HdKeyGenerator.MASTER_PATH);

HdKeyPair purposeKey = hdKeyGenerator.getChildKeyPair(rootKeyPair, derivationPath.getPurpose().getValue(), derivationPath.getPurpose().isHarden());
HdKeyPair coinTypeKey = hdKeyGenerator.getChildKeyPair(purposeKey, derivationPath.getCoinType().getValue(), derivationPath.getCoinType().isHarden());
HdKeyPair accountKey = hdKeyGenerator.getChildKeyPair(coinTypeKey, derivationPath.getAccount().getValue(), derivationPath.getAccount().isHarden());
HdKeyPair roleKey = hdKeyGenerator.getChildKeyPair(accountKey, derivationPath.getRole().getValue(), derivationPath.getRole().isHarden());

return hdKeyGenerator.getChildKeyPair(roleKey, derivationPath.getIndex().getValue(), derivationPath.getIndex().isHarden());
}

/**
* Get an HdKeyPair from the given root key using the master derivation path.
*
* @param rootKey the root key represented as a byte array
* @return the HdKeyPair derived from the provided root key and the master derivation path
*/
public HdKeyPair getRootKeyPairFromRootKey(byte[] rootKey) {
HdKeyGenerator hdKeyGenerator = new HdKeyGenerator();
return hdKeyGenerator.getKeyPairFromSecretKey(rootKey, HdKeyGenerator.MASTER_PATH);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,41 @@ void getPublicKeyFromAccountPubKey_xpub_2() {

assertThat(HexUtil.encodeHexString(hdPublicKey.getBytes())).isEqualTo(expectedStakePubKey);
}

@Test
void getRootKeyPairFromMnemonic() {
String mnemonic = "top exact spice seed cloud birth orient bracket happy cat section girl such outside elder";

var rootKeyPair = new CIP1852().getRootKeyPairFromMnemonic(mnemonic);
String expectedRootKeyBech32 = "root_xsk1zza6z52v8gelnaqdhuny3ywlccud5dtm8rvvyem4utnfwzcaa9pspsmdm99qfpy2qz7sw9sts59mrkegmdqyjen5ykm4z3ccyrkn8g5mm0qw35arvwxclfh6tj3s4x7t2q85wenvppjpxckcxgnf8vd80ug0l6rw";
String expectedRootPvtKeyHex = HexUtil.encodeHexString(Bech32.decode(expectedRootKeyBech32).data);

var rootPvtKey = rootKeyPair.getPrivateKey().getBytes();

assertThat(HexUtil.encodeHexString(rootPvtKey)).isEqualTo(expectedRootPvtKeyHex);
}

@Test
void getKeyPairFromRootKeyAtDerivationPath() {
String rootKey = "root_xsk1zza6z52v8gelnaqdhuny3ywlccud5dtm8rvvyem4utnfwzcaa9pspsmdm99qfpy2qz7sw9sts59mrkegmdqyjen5ykm4z3ccyrkn8g5mm0qw35arvwxclfh6tj3s4x7t2q85wenvppjpxckcxgnf8vd80ug0l6rw";
byte[] rootKeyBytes = Bech32.decode(rootKey).data;

var addr0KeyPair = new CIP1852().getKeyPairFromRootKey(rootKeyBytes, DerivationPath.createExternalAddressDerivationPath(0));

//1852H/1815H/0H/0/0
String expectedAddr0PvtKey = "addr_xsk1artlf4j6xz246j4xqtn6d595l7sy4vk0zuvzawlg7lwvq2qaa9pkujarca9j5ju08m0dlgw2qagauw693lvmrghujzvxsdfj99pwdm7xqwpgj5asad6nl5rzact6hune2xsl5x5gv2tds75ksdptavxr6se0fk8e";
String expectedAddr0PvtKeyHex = HexUtil.encodeHexString(Bech32.decode(expectedAddr0PvtKey).data);

assertThat(HexUtil.encodeHexString(addr0KeyPair.getPrivateKey().getBytes())).isEqualTo(expectedAddr0PvtKeyHex);
}

@Test
void getRootKeyPairFromRootKey() {
String rootKeyBech32 = "root_xsk1zza6z52v8gelnaqdhuny3ywlccud5dtm8rvvyem4utnfwzcaa9pspsmdm99qfpy2qz7sw9sts59mrkegmdqyjen5ykm4z3ccyrkn8g5mm0qw35arvwxclfh6tj3s4x7t2q85wenvppjpxckcxgnf8vd80ug0l6rw";
byte[] expectedRootKeyBytes = Bech32.decode(rootKeyBech32).data;

var rootKeyPair = new CIP1852().getRootKeyPairFromRootKey(expectedRootKeyBytes);

assertThat(HexUtil.encodeHexString(rootKeyPair.getPrivateKey().getBytes())).isEqualTo(HexUtil.encodeHexString(expectedRootKeyBytes));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.bloxbean.cardano.client.backend.api.BackendService;
import com.bloxbean.cardano.client.cip.cip20.MessageMetadata;
import com.bloxbean.cardano.client.common.model.Networks;
import com.bloxbean.cardano.client.crypto.Bech32;
import com.bloxbean.cardano.client.exception.CborSerializationException;
import com.bloxbean.cardano.client.function.helper.SignerProviders;
import com.bloxbean.cardano.client.metadata.Metadata;
Expand Down Expand Up @@ -39,6 +40,7 @@ public class QuickTxBuilderIT extends QuickTxBaseIT {
DefaultWalletUtxoSupplier walletUtxoSupplier;
Wallet wallet1;
Wallet wallet2;
Wallet wallet3;

static Account topupAccount;

Expand All @@ -58,9 +60,12 @@ void setup() {
utxoSupplier = getUTXOSupplier();

String wallet1Mnemonic = "clog book honey force cricket stamp until seed minimum margin denial kind volume undo simple federal then jealous solid legal crucial crazy acoustic thank";
wallet1 = new Wallet(Networks.testnet(), wallet1Mnemonic);
wallet1 = Wallet.createFromMnemonic(Networks.testnet(), wallet1Mnemonic);
String wallet2Mnemonic = "theme orphan remind output arrive lobster decorate ten gap piece casual distance attend total blast dilemma damp punch pride file limit soldier plug canoe";
wallet2 = new Wallet(Networks.testnet(), wallet2Mnemonic);
wallet2 = Wallet.createFromMnemonic(Networks.testnet(), wallet2Mnemonic);

String acctSk = "acct_xsk1azc6gn5zkdprp4gkapmhdckykphjl62rm9224699ut5z6xcaa9p4hv5hmjfgcrzk72tnsqh6dw0njekdjpsv8nv5h5hk6lpd4ag62zenwhzqs205kfurd7kgs8fm5gx4l4j8htutwj060kyp5y5kgw55qc8lsltd";
wallet3 = Wallet.createFromAccountKey(Networks.testnet(), Bech32.decode(acctSk).data);

walletUtxoSupplier = new DefaultWalletUtxoSupplier(backendService.getUtxoService(), wallet1);
}
Expand All @@ -71,15 +76,15 @@ void simplePayment() {
metadata.put(BigInteger.valueOf(100), "This is first metadata");
metadata.putNegative(200, -900);

wallet1.setSearchUtxoByAddrVkh(true);
//topup wallet
splitPaymentBetweenAddress(topupAccount, wallet1, 20, Double.valueOf(50000));

splitPaymentBetweenAddress(topupAccount, wallet1, 20, Double.valueOf(3000), true);

UtxoSupplier walletUtxoSupplier = new DefaultWalletUtxoSupplier(backendService.getUtxoService(), wallet1);
QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService, walletUtxoSupplier);

Tx tx = new Tx()
.payToAddress(wallet2.getBaseAddress(0).getAddress(), Amount.ada(50000))
.payToAddress(wallet2.getBaseAddress(0).getAddress(), Amount.ada(2000))
.from(wallet1);

Result<String> result = quickTxBuilder.compose(tx)
Expand All @@ -99,7 +104,7 @@ void simplePayment() {
@Test
void simplePayment_withIndexesToScan() {
String mnemonic = "buzz sentence empty coffee manage grid claw street misery deputy direct seek tortoise wedding stay twist crew august omit taste expect obscure abandon iron";
Wallet wallet = new Wallet(Networks.testnet(), mnemonic);
Wallet wallet = Wallet.createFromMnemonic(Networks.testnet(), mnemonic);
wallet.setIndexesToScan(new int[]{5, 30, 45});

//topup index 5, 45
Expand Down Expand Up @@ -169,7 +174,7 @@ void utxoTest() {
assertTrue(!utxos.isEmpty());
}

void splitPaymentBetweenAddress(Account topupAccount, Wallet receiverWallet, int totalAddresses, Double adaAmount) {
void splitPaymentBetweenAddress(Account topupAccount, Wallet receiverWallet, int totalAddresses, Double adaAmount, boolean enableEntAddrPayment) {
// Create an amount array with no of totalAddresses with random distribution of split amounts
Double[] amounts = new Double[totalAddresses];
Double remainingAmount = adaAmount;
Expand All @@ -187,7 +192,15 @@ void splitPaymentBetweenAddress(Account topupAccount, Wallet receiverWallet, int
int currentIndex = 0;

for (int i = 0; i < totalAddresses; i++) {
addresses[i] = receiverWallet.getBaseAddressString(currentIndex);
if (enableEntAddrPayment) {
if (i % 2 == 0)
addresses[i] = receiverWallet.getBaseAddressString(currentIndex);
else
addresses[i] = receiverWallet.getEntAddress(currentIndex).toBech32();
} else {
addresses[i] = receiverWallet.getBaseAddressString(currentIndex);
}

currentIndex += random.nextInt(20) + 1;
}

Expand All @@ -205,4 +218,34 @@ void splitPaymentBetweenAddress(Account topupAccount, Wallet receiverWallet, int

System.out.println(result);
}

@Test
void simplePayment_fromAccountKey() {
Metadata metadata = MetadataBuilder.createMetadata();
metadata.put(BigInteger.valueOf(100), "This is first metadata");
metadata.putNegative(200, -900);

//topup wallet
splitPaymentBetweenAddress(topupAccount, wallet3, 20, Double.valueOf(3000), false);

UtxoSupplier walletUtxoSupplier = new DefaultWalletUtxoSupplier(backendService.getUtxoService(), wallet3);
QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService, walletUtxoSupplier);

Tx tx = new Tx()
.payToAddress(wallet2.getBaseAddress(0).getAddress(), Amount.ada(2000))
.from(wallet3);

Result<String> result = quickTxBuilder.compose(tx)
.withSigner(SignerProviders.signerFrom(wallet3))
.withTxInspector(txn -> {
System.out.println(JsonUtil.getPrettyJson(txn));
})
.complete();

System.out.println(result);
assertTrue(result.isSuccessful());
waitForTransaction(result);

checkIfUtxoAvailable(result.getValue(), wallet2.getBaseAddress(0).getAddress());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ static void beforeAll() {
quickTxBuilder = new QuickTxBuilder(backendService);

String wallet1Mnemonic = "clog book honey force cricket stamp until seed minimum margin denial kind volume undo simple federal then jealous solid legal crucial crazy acoustic thank";
wallet1 = new Wallet(Networks.testnet(), wallet1Mnemonic);
wallet1 = Wallet.createFromMnemonic(Networks.testnet(), wallet1Mnemonic);
String wallet2Mnemonic = "theme orphan remind output arrive lobster decorate ten gap piece casual distance attend total blast dilemma damp punch pride file limit soldier plug canoe";
wallet2 = new Wallet(Networks.testnet(), wallet2Mnemonic);
wallet2 = Wallet.createFromMnemonic(Networks.testnet(), wallet2Mnemonic);

if (backendType.equals(DEVKIT)) {
poolId = "pool1wvqhvyrgwch4jq9aa84hc8q4kzvyq2z3xr6mpafkqmx9wce39zy";
Expand Down
44 changes: 44 additions & 0 deletions hd-wallet/src/it/resources/log4j.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" debug="true">
<!-- Console Appender -->
<appender name="console" class="org.apache.log4j.ConsoleAppender">
<param name="Target" value="System.out" />
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%-5p | %d{yyyy-MM-dd HH:mm:ss} | [%t] %C{2} (%F:%L) - %m%n" />
</layout>
</appender>
<!-- Info Log File Appender -->
<appender name="info-log" class="org.apache.log4j.FileAppender">
<param name="File" value="info.log" />
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%-5p | %d{yyyy-MM-dd HH:mm:ss} | [%t] %C{2} (%F:%L) - %m%n" />
</layout>
<filter class="org.apache.log4j.varia.LevelRangeFilter">
<param name="LevelMin" value="debug" />
<param name="LevelMax" value="info" />
<param name="AcceptOnMatch" value="true" />
</filter>
</appender>
<!-- Error Log File Appender -->
<appender name="error-log" class="org.apache.log4j.FileAppender">
<param name="Append" value="false" />
<param name="File" value="yaci_error.log" />
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%-5p | %d{yyyy-MM-dd HH:mm:ss} | [%t] %C{2} (%F:%L) - %m%n" />
</layout>
<filter class="org.apache.log4j.varia.LevelRangeFilter">
<param name="LevelMin" value="warn" />
<param name="LevelMax" value="fatal" />
<param name="AcceptOnMatch" value="true" />
</filter>
</appender>
<!-- <logger name="com.bloxbean.cardano">-->
<!-- <appender-ref ref="info-log" />-->
<!-- <appender-ref ref="error-log" />-->
<!-- </logger>-->
<root>
<level value="debug" />
<appender-ref ref="console" />
</root>
</log4j:configuration>
Loading
Loading