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

213 provide hd wallet implementation #363

Merged
merged 22 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
20a116d
#213 rough sketch of needed functionality as a discussion basis
Kammerlo Jan 16, 2024
52182b1
still work in progress
Kammerlo Jan 16, 2024
20e49d1
Merge branch 'master' into 213-provide-hd-wallet-implementation
Kammerlo Jan 16, 2024
1f7b4d5
merged master
Kammerlo Jan 17, 2024
bb48752
basic transaction working now. Still need to refactor a lot! But firs…
Kammerlo Jan 17, 2024
b14741a
#213 added WalletUtxoSupplier and removed getUtxo functionalities fro…
Kammerlo Jan 18, 2024
dcd184f
#213 added stakekey registrations and address caching to wallet
Kammerlo Jan 19, 2024
596488a
#213 added Tests, WalletUtxoSupplier interface and DefaultWalletUtxoS…
Kammerlo Jan 19, 2024
64df79e
#213 implemented signing with wallet. A UTXOSupplier will be passed.
Kammerlo Jan 22, 2024
9c0ec6e
Merge branch 'master' into 213-provide-hd-wallet-implementation
Kammerlo Jan 22, 2024
242f30c
Merge branch 'master' into 213-provide-hd-wallet-implementation
satran004 Jan 31, 2024
4e98c77
Merge with master
satran004 Nov 22, 2024
41c2615
Merge with master
satran004 Nov 22, 2024
588d3ae
Refactor Tx sender wallet handling and clean up AbstractTx.
satran004 Nov 24, 2024
3894bee
Refactor TxBuilderContext and QuickTxBuilder
satran004 Nov 24, 2024
bc3ee91
Refactor TxSigner to include TxBuilderContext
satran004 Nov 24, 2024
d444737
Normalize mnemonic phrase whitespaces
satran004 Nov 24, 2024
fdf1c4a
Update logging dependency to reload4j in build.gradle
satran004 Nov 24, 2024
a65b76d
Refactor: Change MnemonicUtil import path
satran004 Nov 24, 2024
187351f
Refactor Wallet signing and UTXO handling
satran004 Nov 24, 2024
5316e90
Refactor Wallet class and update related tests
satran004 Nov 25, 2024
0f10f3f
fix: changes to fix SonarQube errors
satran004 Nov 25, 2024
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 @@ -2,6 +2,7 @@

import com.bloxbean.cardano.client.address.Address;
import com.bloxbean.cardano.client.address.AddressProvider;
import com.bloxbean.cardano.client.common.MnemonicUtil;
import com.bloxbean.cardano.client.address.Credential;
import com.bloxbean.cardano.client.common.model.Network;
import com.bloxbean.cardano.client.common.model.Networks;
Expand Down Expand Up @@ -85,7 +86,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 @@ -139,7 +141,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
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.bloxbean.cardano.client.common;

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 {

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 RuntimeException("Mnemonic generation failed", e);
}
return mnemonic;
}
}
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 @@ -7,6 +7,8 @@
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.supplier.WalletUtxoSupplier;

/**
* Provides helper methods to get TxSigner function to sign a <code>{@link Transaction}</code> object
Expand All @@ -30,6 +32,19 @@ public static TxSigner signerFrom(Account... signers) {
};
}

/**
* Function to sign a transaction using one or more <code>Wallet</code>
* @param wallet wallet(s) to sign the transaction
* @param walletUtxoSupplier <code>WalletUtxoSupplier</code> is needed to sign with the right addresses
* @return <code>TxSigner</code> function which returns a <code>Transaction</code> object with witnesses when invoked
*/
public static TxSigner signerFrom(Wallet wallet, WalletUtxoSupplier walletUtxoSupplier) {
return transaction -> {
Transaction outputTxn = wallet.sign(transaction, walletUtxoSupplier);
return outputTxn;
};
}

/**
* Function to sign a transaction with one or more <code>SecretKey</code>
* @param secretKeys secret keys to sign the transaction
Expand Down Expand Up @@ -110,4 +125,13 @@ public static TxSigner drepKeySignerFrom(Account... signers) {
return outputTxn;
};
}

public static TxSigner stakeKeySignerFrom(Wallet... wallets) {
return transaction -> {
Transaction outputTxn = transaction;
for (Wallet wallet : wallets)
outputTxn = wallet.signWithStakeKey(outputTxn);
return outputTxn;
};
}
}
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.log4j)
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'
}
}
}
}
25 changes: 25 additions & 0 deletions hd-wallet/specification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# HD Wallet integration specification

## Motivation

Hierarchical deterministic wallets are a common practice in blockchains like Bitcoin and Cardano.
The idea behind that is to derive multiple keys (private and public) and addresses from a master key.
The advantage in contrast to individual addresses is that these keys/addresses are linked together through the master key.
Thus it is possible to maintain privacy due to changing public addresses frequently.
Otherwise one could track users through various transactions.

Therefore it must be possible for users to use this concept via an easy-to-use API within this library.

## Implementation

- HDWallet Class
- Wrapper for Account - Deriving new Accounts with one Mnemonic
- Scanning strategy -> 20 consecutive empty addresses
- First Interface Approach [HDWalletInterface.java](src/main/java/com/bloxbean/cardano/hdwallet/HDWalletInterface.java)
- Transaction Building - extend QuickTxBuilder to use HDWallet
- Getting UTXOs to pay certain amount
- Getting Signers for the respective UTXOs spend in the transaction
- Build and Sign Transactions from HDWallet
- Minting Assets to a specific address
- Coin Selection Strategies
- Support common UTXO SelectionStrategys (Biggest, sequential, ...)
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.bloxbean.cardano.hdwallet;

import com.bloxbean.cardano.client.api.UtxoSupplier;
import com.bloxbean.cardano.client.api.model.Result;
import com.bloxbean.cardano.client.api.model.Utxo;
import com.bloxbean.cardano.client.backend.api.BackendService;
import com.bloxbean.cardano.client.backend.api.DefaultUtxoSupplier;
import com.bloxbean.cardano.client.backend.blockfrost.common.Constants;
import com.bloxbean.cardano.client.backend.blockfrost.service.BFBackendService;
import com.bloxbean.cardano.client.backend.koios.KoiosBackendService;
import com.bloxbean.cardano.client.backend.model.TransactionContent;
import com.bloxbean.cardano.client.util.JsonUtil;

import java.util.List;
import java.util.Optional;

public class QuickTxBaseIT {

protected String BLOCKFROST = "blockfrost";
protected String KOIOS = "koios";
protected String DEVKIT = "devkit";
protected String backendType = DEVKIT;

public BackendService getBackendService() {
if (BLOCKFROST.equals(backendType)) {
String bfProjectId = System.getProperty("BF_PROJECT_ID");
if (bfProjectId == null || bfProjectId.isEmpty()) {
bfProjectId = System.getenv("BF_PROJECT_ID");
}

return new BFBackendService(Constants.BLOCKFROST_PREPROD_URL, bfProjectId);
} else if (KOIOS.equals(backendType)) {
return new KoiosBackendService(com.bloxbean.cardano.client.backend.koios.Constants.KOIOS_PREPROD_URL);
} else if (DEVKIT.equals(backendType)) {
return new BFBackendService("http://localhost:8080/api/v1/", "Dummy");
} else
return null;
}

public UtxoSupplier getUTXOSupplier() {
return new DefaultUtxoSupplier(getBackendService().getUtxoService());
}

public void waitForTransaction(Result<String> result) {
try {
if (result.isSuccessful()) { //Wait for transaction to be mined
int count = 0;
while (count < 60) {
Result<TransactionContent> txnResult = getBackendService().getTransactionService().getTransaction(result.getValue());
if (txnResult.isSuccessful()) {
System.out.println(JsonUtil.getPrettyJson(txnResult.getValue()));
break;
} else {
System.out.println("Waiting for transaction to be mined ....");
}

count++;
Thread.sleep(2000);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}

protected void checkIfUtxoAvailable(String txHash, String address) {
Optional<Utxo> utxo = Optional.empty();
int count = 0;
while (utxo.isEmpty()) {
if (count++ >= 20)
break;
List<Utxo> utxos = new DefaultUtxoSupplier(getBackendService().getUtxoService()).getAll(address);
utxo = utxos.stream().filter(u -> u.getTxHash().equals(txHash))
.findFirst();
System.out.println("Try to get new output... txhash: " + txHash);
try {
Thread.sleep(1000);
} catch (Exception e) {}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package com.bloxbean.cardano.hdwallet;

import com.bloxbean.cardano.client.api.UtxoSupplier;
import com.bloxbean.cardano.client.api.model.Amount;
import com.bloxbean.cardano.client.api.model.Result;
import com.bloxbean.cardano.client.api.model.Utxo;
import com.bloxbean.cardano.client.api.util.PolicyUtil;
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.exception.CborSerializationException;
import com.bloxbean.cardano.client.function.helper.SignerProviders;
import com.bloxbean.cardano.client.metadata.Metadata;
import com.bloxbean.cardano.client.metadata.MetadataBuilder;
import com.bloxbean.cardano.client.quicktx.QuickTxBuilder;
import com.bloxbean.cardano.client.quicktx.Tx;
import com.bloxbean.cardano.client.transaction.spec.Asset;
import com.bloxbean.cardano.client.transaction.spec.Policy;
import com.bloxbean.cardano.hdwallet.supplier.DefaultWalletUtxoSupplier;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.math.BigInteger;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertTrue;

public class QuickTxBuilderIT extends QuickTxBaseIT {

BackendService backendService;
UtxoSupplier utxoSupplier;
DefaultWalletUtxoSupplier walletUtxoSupplier;
Wallet wallet1;
Wallet wallet2;

@BeforeEach
void setup() {
backendService = getBackendService();
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);
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);
}

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


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

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

Result<String> result = quickTxBuilder.compose(tx)
.withSigner(wallet1)
.complete();

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

checkIfUtxoAvailable(result.getValue(), wallet2.getBaseAddress(0).getAddress());
}

@Test
void minting() throws CborSerializationException {
Policy policy = PolicyUtil.createMultiSigScriptAtLeastPolicy("test_policy", 1, 1);
String assetName = "MyAsset";
BigInteger qty = BigInteger.valueOf(1000);

Tx tx = new Tx()
.mintAssets(policy.getPolicyScript(), new Asset(assetName, qty), wallet1.getBaseAddress(0).getAddress())
.attachMetadata(MessageMetadata.create().add("Minting tx"))
.from(wallet1);

UtxoSupplier walletUtxoSupplier = new DefaultWalletUtxoSupplier(backendService.getUtxoService(), wallet1);
QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService, walletUtxoSupplier);
Result<String> result = quickTxBuilder.compose(tx)
.withSigner(wallet1)
.withSigner(SignerProviders.signerFrom(policy))
.complete();

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

checkIfUtxoAvailable(result.getValue(), wallet1.getBaseAddress(0).getAddress());
}

@Test
void utxoTest() {
List<Utxo> utxos = walletUtxoSupplier.getAll(wallet1);
Map<String, Integer> amountMap = new HashMap<>();
for (Utxo utxo : utxos) {
int totalAmount = 0;
if(amountMap.containsKey(utxo.getAddress())) {
int amount = amountMap.get(utxo.getAddress());
System.out.println(utxo.getAmount().get(0));
totalAmount= amount + utxo.getAmount().get(0).getQuantity().intValue();
}
amountMap.put(utxo.getAddress(), totalAmount);
}

assertTrue(!utxos.isEmpty());
}
}
Loading
Loading