Skip to content

Commit

Permalink
213 provide hd wallet implementation (#363)
Browse files Browse the repository at this point in the history
* #213 rough sketch of needed functionality as a discussion basis

* still work in progress

* basic transaction working now. Still need to refactor a lot! But first good step.

* #213 added WalletUtxoSupplier and removed getUtxo functionalities from Wallet.java
Need to rework signing to get addresses.

* #213 added stakekey registrations and address caching to wallet

* #213 added Tests, WalletUtxoSupplier interface and DefaultWalletUtxoSupplier.java

* #213 implemented signing with wallet. A UTXOSupplier will be passed.
Thus removing the unneeded utxosupplier value in Wallet.

* Refactor Tx sender wallet handling and clean up AbstractTx.

Changed the sender wallet handling to return null instead of throwing an exception in Tx.java to allow flexibility in error management. Also, removed redundant 'amounts' field and its initialization from AbstractTx.java for a cleaner codebase.

* Refactor TxBuilderContext and QuickTxBuilder

Move the transaction building logic into a dedicated private method in `TxBuilderContext`. Refactor `QuickTxBuilder` to use the new build method and streamline the signing process, eliminating redundant code and improving code readability.

* Refactor TxSigner to include TxBuilderContext

* Normalize mnemonic phrase whitespaces

* Update logging dependency to reload4j in build.gradle

* Refactor: Change MnemonicUtil import path

* Refactor Wallet signing and UTXO handling

* Refactor Wallet class and update related tests

Refactor Wallet methods and variables for better naming consistency. Remove unused methods and annotations. Add new test for scanning specific indexes in DefaultWalletUtxoSupplier. Update existing tests to align with the refactored method names and structures.

* fix: changes to fix SonarQube errors

---------

Co-authored-by: Satya <satran004@gmail.com>
  • Loading branch information
Kammerlo and satran004 authored Nov 25, 2024
1 parent 6131dce commit 75d9a39
Show file tree
Hide file tree
Showing 26 changed files with 1,648 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@

import com.bloxbean.cardano.client.address.Address;
import com.bloxbean.cardano.client.address.AddressProvider;
import com.bloxbean.cardano.client.crypto.MnemonicUtil;
import com.bloxbean.cardano.client.address.Credential;
import com.bloxbean.cardano.client.common.model.Network;
import com.bloxbean.cardano.client.common.model.Networks;
import com.bloxbean.cardano.client.crypto.bip32.HdKeyPair;
import com.bloxbean.cardano.client.crypto.bip39.MnemonicCode;
import com.bloxbean.cardano.client.crypto.bip39.MnemonicException;
import com.bloxbean.cardano.client.crypto.bip39.Words;
import com.bloxbean.cardano.client.crypto.cip1852.CIP1852;
import com.bloxbean.cardano.client.crypto.cip1852.DerivationPath;
import com.bloxbean.cardano.client.exception.AddressRuntimeException;
import com.bloxbean.cardano.client.exception.CborDeserializationException;
import com.bloxbean.cardano.client.exception.CborSerializationException;
import com.bloxbean.cardano.client.governance.keys.CommitteeColdKey;
Expand All @@ -22,9 +20,6 @@
import com.bloxbean.cardano.client.util.HexUtil;
import com.fasterxml.jackson.annotation.JsonIgnore;

import java.util.Arrays;
import java.util.stream.Collectors;

/**
* Create and manage secrets, and perform account-based work such as signing transactions.
*/
Expand Down Expand Up @@ -87,7 +82,8 @@ public Account(Network network, int index) {
public Account(Network network, DerivationPath derivationPath, Words noOfWords) {
this.network = network;
this.derivationPath = derivationPath;
generateNew(noOfWords);
this.mnemonic = MnemonicUtil.generateNew(noOfWords);
baseAddress();
}

/**
Expand Down Expand Up @@ -141,7 +137,7 @@ public Account(Network network, String mnemonic, DerivationPath derivationPath)
this.mnemonic = mnemonic;
this.accountKey = null;
this.derivationPath = derivationPath;
validateMnemonic();
MnemonicUtil.validateMnemonic(this.mnemonic);
baseAddress();
}

Expand Down Expand Up @@ -485,32 +481,6 @@ public Transaction signWithCommitteeHotKey(Transaction transaction) {
return TransactionSigner.INSTANCE.sign(transaction, getCommitteeHotKeyPair());
}

private void generateNew(Words noOfWords) {
String mnemonic = null;
try {
mnemonic = MnemonicCode.INSTANCE.createMnemonic(noOfWords).stream().collect(Collectors.joining(" "));
} catch (MnemonicException.MnemonicLengthException e) {
throw new RuntimeException("Mnemonic generation failed", e);
}
this.mnemonic = mnemonic;
baseAddress();
}

private void validateMnemonic() {
if (mnemonic == null) {
throw new AddressRuntimeException("Mnemonic cannot be null");
}

mnemonic = mnemonic.replaceAll("\\s+", " ");
String[] words = mnemonic.split("\\s+");

try {
MnemonicCode.INSTANCE.check(Arrays.asList(words));
} catch (MnemonicException e) {
throw new AddressRuntimeException("Invalid mnemonic phrase", e);
}
}

private HdKeyPair getHdKeyPair() {
HdKeyPair hdKeyPair;
if (mnemonic == null || mnemonic.trim().length() == 0) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.bloxbean.cardano.client.crypto;

import com.bloxbean.cardano.client.crypto.bip39.MnemonicCode;
import com.bloxbean.cardano.client.crypto.bip39.MnemonicException;
import com.bloxbean.cardano.client.crypto.bip39.Words;
import com.bloxbean.cardano.client.exception.AddressRuntimeException;

import java.util.Arrays;
import java.util.stream.Collectors;

public class MnemonicUtil {

private MnemonicUtil() {

}

public static void validateMnemonic(String mnemonic) {
if (mnemonic == null) {
throw new AddressRuntimeException("Mnemonic cannot be null");
}

mnemonic = mnemonic.replaceAll("\\s+", " ");
String[] words = mnemonic.split("\\s+");

try {
MnemonicCode.INSTANCE.check(Arrays.asList(words));
} catch (MnemonicException e) {
throw new AddressRuntimeException("Invalid mnemonic phrase", e);
}
}

public static String generateNew(Words noOfWords) {
String mnemonic = null;
try {
mnemonic = MnemonicCode.INSTANCE.createMnemonic(noOfWords).stream().collect(Collectors.joining(" "));
} catch (MnemonicException.MnemonicLengthException e) {
throw new AddressRuntimeException("Mnemonic generation failed", e);
}
return mnemonic;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,9 @@ public static byte[] toSeed(List<String> words, String passphrase) {

public byte[] toEntropy(String mnemonicPhrase) throws MnemonicException.MnemonicLengthException, MnemonicException.MnemonicWordException, MnemonicException.MnemonicChecksumException {
String[] wordsList;
wordsList = mnemonicPhrase.split(" ");

mnemonicPhrase = mnemonicPhrase.replaceAll("\\s+", " ");
wordsList = mnemonicPhrase.split("\\s+");

return toEntropy(Arrays.asList(wordsList));
}
Expand Down
1 change: 1 addition & 0 deletions function/build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
dependencies {
api project(':core')
api project(':hd-wallet')
}

publishing {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,9 +267,7 @@ public static TxBuilderContext init(UtxoSupplier utxoSupplier, ProtocolParamsSup
* @throws com.bloxbean.cardano.client.function.exception.TxBuildException if exception during transaction build
*/
public Transaction build(TxBuilder txBuilder) {
Transaction transaction = new Transaction();
transaction.setEra(getSerializationEra());
txBuilder.apply(this, transaction);
Transaction transaction = buildTransaction(txBuilder);
clearTempStates();
return transaction;
}
Expand All @@ -282,8 +280,10 @@ public Transaction build(TxBuilder txBuilder) {
* @throws com.bloxbean.cardano.client.function.exception.TxBuildException if exception during transaction build
*/
public Transaction buildAndSign(TxBuilder txBuilder, TxSigner signer) {
Transaction transaction = build(txBuilder);
return signer.sign(transaction);
Transaction transaction = buildTransaction(txBuilder);
Transaction signedTransaction = signer.sign(this, transaction);
clearTempStates();
return signedTransaction;
}

/**
Expand All @@ -297,6 +297,13 @@ public void build(Transaction transaction, TxBuilder txBuilder) {
clearTempStates();
}

private Transaction buildTransaction(TxBuilder txBuilder) {
Transaction transaction = new Transaction();
transaction.setEra(getSerializationEra());
txBuilder.apply(this, transaction);
return transaction;
}

private void clearTempStates() {
clearMintMultiAssets();
clearUtxos();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ public interface TxSigner {
/**
* Apply this function to sign a transaction
*
* @param transaction
* @param context {@link TxBuilderContext}
* @param transaction {@link Transaction} to sign
* @return a signed transaction
*/
Transaction sign(Transaction transaction);
Transaction sign(TxBuilderContext context, Transaction transaction);

/**
* Returns a composed function that first applies this function to
Expand All @@ -29,6 +30,6 @@ public interface TxSigner {
*/
default TxSigner andThen(TxSigner after) {
Objects.requireNonNull(after);
return (transaction) -> after.sign(sign(transaction));
return (context, transaction) -> after.sign(context, sign(context, transaction));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
import com.bloxbean.cardano.client.transaction.TransactionSigner;
import com.bloxbean.cardano.client.transaction.spec.Policy;
import com.bloxbean.cardano.client.transaction.spec.Transaction;
import com.bloxbean.cardano.hdwallet.Wallet;
import com.bloxbean.cardano.hdwallet.model.WalletUtxo;

import java.util.stream.Collectors;

/**
* Provides helper methods to get TxSigner function to sign a <code>{@link Transaction}</code> object
Expand All @@ -20,7 +24,7 @@ public class SignerProviders {
*/
public static TxSigner signerFrom(Account... signers) {

return transaction -> {
return (context, transaction) -> {
Transaction outputTxn = transaction;
for (Account signer : signers) {
outputTxn = signer.sign(outputTxn);
Expand All @@ -30,14 +34,30 @@ public static TxSigner signerFrom(Account... signers) {
};
}

/**
* Function to sign a transaction with a wallet
*
* @param wallet wallet to sign the transaction
* @return <code>TxSigner</code> function which returns a <code>Transaction</code> object with witnesses when invoked
*/
public static TxSigner signerFrom(Wallet wallet) {
return (context, transaction) -> {
var utxos = context.getUtxos()
.stream().filter(utxo -> utxo instanceof WalletUtxo)
.map(utxo -> (WalletUtxo) utxo)
.collect(Collectors.toSet());
return wallet.sign(transaction, utxos);
};
}

/**
* Function to sign a transaction with one or more <code>SecretKey</code>
* @param secretKeys secret keys to sign the transaction
* @return <code>TxSigner</code> function which returns a <code>Transaction</code> object with witnesses when invoked
*/
public static TxSigner signerFrom(SecretKey... secretKeys) {

return transaction -> {
return (context, transaction) -> {
Transaction outputTxn = transaction;
for (SecretKey sk : secretKeys) {
outputTxn = TransactionSigner.INSTANCE.sign(outputTxn, sk);
Expand All @@ -54,7 +74,7 @@ public static TxSigner signerFrom(SecretKey... secretKeys) {
*/
public static TxSigner signerFrom(Policy... policies) {

return transaction -> {
return (context, transaction) -> {
Transaction outputTxn = transaction;
for (Policy policy : policies) {
for (SecretKey sk : policy.getPolicyKeys()) {
Expand All @@ -73,7 +93,7 @@ public static TxSigner signerFrom(Policy... policies) {
*/
public static TxSigner signerFrom(HdKeyPair... hdKeyPairs) {

return transaction -> {
return (context, transaction) -> {
Transaction outputTxn = transaction;
for (HdKeyPair hdKeyPair : hdKeyPairs) {
outputTxn = TransactionSigner.INSTANCE.sign(outputTxn, hdKeyPair);
Expand All @@ -90,7 +110,7 @@ public static TxSigner signerFrom(HdKeyPair... hdKeyPairs) {
*/
public static TxSigner stakeKeySignerFrom(Account... signers) {

return transaction -> {
return (context, transaction) -> {
Transaction outputTxn = transaction;
for (Account signer : signers) {
outputTxn = signer.signWithStakeKey(outputTxn);
Expand All @@ -106,7 +126,7 @@ public static TxSigner stakeKeySignerFrom(Account... signers) {
* @return <code>TxSigner</code> function which returns a <code>Transaction</code> object with witnesses when invoked
*/
public static TxSigner drepKeySignerFrom(Account... signers) {
return transaction -> {
return (context, transaction) -> {
Transaction outputTxn = transaction;
for (Account signer : signers) {
outputTxn = signer.signWithDRepKey(outputTxn);
Expand All @@ -116,14 +136,23 @@ public static TxSigner drepKeySignerFrom(Account... signers) {
};
}

public static TxSigner stakeKeySignerFrom(Wallet... wallets) {
return (context, transaction) -> {
Transaction outputTxn = transaction;
for (Wallet wallet : wallets)
outputTxn = wallet.signWithStakeKey(outputTxn);
return outputTxn;
};
}

//TODO -- Add Integration test
/**
* Function to sign a transaction with one or more Committee Cold key(s) of <code>Account</code>(s)
* @param signers - account(s) to sign the transaction
* @return <code>TxSigner</code> function which returns a <code>Transaction</code> object with witnesses when invoked
*/
public static TxSigner committeeColdKeySignerFrom(Account... signers) {
return transaction -> {
return (context, transaction) -> {
Transaction outputTxn = transaction;
for (Account signer : signers) {
outputTxn = signer.signWithCommitteeColdKey(outputTxn);
Expand All @@ -140,7 +169,7 @@ public static TxSigner committeeColdKeySignerFrom(Account... signers) {
* @return <code>TxSigner</code> function which returns a <code>Transaction</code> object with witnesses when invoked
*/
public static TxSigner committeeHotKeySignerFrom(Account... signers) {
return transaction -> {
return (context, transaction) -> {
Transaction outputTxn = transaction;
for (Account signer : signers) {
outputTxn = signer.signWithCommitteeHotKey(outputTxn);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ void signerFromAccounts() throws Exception {
Account account2 = new Account(Networks.testnet());

Transaction signedTxn = SignerProviders.signerFrom(account1, account2)
.sign(buildTransaction());
.sign(null, buildTransaction());

assertThat(signedTxn.getWitnessSet().getVkeyWitnesses()).hasSize(2);
}
Expand All @@ -37,7 +37,7 @@ void signerFromSecretKey() throws Exception {
SecretKey sk3 = KeyGenUtil.generateKey().getSkey();

Transaction signedTxn = SignerProviders.signerFrom(sk1, sk2, sk3)
.sign(buildTransaction());
.sign(null, buildTransaction());

assertThat(signedTxn.getWitnessSet().getVkeyWitnesses()).hasSize(3);
}
Expand All @@ -48,7 +48,7 @@ void signerFromPolicies() throws Exception {
Policy policy2 = PolicyUtil.createMultiSigScriptAllPolicy("2", 4);

Transaction signedTxn = SignerProviders.signerFrom(policy1, policy2)
.sign(buildTransaction());
.sign(null, buildTransaction());

assertThat(signedTxn.getWitnessSet().getVkeyWitnesses()).hasSize(7);
}
Expand All @@ -60,7 +60,7 @@ void signerFromHdKeyPairs() throws Exception {

Transaction signedTxn = SignerProviders.signerFrom(account1.stakeHdKeyPair(), account2.stakeHdKeyPair())
.andThen(SignerProviders.signerFrom(account1))
.sign(buildTransaction());
.sign(null, buildTransaction());

assertThat(signedTxn.getWitnessSet().getVkeyWitnesses()).hasSize(3);
}
Expand All @@ -72,7 +72,7 @@ void signerFromAccountStakeKeys() throws Exception {

Transaction signedTxn = SignerProviders.stakeKeySignerFrom(account1, account2)
.andThen(SignerProviders.signerFrom(account1))
.sign(buildTransaction());
.sign(null, buildTransaction());

assertThat(signedTxn.getWitnessSet().getVkeyWitnesses()).hasSize(3);
}
Expand Down
29 changes: 29 additions & 0 deletions hd-wallet/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
dependencies {
api project(':core-api')
api project(':core')
api project(':common')
api project(':crypto')
api project(':backend')
implementation(libs.bouncycastle.bcprov)

integrationTestImplementation(libs.slf4j.reload4j)
integrationTestImplementation(libs.aiken.java.binding)
integrationTestImplementation project(':')
integrationTestImplementation project(':backend-modules:blockfrost')
integrationTestImplementation project(':backend-modules:koios')
integrationTestImplementation project(':backend-modules:ogmios')
integrationTestImplementation project(':backend-modules:ogmios')

integrationTestAnnotationProcessor project(':annotation-processor')
}

publishing {
publications {
mavenJava(MavenPublication) {
pom {
name = 'Cardano Client HD Wallet'
description = 'Cardano Client Lib - HD Wallet Integration'
}
}
}
}
Loading

0 comments on commit 75d9a39

Please sign in to comment.