From 20a116dbfbc44b03272252617b4a08673d5b55eb Mon Sep 17 00:00:00 2001 From: Kammerlo Date: Tue, 16 Jan 2024 08:20:57 +0100 Subject: [PATCH 01/16] #213 rough sketch of needed functionality as a discussion basis --- hd-wallet/specification.md | 25 +++++++++ .../cardano/hdwallet/HDWalletInterface.java | 56 +++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 hd-wallet/specification.md create mode 100644 hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/HDWalletInterface.java diff --git a/hd-wallet/specification.md b/hd-wallet/specification.md new file mode 100644 index 00000000..04f27a0c --- /dev/null +++ b/hd-wallet/specification.md @@ -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, ...) \ No newline at end of file diff --git a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/HDWalletInterface.java b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/HDWalletInterface.java new file mode 100644 index 00000000..4a863606 --- /dev/null +++ b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/HDWalletInterface.java @@ -0,0 +1,56 @@ +package com.bloxbean.cardano.hdwallet; + +import com.bloxbean.cardano.client.account.Account; +import com.bloxbean.cardano.client.api.model.Amount; + +import java.util.List; + +public interface HDWalletInterface { + + private String mnemonic = ""; + +// public HDWalletInterface(Network network, String mnemonic); +// public HDWalletInterface(Network network); + + // sum up all amounts within this wallet + // scanning strategy is as described in specification.md + public List getWalletBalance(); + + /** + * Returns the account for the given index + * @param index + * @return + */ + public Account getAccount(int index); + + /** + * Creates a new Account for the first empty index. + * @return + */ + public Account newAccount(); + + /** + * Returns the stake address + * @return + */ + public String getStakeAddress(); + + /** + * returns the master private key + * @return + */ + public byte[] getMasterPrivateKey(); + + /** + * Returns the master public key + * @return + */ + public String getMasterPublicKey(); + + /** + * returns the master adress from where other addresses can be derived + * @return + */ + public String getMasterAddress(); // prio 2 + +} From 52182b105bb7a515f589f47c1cfb4d2eef67f516 Mon Sep 17 00:00:00 2001 From: Kammerlo Date: Tue, 16 Jan 2024 16:20:16 +0100 Subject: [PATCH 02/16] still work in progress --- .../cardano/client/account/Account.java | 32 +--- .../cardano/client/common/MnemonicUtil.java | 37 +++++ hd-wallet/build.gradle | 20 +++ .../bloxbean/cardano/hdwallet/HDWallet.java | 154 ++++++++++++++++++ .../cardano/hdwallet/HDWalletTest.java | 45 +++++ settings.gradle | 1 + 6 files changed, 261 insertions(+), 28 deletions(-) create mode 100644 core/src/main/java/com/bloxbean/cardano/client/common/MnemonicUtil.java create mode 100644 hd-wallet/build.gradle create mode 100644 hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/HDWallet.java create mode 100644 hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/HDWalletTest.java diff --git a/core/src/main/java/com/bloxbean/cardano/client/account/Account.java b/core/src/main/java/com/bloxbean/cardano/client/account/Account.java index 2d9824e8..923bce2d 100644 --- a/core/src/main/java/com/bloxbean/cardano/client/account/Account.java +++ b/core/src/main/java/com/bloxbean/cardano/client/account/Account.java @@ -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.common.model.Network; import com.bloxbean.cardano.client.common.model.Networks; import com.bloxbean.cardano.client.crypto.bip32.HdKeyPair; @@ -82,7 +83,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(); } /** @@ -136,7 +138,7 @@ public Account(Network network, String mnemonic, DerivationPath derivationPath) this.mnemonic = mnemonic; this.accountKey = null; this.derivationPath = derivationPath; - validateMnemonic(); + MnemonicUtil.validateMnemonic(this.mnemonic); baseAddress(); } @@ -371,32 +373,6 @@ public Transaction signWithStakeKey(Transaction transaction) { return TransactionSigner.INSTANCE.sign(transaction, getStakeKeyPair()); } - 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) { diff --git a/core/src/main/java/com/bloxbean/cardano/client/common/MnemonicUtil.java b/core/src/main/java/com/bloxbean/cardano/client/common/MnemonicUtil.java new file mode 100644 index 00000000..7e250517 --- /dev/null +++ b/core/src/main/java/com/bloxbean/cardano/client/common/MnemonicUtil.java @@ -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; + } +} diff --git a/hd-wallet/build.gradle b/hd-wallet/build.gradle new file mode 100644 index 00000000..50db06bf --- /dev/null +++ b/hd-wallet/build.gradle @@ -0,0 +1,20 @@ +dependencies { + api project(':core-api') + api project(':core') + api project(':common') + api project(':crypto') + api project(':backend') + api project(':backend-modules:blockfrost') + implementation(libs.bouncycastle.bcprov) +} + +publishing { + publications { + mavenJava(MavenPublication) { + pom { + name = 'Cardano Client HD Wallet' + description = 'Cardano Client Lib - HD Wallet Integration' + } + } + } +} diff --git a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/HDWallet.java b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/HDWallet.java new file mode 100644 index 00000000..1b47ed2f --- /dev/null +++ b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/HDWallet.java @@ -0,0 +1,154 @@ +package com.bloxbean.cardano.hdwallet; + +import com.bloxbean.cardano.client.account.Account; +import com.bloxbean.cardano.client.address.Address; +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.common.MnemonicUtil; +import com.bloxbean.cardano.client.common.model.Network; +import com.bloxbean.cardano.client.common.model.Networks; +import com.bloxbean.cardano.client.crypto.bip32.HdKeyGenerator; +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.DerivationPath; +import lombok.Getter; +import lombok.Setter; +import lombok.SneakyThrows; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +public class HDWallet { + + @Setter + @Getter + private int account = 0; + @Getter + private Network network; + @Getter + private String mnemonic; + private int startIndex = 0; + + public HDWallet() { + this(Networks.mainnet()); + } + + public HDWallet(Network network) { + this(network, Words.TWENTY_FOUR); + } + + public HDWallet(Network network, Words noOfWords) { + this(network, noOfWords, 0); + } + + public HDWallet(Network network, Words noOfWords, int account) { + this.network = network; + this.mnemonic = MnemonicUtil.generateNew(noOfWords); + this.account = account; + } + + public HDWallet(String mnemonic) { + this(Networks.mainnet(), mnemonic); + } + + public HDWallet(Network network, String mnemonic) { + this(network,mnemonic, 0); + } + + public HDWallet(Network network, String mnemonic, int account) { + this.network = network; + this.mnemonic = mnemonic; + this.account = account; + MnemonicUtil.validateMnemonic(this.mnemonic); + } + + public Address getEntAddress(int index) { + return getEntAddress(this.account, index); + } + + private Address getEntAddress(int account, int index) { + DerivationPath derivationPath = DerivationPath.createExternalAddressDerivationPathForAccount(account); + derivationPath.getIndex().setValue(index); + + return new Account(this.network, this.mnemonic, derivationPath).getEnterpriseAddress(); + } + + public Address getBaseAddress(int index) { + return getBaseAddress(this.account, index); + } + + public Address getBaseAddress(int account, int index) { + DerivationPath derivationPath = DerivationPath.createExternalAddressDerivationPathForAccount(account); + derivationPath.getIndex().setValue(index); + return new Account(this.network, this.mnemonic, derivationPath).getBaseAddress(); + } + + public HdKeyPair getHDWalletKeyPair() { + HdKeyGenerator hdKeyGenerator = new HdKeyGenerator(); + HdKeyPair rootKeys; + try { + byte[] entropy = MnemonicCode.INSTANCE.toEntropy(this.mnemonic); + rootKeys = hdKeyGenerator.getRootKeyPairFromEntropy(entropy); + } catch (MnemonicException.MnemonicLengthException e) { + throw new RuntimeException(e); + } catch (MnemonicException.MnemonicWordException e) { + throw new RuntimeException(e); + } catch (MnemonicException.MnemonicChecksumException e) { + throw new RuntimeException(e); + } + return rootKeys; + } + + /** + * Scanning all accounts takes to much time for now + * @param backendService + * @return + */ + public List getUtxos(BackendService backendService) { + return getUtxos(this.account, backendService); + } + + @SneakyThrows + public List getUtxos(int account, BackendService backendService) { + List utxos = new ArrayList<>(); + int index = this.startIndex; + int noUtxoFound = 0; + while(noUtxoFound < 20) { + List utxoFromIndex = getUtxos(account, index, backendService); + utxos.addAll(utxoFromIndex); + noUtxoFound = utxoFromIndex.isEmpty()? noUtxoFound + 1 : 0; + + index++; // increasing search index + } + return utxos; + } + + @SneakyThrows + public List getUtxos(int account, int index, BackendService backendService) { + List utxos = new ArrayList<>(); + Address address = getBaseAddress(account, index); + boolean fetchNextPage = false; + int page = 1; + final int MAX_PAGE_SIZE = 100; + do { + Result> utxos1 = backendService.getUtxoService().getUtxos(address.getAddress(), MAX_PAGE_SIZE, page); + if(utxos1.isSuccessful()) { + List value = utxos1.getValue(); + utxos.addAll(value); + if(value.size() == MAX_PAGE_SIZE) { // need to fetch next page + fetchNextPage = true; + page++; + } else { + fetchNextPage = false; + } + } + } while (fetchNextPage); + return utxos; + } + + +} diff --git a/hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/HDWalletTest.java b/hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/HDWalletTest.java new file mode 100644 index 00000000..78f58ceb --- /dev/null +++ b/hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/HDWalletTest.java @@ -0,0 +1,45 @@ +package com.bloxbean.cardano.hdwallet; + +import com.bloxbean.cardano.client.account.Account; +import com.bloxbean.cardano.client.address.Address; +import com.bloxbean.cardano.client.api.model.Utxo; +import com.bloxbean.cardano.client.backend.api.BackendService; +import com.bloxbean.cardano.client.backend.blockfrost.common.Constants; +import com.bloxbean.cardano.client.backend.blockfrost.service.BFBackendService; +import com.bloxbean.cardano.client.common.model.Networks; +import com.bloxbean.cardano.client.crypto.bip32.HdKeyPair; +import com.bloxbean.cardano.client.crypto.cip1852.CIP1852; +import com.bloxbean.cardano.client.crypto.cip1852.DerivationPath; +import com.bloxbean.cardano.client.crypto.cip1852.Segment; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class HDWalletTest { + + @Test + void generateMnemonicTest() { + HDWallet hdWallet = new HDWallet(); + String mnemonic = hdWallet.getMnemonic(); + assertEquals(24, mnemonic.split(" ").length); + } + + @Test + void getAccountFromIndex() { + HDWallet hdWallet = new HDWallet(); + Address address = hdWallet.getBaseAddress(0); + Account a = new Account(hdWallet.getNetwork(), 0); + assertEquals(address.getAddress(), a.getBaseAddress().getAddress()); + } + + @Test + void utxoTest() { + BackendService backendService = new BFBackendService(Constants.BLOCKFROST_PREPROD_URL, "preprodEwHzH2AgK20tmjrQnIN0P6zh65mUjSvR"); + String sender3Mnemonic = "clog book honey force cricket stamp until seed minimum margin denial kind volume undo simple federal then jealous solid legal crucial crazy acoustic thank"; + HDWallet hdWallet = new HDWallet(Networks.preprod(), sender3Mnemonic); + List utxos = hdWallet.getUtxos(backendService); + System.out.println("aasd"); + } +} diff --git a/settings.gradle b/settings.gradle index 5d840a68..d9836438 100644 --- a/settings.gradle +++ b/settings.gradle @@ -13,6 +13,7 @@ include 'core' include 'function' include 'quicktx' include 'annotation-processor' +include 'hd-wallet' //CIPs include 'cip' From bb48752cd04ae4791e51e1326edadad01426f5e0 Mon Sep 17 00:00:00 2001 From: Kammerlo Date: Wed, 17 Jan 2024 16:10:38 +0100 Subject: [PATCH 03/16] basic transaction working now. Still need to refactor a lot! But first good step. --- function/build.gradle | 1 + .../function/helper/SignerProviders.java | 8 + hd-wallet/build.gradle | 8 +- .../cardano/hdwallet/QuickTXBaseIT.java | 81 ++++++++++ .../cardano/hdwallet/QuickTxBuilderIT.java | 132 +++++++++++++++++ ...aultHDWalletUtxoSelectionStrategyImpl.java | 138 ++++++++++++++++++ .../bloxbean/cardano/hdwallet/HDWallet.java | 130 ++++++++++++----- .../cardano/hdwallet/HDWalletInterface.java | 3 +- .../HDWalletUtxoSelectionStrategy.java | 13 ++ .../cardano/hdwallet/HDWalletTest.java | 26 +--- quicktx/build.gradle | 1 + .../cardano/client/quicktx/AbstractTx.java | 11 +- .../cardano/client/quicktx/ScriptTx.java | 6 + .../bloxbean/cardano/client/quicktx/Tx.java | 22 +++ 14 files changed, 512 insertions(+), 68 deletions(-) create mode 100644 hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTXBaseIT.java create mode 100644 hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTxBuilderIT.java create mode 100644 hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/DefaultHDWalletUtxoSelectionStrategyImpl.java create mode 100644 hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/HDWalletUtxoSelectionStrategy.java diff --git a/function/build.gradle b/function/build.gradle index a62ab4a6..f885924b 100644 --- a/function/build.gradle +++ b/function/build.gradle @@ -1,5 +1,6 @@ dependencies { api project(':core') + api project(':hd-wallet') } publishing { diff --git a/function/src/main/java/com/bloxbean/cardano/client/function/helper/SignerProviders.java b/function/src/main/java/com/bloxbean/cardano/client/function/helper/SignerProviders.java index 78b478b4..a1840169 100644 --- a/function/src/main/java/com/bloxbean/cardano/client/function/helper/SignerProviders.java +++ b/function/src/main/java/com/bloxbean/cardano/client/function/helper/SignerProviders.java @@ -7,6 +7,7 @@ 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.HDWallet; /** * Provides helper methods to get TxSigner function to sign a {@link Transaction} object @@ -30,6 +31,13 @@ public static TxSigner signerFrom(Account... signers) { }; } + public static TxSigner signerFrom(HDWallet wallet) { + return transaction -> { + Transaction outputTxn = wallet.sign(transaction); + return outputTxn; + }; + } + /** * Function to sign a transaction with one or more SecretKey * @param secretKeys secret keys to sign the transaction diff --git a/hd-wallet/build.gradle b/hd-wallet/build.gradle index 50db06bf..c9e12301 100644 --- a/hd-wallet/build.gradle +++ b/hd-wallet/build.gradle @@ -4,8 +4,14 @@ dependencies { api project(':common') api project(':crypto') api project(':backend') - api project(':backend-modules:blockfrost') implementation(libs.bouncycastle.bcprov) + + integrationTestImplementation project(':') + integrationTestImplementation project(':backend-modules:blockfrost') + integrationTestImplementation project(':backend-modules:koios') + integrationTestImplementation project(':backend-modules:ogmios') + + integrationTestAnnotationProcessor project(':annotation-processor') } publishing { diff --git a/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTXBaseIT.java b/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTXBaseIT.java new file mode 100644 index 00000000..95477cf7 --- /dev/null +++ b/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTXBaseIT.java @@ -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 = BLOCKFROST; + + 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 result) { + try { + if (result.isSuccessful()) { //Wait for transaction to be mined + int count = 0; + while (count < 60) { + Result 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 = Optional.empty(); + int count = 0; + while (utxo.isEmpty()) { + if (count++ >= 20) + break; + List 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) {} + } + } +} diff --git a/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTxBuilderIT.java b/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTxBuilderIT.java new file mode 100644 index 00000000..ea05f633 --- /dev/null +++ b/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTxBuilderIT.java @@ -0,0 +1,132 @@ +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.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.blockfrost.service.BFUtxoService; +import com.bloxbean.cardano.client.common.model.Networks; +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 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; + HDWallet wallet1; + HDWallet 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 HDWallet(Networks.testnet(), wallet1Mnemonic, utxoSupplier); + 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 HDWallet(Networks.testnet(), wallet2Mnemonic, utxoSupplier); + } + + @Test + void simplePayment() { + Metadata metadata = MetadataBuilder.createMetadata(); + metadata.put(BigInteger.valueOf(100), "This is first metadata"); + metadata.putNegative(200, -900); + + QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService); + + Tx tx = new Tx() + .payToAddress(wallet2.getBaseAddress(0).getAddress(), Amount.ada(3)) + .from(wallet1); + + Result result = quickTxBuilder.compose(tx) + .withSigner(SignerProviders.signerFrom(wallet1)) + .complete(); + + System.out.println(result); + assertTrue(result.isSuccessful()); + waitForTransaction(result); + + checkIfUtxoAvailable(result.getValue(), wallet2.getBaseAddress(0).getAddress()); + } + + @Test + void simplePayment2() { + Metadata metadata = MetadataBuilder.createMetadata(); + metadata.put(BigInteger.valueOf(100), "This is first metadata"); + metadata.putNegative(200, -900); + + QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService); + Tx tx = new Tx() + .payToAddress(wallet2.getBaseAddress(0).getAddress(), Amount.ada(3)) + + .from(wallet1.getBaseAddress(0).getAddress()); // TODO - Set a HDWallet here + + Result result = quickTxBuilder.compose(tx) + .withSigner(SignerProviders.signerFrom(wallet1.getSigner(0))) + .complete(); + + System.out.println(result); + assertTrue(result.isSuccessful()); + waitForTransaction(result); + + checkIfUtxoAvailable(result.getValue(), wallet2.getBaseAddress(0).getAddress()); + } + + @Test + void simplePayment3() { + Metadata metadata = MetadataBuilder.createMetadata(); + metadata.put(BigInteger.valueOf(100), "This is first metadata"); + metadata.putNegative(200, -900); + + QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService); + Tx tx = new Tx() + .payToAddress(wallet1.getBaseAddress(0).getAddress(), Amount.ada(7)) + .from(wallet2); // TODO - Set a HDWallet here + QuickTxBuilder.TxContext compose = quickTxBuilder.compose(tx); + + compose = compose.withSigner(SignerProviders.signerFrom(wallet2.getSigner(0))); + compose = compose.withSigner(SignerProviders.signerFrom(wallet2.getSigner(1))); + + Result result = compose.complete(); + + System.out.println(result); + assertTrue(result.isSuccessful()); + waitForTransaction(result); + + checkIfUtxoAvailable(result.getValue(), wallet2.getBaseAddress(0).getAddress()); + } + + @Test + void utxoTest() { + List utxos = wallet1.getUtxos(); + Map 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()); + } +} \ No newline at end of file diff --git a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/DefaultHDWalletUtxoSelectionStrategyImpl.java b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/DefaultHDWalletUtxoSelectionStrategyImpl.java new file mode 100644 index 00000000..8df4461d --- /dev/null +++ b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/DefaultHDWalletUtxoSelectionStrategyImpl.java @@ -0,0 +1,138 @@ +package com.bloxbean.cardano.hdwallet; + +import com.bloxbean.cardano.client.api.UtxoSupplier; +import com.bloxbean.cardano.client.api.exception.ApiRuntimeException; +import com.bloxbean.cardano.client.api.exception.InsufficientBalanceException; +import com.bloxbean.cardano.client.api.model.Amount; +import com.bloxbean.cardano.client.api.model.Utxo; +import com.bloxbean.cardano.client.coinselection.UtxoSelectionStrategy; +import com.bloxbean.cardano.client.coinselection.exception.InputsLimitExceededException; +import com.bloxbean.cardano.client.plutus.spec.PlutusData; +import lombok.Setter; + +import java.math.BigInteger; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Out-of-box implementation of {@link UtxoSelectionStrategy} + * Applications can provide their own custom implementation + */ +public class DefaultHDWalletUtxoSelectionStrategyImpl implements HDWalletUtxoSelectionStrategy{ + + private final UtxoSupplier utxoSupplier; + @Setter + private boolean ignoreUtxosWithDatumHash; + + public DefaultHDWalletUtxoSelectionStrategyImpl(UtxoSupplier utxoSupplier) { + this(utxoSupplier, true); + } + + public DefaultHDWalletUtxoSelectionStrategyImpl(UtxoSupplier utxoSupplier, boolean ignoreUtxosWithDatumHash) { + this.utxoSupplier = utxoSupplier; + this.ignoreUtxosWithDatumHash = ignoreUtxosWithDatumHash; + } + + public Set select(List inputUtxos, List outputAmounts, String datumHash, PlutusData inlineDatum, Set utxosToExclude, int maxUtxoSelectionLimit) { + if(outputAmounts == null || outputAmounts.isEmpty()){ + return Collections.emptySet(); + } + + //TODO -- Should we throw error if both datumHash and inlineDatum are set ?? + + try{ + // loop over utxo's, find matching requested amount + Set selectedUtxos = new HashSet<>(); + + final Map remaining = new HashMap<>(outputAmounts.stream() + .collect(Collectors.groupingBy(Amount::getUnit, + Collectors.reducing(BigInteger.ZERO, + Amount::getQuantity, + BigInteger::add)))) + .entrySet().stream() + .filter(entry -> BigInteger.ZERO.compareTo(entry.getValue()) < 0) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + while(!remaining.isEmpty()){ + + + var sorted = inputUtxos != null + ? inputUtxos.stream() + .sorted(sortByMostMatchingAssets(outputAmounts)) + .collect(Collectors.toList()) + : Collections.emptyList(); + + for(Utxo utxo : sorted) { + if(!accept(utxo)){ + continue; + } + if(utxosToExclude != null && utxosToExclude.contains(utxo)){ + continue; + } + if(utxo.getDataHash() != null && !utxo.getDataHash().isEmpty() && ignoreUtxosWithDatumHash){ + continue; + } + if(datumHash != null && !datumHash.isEmpty() && !datumHash.equals(utxo.getDataHash())){ + continue; + } + //TODO - add tests for this scenario + if(inlineDatum != null && !inlineDatum.serializeToHex().equals(utxo.getInlineDatum())) { + continue; + } + if(selectedUtxos.contains(utxo)){ + continue; + } + List utxoAmounts = utxo.getAmount(); + + boolean utxoSelected = false; + for(Amount amount : utxoAmounts) { + var remainingAmount = remaining.get(amount.getUnit()); + if(remainingAmount != null && BigInteger.ZERO.compareTo(remainingAmount) < 0){ + utxoSelected = true; + var newRemaining = remainingAmount.subtract(amount.getQuantity()); + if(BigInteger.ZERO.compareTo(newRemaining) < 0){ + remaining.put(amount.getUnit(), newRemaining); + }else{ + remaining.remove(amount.getUnit()); + } + } + } + + if(utxoSelected){ + selectedUtxos.add(utxo); + if(!remaining.isEmpty() && selectedUtxos.size() > maxUtxoSelectionLimit){ + throw new InputsLimitExceededException("Selection limit of " + maxUtxoSelectionLimit + " utxos reached with " + remaining + " remaining"); + } + } + } + if(sorted.isEmpty()){ + break; + } + } + if(!remaining.isEmpty()){ + throw new InsufficientBalanceException("Not enough funds for [" + remaining + "], in Wallet"); + } + return selectedUtxos; + }catch(InputsLimitExceededException e){ + throw new ApiRuntimeException("Input limit exceeded and no fallback provided", e); + } + } + + private static Comparator sortByMostMatchingAssets(List outputAmounts){ + // first process utxos which contain most matching assets + return (o1, o2) -> Integer.compare(countMatchingAssets(o2.getAmount(), outputAmounts), countMatchingAssets(o1.getAmount(), outputAmounts)); + } + private static int countMatchingAssets(List l1, List outputAmounts){ + if(l1 == null || l1.isEmpty() || outputAmounts == null || outputAmounts.isEmpty()){ + return 0; + } + return (int) l1.stream() + .filter(it1 -> outputAmounts.stream().filter(outputAmount -> it1.getUnit() != null && it1.getUnit().equals(outputAmount.getUnit())).findFirst().isPresent()) + .map(it -> 1) + .count(); + } + + protected boolean accept(Utxo utxo) { + return true; + } +} diff --git a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/HDWallet.java b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/HDWallet.java index 1b47ed2f..93938e46 100644 --- a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/HDWallet.java +++ b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/HDWallet.java @@ -2,9 +2,12 @@ import com.bloxbean.cardano.client.account.Account; import com.bloxbean.cardano.client.address.Address; +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.backend.api.BackendService; +import com.bloxbean.cardano.client.coinselection.impl.DefaultUtxoSelectionStrategyImpl; import com.bloxbean.cardano.client.common.MnemonicUtil; import com.bloxbean.cardano.client.common.model.Network; import com.bloxbean.cardano.client.common.model.Networks; @@ -14,13 +17,17 @@ import com.bloxbean.cardano.client.crypto.bip39.MnemonicException; import com.bloxbean.cardano.client.crypto.bip39.Words; import com.bloxbean.cardano.client.crypto.cip1852.DerivationPath; +import com.bloxbean.cardano.client.transaction.spec.Transaction; +import com.bloxbean.cardano.client.transaction.spec.TransactionInput; +import com.bloxbean.cardano.client.transaction.spec.TransactionOutput; +import com.bloxbean.cardano.client.transaction.spec.Value; import lombok.Getter; import lombok.Setter; import lombok.SneakyThrows; -import java.util.ArrayList; -import java.util.List; -import java.util.function.Function; +import java.math.BigInteger; +import java.util.*; +import java.util.stream.Collectors; public class HDWallet { @@ -32,38 +39,47 @@ public class HDWallet { @Getter private String mnemonic; private int startIndex = 0; + @Setter + private UtxoSupplier utxoSupplier; + @Setter + HDWalletUtxoSelectionStrategy utxoSelectionStrategy; - public HDWallet() { - this(Networks.mainnet()); + public HDWallet(UtxoSupplier utxoSupplier) { + this(Networks.mainnet(), utxoSupplier); } - public HDWallet(Network network) { - this(network, Words.TWENTY_FOUR); + public HDWallet(Network network, UtxoSupplier utxoSupplier) { + this(network, Words.TWENTY_FOUR, utxoSupplier); } - public HDWallet(Network network, Words noOfWords) { - this(network, noOfWords, 0); + public HDWallet(Network network, Words noOfWords, UtxoSupplier utxoSupplier) { + this(network, noOfWords, 0, utxoSupplier); } - public HDWallet(Network network, Words noOfWords, int account) { + public HDWallet(Network network, Words noOfWords, int account, UtxoSupplier utxoSupplier) { this.network = network; this.mnemonic = MnemonicUtil.generateNew(noOfWords); this.account = account; + this.utxoSupplier = utxoSupplier; + this.utxoSelectionStrategy = new DefaultHDWalletUtxoSelectionStrategyImpl(utxoSupplier); + } - public HDWallet(String mnemonic) { - this(Networks.mainnet(), mnemonic); + public HDWallet(String mnemonic, UtxoSupplier utxoSupplier) { + this(Networks.mainnet(), mnemonic, utxoSupplier); } - public HDWallet(Network network, String mnemonic) { - this(network,mnemonic, 0); + public HDWallet(Network network, String mnemonic, UtxoSupplier utxoSupplier) { + this(network,mnemonic, 0, utxoSupplier); } - public HDWallet(Network network, String mnemonic, int account) { + public HDWallet(Network network, String mnemonic, int account, UtxoSupplier utxoSupplier) { this.network = network; this.mnemonic = mnemonic; this.account = account; MnemonicUtil.validateMnemonic(this.mnemonic); + this.utxoSupplier = utxoSupplier; + this.utxoSelectionStrategy = new DefaultHDWalletUtxoSelectionStrategyImpl(utxoSupplier); } public Address getEntAddress(int index) { @@ -105,20 +121,19 @@ public HdKeyPair getHDWalletKeyPair() { /** * Scanning all accounts takes to much time for now - * @param backendService * @return */ - public List getUtxos(BackendService backendService) { - return getUtxos(this.account, backendService); + public List getUtxos() { + return getUtxos(this.account); } @SneakyThrows - public List getUtxos(int account, BackendService backendService) { + public List getUtxos(int account) { List utxos = new ArrayList<>(); int index = this.startIndex; int noUtxoFound = 0; while(noUtxoFound < 20) { - List utxoFromIndex = getUtxos(account, index, backendService); + List utxoFromIndex = getUtxos(account, index); utxos.addAll(utxoFromIndex); noUtxoFound = utxoFromIndex.isEmpty()? noUtxoFound + 1 : 0; @@ -128,27 +143,64 @@ public List getUtxos(int account, BackendService backendService) { } @SneakyThrows - public List getUtxos(int account, int index, BackendService backendService) { - List utxos = new ArrayList<>(); + public List getUtxos(int account, int index) { Address address = getBaseAddress(account, index); - boolean fetchNextPage = false; - int page = 1; - final int MAX_PAGE_SIZE = 100; - do { - Result> utxos1 = backendService.getUtxoService().getUtxos(address.getAddress(), MAX_PAGE_SIZE, page); - if(utxos1.isSuccessful()) { - List value = utxos1.getValue(); - utxos.addAll(value); - if(value.size() == MAX_PAGE_SIZE) { // need to fetch next page - fetchNextPage = true; - page++; - } else { - fetchNextPage = false; - } - } - } while (fetchNextPage); - return utxos; + return utxoSupplier.getAll(address.getAddress()); } + public Account getSigner(int account, int index) { + DerivationPath derivationPath = DerivationPath.createDRepKeyDerivationPathForAccount(account); + derivationPath.getIndex().setValue(index); + return new Account(this.network, this.mnemonic, derivationPath); + } + + public Account getSigner(int index) { +// return getSigner(this.account, index); + return new Account(this.network, this.mnemonic, index); + } + // Pretty stupid bruteforce approach for iteration 1 + public Transaction sign(Transaction outputTxn) { + List signers = getSignersForInputs(outputTxn.getBody().getInputs()); + if(signers.isEmpty()) + throw new RuntimeException("No signers found!"); + Transaction signed = outputTxn; + for (Account signer : signers) { + signed = signer.sign(signed); + } + return signed; + } + + + private List getSignersForInputs(List inputs) { + // searching for address to sign + List signers = new ArrayList<>(); + List remaining = new ArrayList<>(inputs); + int index = 0; + int emptyCounter = 0; + while (!remaining.isEmpty() || emptyCounter >= 20) { + List utxos = getUtxos(this.account, index); + if(utxos.isEmpty()) { + emptyCounter++; + } else { + emptyCounter = 0; + } + for (Utxo utxo : utxos) { + for (TransactionInput input : inputs) { + if(utxo.getTxHash().equals(input.getTransactionId())) { + signers.add(getSigner(index)); + remaining.remove(input); + } + } + if(remaining.isEmpty()) + break; + } + index++; + } + return signers; + } + public List getUtxosForOutputs(List outputs) { + List allUtxosFromWallet = getUtxos(); + return new ArrayList(utxoSelectionStrategy.select(allUtxosFromWallet, outputs, null, null, null, Integer.MAX_VALUE)); + } } diff --git a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/HDWalletInterface.java b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/HDWalletInterface.java index 4a863606..0fd45020 100644 --- a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/HDWalletInterface.java +++ b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/HDWalletInterface.java @@ -7,8 +7,7 @@ public interface HDWalletInterface { - private String mnemonic = ""; - +// private String mnemonic = ""; // public HDWalletInterface(Network network, String mnemonic); // public HDWalletInterface(Network network); diff --git a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/HDWalletUtxoSelectionStrategy.java b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/HDWalletUtxoSelectionStrategy.java new file mode 100644 index 00000000..1b740556 --- /dev/null +++ b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/HDWalletUtxoSelectionStrategy.java @@ -0,0 +1,13 @@ +package com.bloxbean.cardano.hdwallet; + +import com.bloxbean.cardano.client.api.model.Amount; +import com.bloxbean.cardano.client.api.model.Utxo; +import com.bloxbean.cardano.client.plutus.spec.PlutusData; + +import java.util.List; +import java.util.Set; + +public interface HDWalletUtxoSelectionStrategy { + + Set select(List inputUtxos, List outputAmounts, String datumHash, PlutusData inlineDatum, Set utxosToExclude, int maxUtxoSelectionLimit); +} diff --git a/hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/HDWalletTest.java b/hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/HDWalletTest.java index 78f58ceb..171961a7 100644 --- a/hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/HDWalletTest.java +++ b/hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/HDWalletTest.java @@ -2,44 +2,24 @@ import com.bloxbean.cardano.client.account.Account; import com.bloxbean.cardano.client.address.Address; -import com.bloxbean.cardano.client.api.model.Utxo; -import com.bloxbean.cardano.client.backend.api.BackendService; -import com.bloxbean.cardano.client.backend.blockfrost.common.Constants; -import com.bloxbean.cardano.client.backend.blockfrost.service.BFBackendService; -import com.bloxbean.cardano.client.common.model.Networks; -import com.bloxbean.cardano.client.crypto.bip32.HdKeyPair; -import com.bloxbean.cardano.client.crypto.cip1852.CIP1852; -import com.bloxbean.cardano.client.crypto.cip1852.DerivationPath; -import com.bloxbean.cardano.client.crypto.cip1852.Segment; import org.junit.jupiter.api.Test; -import java.util.List; - import static org.junit.jupiter.api.Assertions.assertEquals; public class HDWalletTest { @Test void generateMnemonicTest() { - HDWallet hdWallet = new HDWallet(); + HDWallet hdWallet = new HDWallet(null); String mnemonic = hdWallet.getMnemonic(); assertEquals(24, mnemonic.split(" ").length); } @Test void getAccountFromIndex() { - HDWallet hdWallet = new HDWallet(); + HDWallet hdWallet = new HDWallet(null); Address address = hdWallet.getBaseAddress(0); - Account a = new Account(hdWallet.getNetwork(), 0); + Account a = new Account(hdWallet.getNetwork(), hdWallet.getMnemonic(), 0); assertEquals(address.getAddress(), a.getBaseAddress().getAddress()); } - - @Test - void utxoTest() { - BackendService backendService = new BFBackendService(Constants.BLOCKFROST_PREPROD_URL, "preprodEwHzH2AgK20tmjrQnIN0P6zh65mUjSvR"); - String sender3Mnemonic = "clog book honey force cricket stamp until seed minimum margin denial kind volume undo simple federal then jealous solid legal crucial crazy acoustic thank"; - HDWallet hdWallet = new HDWallet(Networks.preprod(), sender3Mnemonic); - List utxos = hdWallet.getUtxos(backendService); - System.out.println("aasd"); - } } diff --git a/quicktx/build.gradle b/quicktx/build.gradle index 797a6e69..f7c4df3b 100644 --- a/quicktx/build.gradle +++ b/quicktx/build.gradle @@ -2,6 +2,7 @@ dependencies { api project(':core') api project(':function') api project(':backend') + api project(':hd-wallet') integrationTestImplementation(libs.slf4j.log4j) integrationTestImplementation(libs.aiken.java.binding) diff --git a/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/AbstractTx.java b/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/AbstractTx.java index 8c76ce56..9cad8875 100644 --- a/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/AbstractTx.java +++ b/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/AbstractTx.java @@ -18,12 +18,14 @@ import com.bloxbean.cardano.client.transaction.spec.*; import com.bloxbean.cardano.client.util.HexUtil; import com.bloxbean.cardano.client.util.Tuple; +import com.bloxbean.cardano.hdwallet.HDWallet; import lombok.NonNull; import lombok.SneakyThrows; import java.math.BigInteger; import java.util.ArrayList; import java.util.List; +import java.util.Set; import static com.bloxbean.cardano.client.common.CardanoConstants.LOVELACE; @@ -34,6 +36,7 @@ public abstract class AbstractTx { //custom change address protected String changeAddress; protected List inputUtxos; + protected List amounts; //Required for script protected PlutusData changeData; @@ -182,7 +185,9 @@ protected T payToAddress(String address, List amounts, byte[] datumHash, .address(address) .value(Value.builder().coin(BigInteger.ZERO).build()) .build(); - + if(this.amounts == null) + this.amounts = new ArrayList<>(); + this.amounts.addAll(amounts); for (Amount amount : amounts) { String unit = amount.getUnit(); if (unit.equals(LOVELACE)) { @@ -211,7 +216,6 @@ protected T payToAddress(String address, List amounts, byte[] datumHash, if (outputs == null) outputs = new ArrayList<>(); outputs.add(transactionOutput); - return (T) this; } @@ -368,6 +372,8 @@ protected void addToMultiAssetList(@NonNull Script script, List assets) { */ protected abstract String getFromAddress(); + protected abstract HDWallet getFromWallet(); + /** * Perform post balanceTx action * @@ -386,5 +392,4 @@ protected void addToMultiAssetList(@NonNull Script script, List assets) { * @return String */ protected abstract String getFeePayer(); - } diff --git a/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/ScriptTx.java b/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/ScriptTx.java index 856bd856..afa6af55 100644 --- a/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/ScriptTx.java +++ b/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/ScriptTx.java @@ -11,6 +11,7 @@ import com.bloxbean.cardano.client.plutus.spec.*; import com.bloxbean.cardano.client.transaction.spec.*; import com.bloxbean.cardano.client.util.Tuple; +import com.bloxbean.cardano.hdwallet.HDWallet; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NonNull; @@ -502,6 +503,11 @@ protected String getFromAddress() { return fromAddress; } + @Override + protected HDWallet getFromWallet() { + return null; + } + void from(String address) { this.fromAddress = address; } diff --git a/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/Tx.java b/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/Tx.java index 468bc5a7..e1bed734 100644 --- a/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/Tx.java +++ b/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/Tx.java @@ -19,9 +19,11 @@ import com.bloxbean.cardano.client.transaction.spec.governance.actions.GovActionId; import com.bloxbean.cardano.client.transaction.spec.script.NativeScript; import com.bloxbean.cardano.client.util.Tuple; +import com.bloxbean.cardano.hdwallet.HDWallet; import lombok.NonNull; import java.math.BigInteger; +import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -32,6 +34,7 @@ public class Tx extends AbstractTx { private String sender; protected boolean senderAdded = false; + private HDWallet senderWallet; /** * Create Tx @@ -121,6 +124,15 @@ public Tx from(String sender) { return this; } + public Tx from(HDWallet sender) { + verifySenderNotExists(); + this.inputUtxos = sender.getUtxosForOutputs(amounts); + this.sender = this.inputUtxos.get(0).getAddress(); // TODO - is it clever to use the first address as sender here? + this.changeAddress = this.sender; + this.senderAdded = true; + return this; + } + /** * Create Tx with given utxos as inputs. * @param utxos List of utxos @@ -465,6 +477,8 @@ protected String getChangeAddress() { return changeAddress; else if (sender != null) return sender; + else if (senderWallet != null) + return senderWallet.getBaseAddress(0).getAddress(); // TODO - Change address to a new index?? else throw new TxBuildException("No change address. " + "Please define at least one of sender address or sender account or change address"); @@ -478,6 +492,14 @@ protected String getFromAddress() { throw new TxBuildException("No sender address or sender account defined"); } + @Override + protected HDWallet getFromWallet() { + if(senderWallet != null) + return senderWallet; + else + throw new TxBuildException("No sender wallet defined"); + } + @Override protected void postBalanceTx(Transaction transaction) { From b14741acebf539ce131c5ab6ee9c0ca6823374ad Mon Sep 17 00:00:00 2001 From: Kammerlo Date: Thu, 18 Jan 2024 13:39:35 +0100 Subject: [PATCH 04/16] #213 added WalletUtxoSupplier and removed getUtxo functionalities from Wallet.java Need to rework signing to get addresses. --- .../function/helper/SignerProviders.java | 7 +- .../cardano/hdwallet/QuickTxBuilderIT.java | 24 +-- ...aultHDWalletUtxoSelectionStrategyImpl.java | 138 ---------------- .../hdwallet/{HDWallet.java => Wallet.java} | 154 +++++++++--------- ...letInterface.java => WalletInterface.java} | 2 +- .../cardano/hdwallet/WalletUtxoSupplier.java | 105 ++++++++++++ .../{HDWalletTest.java => WalletTest.java} | 7 +- .../cardano/client/quicktx/AbstractTx.java | 5 +- .../client/quicktx/QuickTxBuilder.java | 6 + .../cardano/client/quicktx/ScriptTx.java | 4 +- .../bloxbean/cardano/client/quicktx/Tx.java | 14 +- 11 files changed, 224 insertions(+), 242 deletions(-) delete mode 100644 hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/DefaultHDWalletUtxoSelectionStrategyImpl.java rename hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/{HDWallet.java => Wallet.java} (58%) rename hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/{HDWalletInterface.java => WalletInterface.java} (97%) create mode 100644 hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/WalletUtxoSupplier.java rename hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/{HDWalletTest.java => WalletTest.java} (75%) diff --git a/function/src/main/java/com/bloxbean/cardano/client/function/helper/SignerProviders.java b/function/src/main/java/com/bloxbean/cardano/client/function/helper/SignerProviders.java index a1840169..c3554e8d 100644 --- a/function/src/main/java/com/bloxbean/cardano/client/function/helper/SignerProviders.java +++ b/function/src/main/java/com/bloxbean/cardano/client/function/helper/SignerProviders.java @@ -7,7 +7,7 @@ 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.HDWallet; +import com.bloxbean.cardano.hdwallet.Wallet; /** * Provides helper methods to get TxSigner function to sign a {@link Transaction} object @@ -31,9 +31,10 @@ public static TxSigner signerFrom(Account... signers) { }; } - public static TxSigner signerFrom(HDWallet wallet) { + public static TxSigner signerFrom(Wallet wallet) { return transaction -> { - Transaction outputTxn = wallet.sign(transaction); +// transaction.getBody().getRequiredSigners(); // TODO - look into using this field - it is normally used for smart contracts. Downside it will increase TX size. + Transaction outputTxn = wallet.sign(transaction); // TODO - check if it's possible to get the context here to avoid fetching all utxos over and over again return outputTxn; }; } diff --git a/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTxBuilderIT.java b/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTxBuilderIT.java index ea05f633..d0108e46 100644 --- a/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTxBuilderIT.java +++ b/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTxBuilderIT.java @@ -5,10 +5,7 @@ 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.blockfrost.service.BFUtxoService; +import com.bloxbean.cardano.client.coinselection.impl.DefaultUtxoSelectionStrategyImpl; import com.bloxbean.cardano.client.common.model.Networks; import com.bloxbean.cardano.client.function.helper.SignerProviders; import com.bloxbean.cardano.client.metadata.Metadata; @@ -29,18 +26,19 @@ public class QuickTxBuilderIT extends QuickTXBaseIT { BackendService backendService; UtxoSupplier utxoSupplier; - HDWallet wallet1; - HDWallet wallet2; + WalletUtxoSupplier walletUtxoSupplier; + Wallet wallet1; + Wallet wallet2; @BeforeEach void setup() { backendService = getBackendService(); utxoSupplier = getUTXOSupplier(); - + walletUtxoSupplier = new WalletUtxoSupplier(backendService.getUtxoService()); 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 HDWallet(Networks.testnet(), wallet1Mnemonic, utxoSupplier); + wallet1 = new Wallet(Networks.testnet(), wallet1Mnemonic, walletUtxoSupplier); 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 HDWallet(Networks.testnet(), wallet2Mnemonic, utxoSupplier); + wallet2 = new Wallet(Networks.testnet(), wallet2Mnemonic, walletUtxoSupplier); } @Test @@ -49,10 +47,12 @@ void simplePayment() { metadata.put(BigInteger.valueOf(100), "This is first metadata"); metadata.putNegative(200, -900); - QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService); + + UtxoSupplier walletUtxoSupplier = new WalletUtxoSupplier(backendService.getUtxoService(), wallet1); + QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService, walletUtxoSupplier); Tx tx = new Tx() - .payToAddress(wallet2.getBaseAddress(0).getAddress(), Amount.ada(3)) + .payToAddress(wallet2.getBaseAddress(0).getAddress(), Amount.ada(4)) .from(wallet1); Result result = quickTxBuilder.compose(tx) @@ -115,7 +115,7 @@ void simplePayment3() { @Test void utxoTest() { - List utxos = wallet1.getUtxos(); + List utxos = walletUtxoSupplier.getAll(wallet1); Map amountMap = new HashMap<>(); for (Utxo utxo : utxos) { int totalAmount = 0; diff --git a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/DefaultHDWalletUtxoSelectionStrategyImpl.java b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/DefaultHDWalletUtxoSelectionStrategyImpl.java deleted file mode 100644 index 8df4461d..00000000 --- a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/DefaultHDWalletUtxoSelectionStrategyImpl.java +++ /dev/null @@ -1,138 +0,0 @@ -package com.bloxbean.cardano.hdwallet; - -import com.bloxbean.cardano.client.api.UtxoSupplier; -import com.bloxbean.cardano.client.api.exception.ApiRuntimeException; -import com.bloxbean.cardano.client.api.exception.InsufficientBalanceException; -import com.bloxbean.cardano.client.api.model.Amount; -import com.bloxbean.cardano.client.api.model.Utxo; -import com.bloxbean.cardano.client.coinselection.UtxoSelectionStrategy; -import com.bloxbean.cardano.client.coinselection.exception.InputsLimitExceededException; -import com.bloxbean.cardano.client.plutus.spec.PlutusData; -import lombok.Setter; - -import java.math.BigInteger; -import java.util.*; -import java.util.stream.Collectors; - -/** - * Out-of-box implementation of {@link UtxoSelectionStrategy} - * Applications can provide their own custom implementation - */ -public class DefaultHDWalletUtxoSelectionStrategyImpl implements HDWalletUtxoSelectionStrategy{ - - private final UtxoSupplier utxoSupplier; - @Setter - private boolean ignoreUtxosWithDatumHash; - - public DefaultHDWalletUtxoSelectionStrategyImpl(UtxoSupplier utxoSupplier) { - this(utxoSupplier, true); - } - - public DefaultHDWalletUtxoSelectionStrategyImpl(UtxoSupplier utxoSupplier, boolean ignoreUtxosWithDatumHash) { - this.utxoSupplier = utxoSupplier; - this.ignoreUtxosWithDatumHash = ignoreUtxosWithDatumHash; - } - - public Set select(List inputUtxos, List outputAmounts, String datumHash, PlutusData inlineDatum, Set utxosToExclude, int maxUtxoSelectionLimit) { - if(outputAmounts == null || outputAmounts.isEmpty()){ - return Collections.emptySet(); - } - - //TODO -- Should we throw error if both datumHash and inlineDatum are set ?? - - try{ - // loop over utxo's, find matching requested amount - Set selectedUtxos = new HashSet<>(); - - final Map remaining = new HashMap<>(outputAmounts.stream() - .collect(Collectors.groupingBy(Amount::getUnit, - Collectors.reducing(BigInteger.ZERO, - Amount::getQuantity, - BigInteger::add)))) - .entrySet().stream() - .filter(entry -> BigInteger.ZERO.compareTo(entry.getValue()) < 0) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - - while(!remaining.isEmpty()){ - - - var sorted = inputUtxos != null - ? inputUtxos.stream() - .sorted(sortByMostMatchingAssets(outputAmounts)) - .collect(Collectors.toList()) - : Collections.emptyList(); - - for(Utxo utxo : sorted) { - if(!accept(utxo)){ - continue; - } - if(utxosToExclude != null && utxosToExclude.contains(utxo)){ - continue; - } - if(utxo.getDataHash() != null && !utxo.getDataHash().isEmpty() && ignoreUtxosWithDatumHash){ - continue; - } - if(datumHash != null && !datumHash.isEmpty() && !datumHash.equals(utxo.getDataHash())){ - continue; - } - //TODO - add tests for this scenario - if(inlineDatum != null && !inlineDatum.serializeToHex().equals(utxo.getInlineDatum())) { - continue; - } - if(selectedUtxos.contains(utxo)){ - continue; - } - List utxoAmounts = utxo.getAmount(); - - boolean utxoSelected = false; - for(Amount amount : utxoAmounts) { - var remainingAmount = remaining.get(amount.getUnit()); - if(remainingAmount != null && BigInteger.ZERO.compareTo(remainingAmount) < 0){ - utxoSelected = true; - var newRemaining = remainingAmount.subtract(amount.getQuantity()); - if(BigInteger.ZERO.compareTo(newRemaining) < 0){ - remaining.put(amount.getUnit(), newRemaining); - }else{ - remaining.remove(amount.getUnit()); - } - } - } - - if(utxoSelected){ - selectedUtxos.add(utxo); - if(!remaining.isEmpty() && selectedUtxos.size() > maxUtxoSelectionLimit){ - throw new InputsLimitExceededException("Selection limit of " + maxUtxoSelectionLimit + " utxos reached with " + remaining + " remaining"); - } - } - } - if(sorted.isEmpty()){ - break; - } - } - if(!remaining.isEmpty()){ - throw new InsufficientBalanceException("Not enough funds for [" + remaining + "], in Wallet"); - } - return selectedUtxos; - }catch(InputsLimitExceededException e){ - throw new ApiRuntimeException("Input limit exceeded and no fallback provided", e); - } - } - - private static Comparator sortByMostMatchingAssets(List outputAmounts){ - // first process utxos which contain most matching assets - return (o1, o2) -> Integer.compare(countMatchingAssets(o2.getAmount(), outputAmounts), countMatchingAssets(o1.getAmount(), outputAmounts)); - } - private static int countMatchingAssets(List l1, List outputAmounts){ - if(l1 == null || l1.isEmpty() || outputAmounts == null || outputAmounts.isEmpty()){ - return 0; - } - return (int) l1.stream() - .filter(it1 -> outputAmounts.stream().filter(outputAmount -> it1.getUnit() != null && it1.getUnit().equals(outputAmount.getUnit())).findFirst().isPresent()) - .map(it -> 1) - .count(); - } - - protected boolean accept(Utxo utxo) { - return true; - } -} diff --git a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/HDWallet.java b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/Wallet.java similarity index 58% rename from hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/HDWallet.java rename to hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/Wallet.java index 93938e46..d191e0a3 100644 --- a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/HDWallet.java +++ b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/Wallet.java @@ -4,10 +4,7 @@ import com.bloxbean.cardano.client.address.Address; 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.backend.api.BackendService; -import com.bloxbean.cardano.client.coinselection.impl.DefaultUtxoSelectionStrategyImpl; import com.bloxbean.cardano.client.common.MnemonicUtil; import com.bloxbean.cardano.client.common.model.Network; import com.bloxbean.cardano.client.common.model.Networks; @@ -19,17 +16,14 @@ import com.bloxbean.cardano.client.crypto.cip1852.DerivationPath; import com.bloxbean.cardano.client.transaction.spec.Transaction; import com.bloxbean.cardano.client.transaction.spec.TransactionInput; -import com.bloxbean.cardano.client.transaction.spec.TransactionOutput; -import com.bloxbean.cardano.client.transaction.spec.Value; import lombok.Getter; import lombok.Setter; -import lombok.SneakyThrows; import java.math.BigInteger; import java.util.*; import java.util.stream.Collectors; -public class HDWallet { +public class Wallet { @Setter @Getter @@ -38,54 +32,61 @@ public class HDWallet { private Network network; @Getter private String mnemonic; - private int startIndex = 0; @Setter - private UtxoSupplier utxoSupplier; - @Setter - HDWalletUtxoSelectionStrategy utxoSelectionStrategy; + @Getter + private WalletUtxoSupplier utxoSupplier; + private static final int INDEX_SEARCH_RANGE = 20; - public HDWallet(UtxoSupplier utxoSupplier) { + public Wallet(WalletUtxoSupplier utxoSupplier) { this(Networks.mainnet(), utxoSupplier); } - public HDWallet(Network network, UtxoSupplier utxoSupplier) { + public Wallet(Network network, WalletUtxoSupplier utxoSupplier) { this(network, Words.TWENTY_FOUR, utxoSupplier); } - public HDWallet(Network network, Words noOfWords, UtxoSupplier utxoSupplier) { + public Wallet(Network network, Words noOfWords, WalletUtxoSupplier utxoSupplier) { this(network, noOfWords, 0, utxoSupplier); } - public HDWallet(Network network, Words noOfWords, int account, UtxoSupplier utxoSupplier) { + public Wallet(Network network, Words noOfWords, int account, WalletUtxoSupplier utxoSupplier) { this.network = network; this.mnemonic = MnemonicUtil.generateNew(noOfWords); this.account = account; this.utxoSupplier = utxoSupplier; - this.utxoSelectionStrategy = new DefaultHDWalletUtxoSelectionStrategyImpl(utxoSupplier); - } - public HDWallet(String mnemonic, UtxoSupplier utxoSupplier) { + public Wallet(String mnemonic, WalletUtxoSupplier utxoSupplier) { this(Networks.mainnet(), mnemonic, utxoSupplier); } - public HDWallet(Network network, String mnemonic, UtxoSupplier utxoSupplier) { + public Wallet(Network network, String mnemonic, WalletUtxoSupplier utxoSupplier) { this(network,mnemonic, 0, utxoSupplier); } - public HDWallet(Network network, String mnemonic, int account, UtxoSupplier utxoSupplier) { + public Wallet(Network network, String mnemonic, int account, WalletUtxoSupplier utxoSupplier) { this.network = network; this.mnemonic = mnemonic; this.account = account; MnemonicUtil.validateMnemonic(this.mnemonic); this.utxoSupplier = utxoSupplier; - this.utxoSelectionStrategy = new DefaultHDWalletUtxoSelectionStrategyImpl(utxoSupplier); } + /** + * Get Enterpriseaddress for current account. Account can be changed via the setter. + * @param index + * @return + */ public Address getEntAddress(int index) { return getEntAddress(this.account, index); } + /** + * Get Enterpriseaddress for derivationpath m/1852'/1815'/{account}'/0/{index} + * @param account + * @param index + * @return + */ private Address getEntAddress(int account, int index) { DerivationPath derivationPath = DerivationPath.createExternalAddressDerivationPathForAccount(account); derivationPath.getIndex().setValue(index); @@ -93,16 +94,31 @@ private Address getEntAddress(int account, int index) { return new Account(this.network, this.mnemonic, derivationPath).getEnterpriseAddress(); } + /** + * Get Baseaddress for current account. Account can be changed via the setter. + * @param index + * @return + */ public Address getBaseAddress(int index) { return getBaseAddress(this.account, index); } + /** + * Get Baseaddress for derivationpath m/1852'/1815'/{account}'/0/{index} + * @param account + * @param index + * @return + */ public Address getBaseAddress(int account, int index) { DerivationPath derivationPath = DerivationPath.createExternalAddressDerivationPathForAccount(account); derivationPath.getIndex().setValue(index); return new Account(this.network, this.mnemonic, derivationPath).getBaseAddress(); } + /** + * Returns the RootkeyPair + * @return + */ public HdKeyPair getHDWalletKeyPair() { HdKeyGenerator hdKeyGenerator = new HdKeyGenerator(); HdKeyPair rootKeys; @@ -120,87 +136,79 @@ public HdKeyPair getHDWalletKeyPair() { } /** - * Scanning all accounts takes to much time for now + * TODO Renaming?? + * @param account + * @param index * @return */ - public List getUtxos() { - return getUtxos(this.account); - } - - @SneakyThrows - public List getUtxos(int account) { - List utxos = new ArrayList<>(); - int index = this.startIndex; - int noUtxoFound = 0; - while(noUtxoFound < 20) { - List utxoFromIndex = getUtxos(account, index); - utxos.addAll(utxoFromIndex); - noUtxoFound = utxoFromIndex.isEmpty()? noUtxoFound + 1 : 0; - - index++; // increasing search index - } - return utxos; - } - - @SneakyThrows - public List getUtxos(int account, int index) { - Address address = getBaseAddress(account, index); - return utxoSupplier.getAll(address.getAddress()); - } - public Account getSigner(int account, int index) { DerivationPath derivationPath = DerivationPath.createDRepKeyDerivationPathForAccount(account); derivationPath.getIndex().setValue(index); return new Account(this.network, this.mnemonic, derivationPath); } + /** + * TODO Renaming?? + * @param index + * @return + */ public Account getSigner(int index) { -// return getSigner(this.account, index); return new Account(this.network, this.mnemonic, index); } - // Pretty stupid bruteforce approach for iteration 1 - public Transaction sign(Transaction outputTxn) { - List signers = getSignersForInputs(outputTxn.getBody().getInputs()); + /** + * Finds needed signers within wallet and signs the transaction with each one + * @param txToSign + * @return signed Transaction + */ + public Transaction sign(Transaction txToSign) { + List signers = getSignersForTransaction(txToSign); + if(signers.isEmpty()) throw new RuntimeException("No signers found!"); - Transaction signed = outputTxn; - for (Account signer : signers) { - signed = signer.sign(signed); - } - return signed; + + for (Account signer : signers) txToSign = signer.sign(txToSign); + + return txToSign; } + /** + * Returns a list with signers needed for this transaction + * @param tx + * @return + */ + public List getSignersForTransaction(Transaction tx) { + return getSignersForInputs(tx.getBody().getInputs()); + } private List getSignersForInputs(List inputs) { // searching for address to sign List signers = new ArrayList<>(); List remaining = new ArrayList<>(inputs); + int index = 0; int emptyCounter = 0; - while (!remaining.isEmpty() || emptyCounter >= 20) { - List utxos = getUtxos(this.account, index); - if(utxos.isEmpty()) { - emptyCounter++; - } else { - emptyCounter = 0; - } + while (!remaining.isEmpty() || emptyCounter >= INDEX_SEARCH_RANGE) { + List utxos = utxoSupplier.getUtxosForAccountAndIndex(this, this.account, index); + emptyCounter = utxos.isEmpty() ? emptyCounter + 1 : 0; + for (Utxo utxo : utxos) { - for (TransactionInput input : inputs) { - if(utxo.getTxHash().equals(input.getTransactionId())) { - signers.add(getSigner(index)); - remaining.remove(input); - } - } - if(remaining.isEmpty()) + if(matchUtxoWithInputs(inputs, utxo, signers, index, remaining)) break; } index++; } return signers; } - public List getUtxosForOutputs(List outputs) { - List allUtxosFromWallet = getUtxos(); - return new ArrayList(utxoSelectionStrategy.select(allUtxosFromWallet, outputs, null, null, null, Integer.MAX_VALUE)); + + private boolean matchUtxoWithInputs(List inputs, Utxo utxo, List signers, int index, List remaining) { + for (TransactionInput input : inputs) { + if(utxo.getTxHash().equals(input.getTransactionId()) && utxo.getOutputIndex() == input.getIndex()) { + signers.add(getSigner(index)); + remaining.remove(input); + } + } + return remaining.isEmpty(); } + } diff --git a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/HDWalletInterface.java b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/WalletInterface.java similarity index 97% rename from hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/HDWalletInterface.java rename to hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/WalletInterface.java index 0fd45020..19a38e83 100644 --- a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/HDWalletInterface.java +++ b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/WalletInterface.java @@ -5,7 +5,7 @@ import java.util.List; -public interface HDWalletInterface { +public interface WalletInterface { // private String mnemonic = ""; // public HDWalletInterface(Network network, String mnemonic); diff --git a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/WalletUtxoSupplier.java b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/WalletUtxoSupplier.java new file mode 100644 index 00000000..d8a42095 --- /dev/null +++ b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/WalletUtxoSupplier.java @@ -0,0 +1,105 @@ +package com.bloxbean.cardano.hdwallet; + +import com.bloxbean.cardano.client.address.Address; +import com.bloxbean.cardano.client.api.UtxoSupplier; +import com.bloxbean.cardano.client.api.common.OrderEnum; +import com.bloxbean.cardano.client.api.exception.ApiException; +import com.bloxbean.cardano.client.api.exception.ApiRuntimeException; +import com.bloxbean.cardano.client.api.model.Result; +import com.bloxbean.cardano.client.api.model.Utxo; +import com.bloxbean.cardano.client.backend.api.UtxoService; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +public class WalletUtxoSupplier implements UtxoSupplier { + + private final UtxoService utxoService; + @Setter + private Wallet wallet; + private static final int INDEX_SEARCH_RANGE = 20; // according to specifications + + public WalletUtxoSupplier(UtxoService utxoService, Wallet wallet) { + this.utxoService = utxoService; + this.wallet = wallet; + } + + public WalletUtxoSupplier(UtxoService utxoService) { + this.utxoService = utxoService; + } + + @Override + public List getPage(String address, Integer nrOfItems, Integer page, OrderEnum order) { + return getAll(address); // todo get Page of utxo over multipe addresses - find a good way to aktually do something with page, nrOfItems and order + } + + @Override + public Optional getTxOutput(String txHash, int outputIndex) { + try { + var result = utxoService.getTxOutput(txHash, outputIndex); + return result != null && result.getValue() != null ? Optional.of(result.getValue()) : Optional.empty(); + } catch (ApiException e) { + throw new ApiRuntimeException(e); + } + } + + @Override + public List getAll(String address) { + checkIfWalletIsSet(); + return getAll(wallet); + } + + public List getAll(Wallet wallet) { + List utxos = new ArrayList<>(); + int index = 0; + int noUtxoFound = 0; + while(noUtxoFound < INDEX_SEARCH_RANGE) { + List utxoFromIndex = getUtxosForAccountAndIndex(wallet.getAccount(), index); + utxos.addAll(utxoFromIndex); + noUtxoFound = utxoFromIndex.isEmpty() ? noUtxoFound + 1 : 0; + + index++; // increasing search index + } + return utxos; + } + + private void checkIfWalletIsSet() { + if(this.wallet == null) + throw new RuntimeException("Wallet has to be provided!"); + } + + /** + * Returns all UTXOs for a specific address m/1852'/1815'/{account}'/0/{index} + * @param account + * @param index + * @return + */ + public List getUtxosForAccountAndIndex(int account, int index) { + checkIfWalletIsSet(); + return getUtxosForAccountAndIndex(this.wallet, account, index); + } + + public List getUtxosForAccountAndIndex(Wallet wallet, int account, int index) { + String address = wallet.getBaseAddress(account, index).getAddress(); + List utxos = new ArrayList<>(); + int page = 1; + while(true) { + Result> result = null; + try { + result = utxoService.getUtxos(address, UtxoSupplier.DEFAULT_NR_OF_ITEMS_TO_FETCH, page, OrderEnum.asc); + } catch (ApiException e) { + throw new ApiRuntimeException(e); + } + List utxoPage = result != null && result.getValue() != null ? result.getValue() : Collections.emptyList(); + utxos.addAll(utxoPage); + if(utxoPage.size() < 100) + break; + page++; + } + + return utxos; + } +} diff --git a/hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/HDWalletTest.java b/hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/WalletTest.java similarity index 75% rename from hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/HDWalletTest.java rename to hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/WalletTest.java index 171961a7..fe5101ab 100644 --- a/hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/HDWalletTest.java +++ b/hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/WalletTest.java @@ -2,22 +2,23 @@ import com.bloxbean.cardano.client.account.Account; import com.bloxbean.cardano.client.address.Address; +import com.bloxbean.cardano.client.common.model.Networks; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; -public class HDWalletTest { +public class WalletTest { @Test void generateMnemonicTest() { - HDWallet hdWallet = new HDWallet(null); + Wallet hdWallet = new Wallet(Networks.testnet(), null); String mnemonic = hdWallet.getMnemonic(); assertEquals(24, mnemonic.split(" ").length); } @Test void getAccountFromIndex() { - HDWallet hdWallet = new HDWallet(null); + Wallet hdWallet = new Wallet(Networks.testnet(), null); Address address = hdWallet.getBaseAddress(0); Account a = new Account(hdWallet.getNetwork(), hdWallet.getMnemonic(), 0); assertEquals(address.getAddress(), a.getBaseAddress().getAddress()); diff --git a/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/AbstractTx.java b/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/AbstractTx.java index 9cad8875..a4b3b0c6 100644 --- a/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/AbstractTx.java +++ b/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/AbstractTx.java @@ -18,14 +18,13 @@ import com.bloxbean.cardano.client.transaction.spec.*; import com.bloxbean.cardano.client.util.HexUtil; import com.bloxbean.cardano.client.util.Tuple; -import com.bloxbean.cardano.hdwallet.HDWallet; +import com.bloxbean.cardano.hdwallet.Wallet; import lombok.NonNull; import lombok.SneakyThrows; import java.math.BigInteger; import java.util.ArrayList; import java.util.List; -import java.util.Set; import static com.bloxbean.cardano.client.common.CardanoConstants.LOVELACE; @@ -372,7 +371,7 @@ protected void addToMultiAssetList(@NonNull Script script, List assets) { */ protected abstract String getFromAddress(); - protected abstract HDWallet getFromWallet(); + protected abstract Wallet getFromWallet(); /** * Perform post balanceTx action diff --git a/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/QuickTxBuilder.java b/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/QuickTxBuilder.java index f6da46e2..f08a890f 100644 --- a/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/QuickTxBuilder.java +++ b/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/QuickTxBuilder.java @@ -94,6 +94,12 @@ public QuickTxBuilder(BackendService backendService) { ); } + public QuickTxBuilder(BackendService backendService, UtxoSupplier utxoSupplier) { + this(utxoSupplier, + new DefaultProtocolParamsSupplier(backendService.getEpochService()), + new DefaultTransactionProcessor(backendService.getTransactionService())); + } + /** * Create TxContext for the given txs * diff --git a/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/ScriptTx.java b/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/ScriptTx.java index afa6af55..6d5daa0d 100644 --- a/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/ScriptTx.java +++ b/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/ScriptTx.java @@ -11,7 +11,7 @@ import com.bloxbean.cardano.client.plutus.spec.*; import com.bloxbean.cardano.client.transaction.spec.*; import com.bloxbean.cardano.client.util.Tuple; -import com.bloxbean.cardano.hdwallet.HDWallet; +import com.bloxbean.cardano.hdwallet.Wallet; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NonNull; @@ -504,7 +504,7 @@ protected String getFromAddress() { } @Override - protected HDWallet getFromWallet() { + protected Wallet getFromWallet() { return null; } diff --git a/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/Tx.java b/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/Tx.java index e1bed734..85fba29c 100644 --- a/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/Tx.java +++ b/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/Tx.java @@ -19,11 +19,10 @@ import com.bloxbean.cardano.client.transaction.spec.governance.actions.GovActionId; import com.bloxbean.cardano.client.transaction.spec.script.NativeScript; import com.bloxbean.cardano.client.util.Tuple; -import com.bloxbean.cardano.hdwallet.HDWallet; +import com.bloxbean.cardano.hdwallet.Wallet; import lombok.NonNull; import java.math.BigInteger; -import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -34,7 +33,7 @@ public class Tx extends AbstractTx { private String sender; protected boolean senderAdded = false; - private HDWallet senderWallet; + private Wallet senderWallet; /** * Create Tx @@ -124,10 +123,11 @@ public Tx from(String sender) { return this; } - public Tx from(HDWallet sender) { + public Tx from(Wallet sender) { verifySenderNotExists(); - this.inputUtxos = sender.getUtxosForOutputs(amounts); - this.sender = this.inputUtxos.get(0).getAddress(); // TODO - is it clever to use the first address as sender here? + this.senderWallet = sender; + // TODO sender is not used in this scenarios, but it must be set to avoid breaking other things. + this.sender = this.senderWallet.getBaseAddress(0).getAddress(); // TODO - is it clever to use the first address as sender here? this.changeAddress = this.sender; this.senderAdded = true; return this; @@ -493,7 +493,7 @@ protected String getFromAddress() { } @Override - protected HDWallet getFromWallet() { + protected Wallet getFromWallet() { if(senderWallet != null) return senderWallet; else From dcd184fe706729c5d2f2a1e8b1ce19cb85e2ba47 Mon Sep 17 00:00:00 2001 From: Kammerlo Date: Fri, 19 Jan 2024 08:18:10 +0100 Subject: [PATCH 05/16] #213 added stakekey registrations and address caching to wallet --- .../function/helper/SignerProviders.java | 9 ++ hd-wallet/build.gradle | 3 + ...{QuickTXBaseIT.java => QuickTxBaseIT.java} | 4 +- .../HDWalletUtxoSelectionStrategy.java | 13 --- .../com/bloxbean/cardano/hdwallet/Wallet.java | 104 +++++++++++++----- .../bloxbean/cardano/client/quicktx/Tx.java | 15 +++ 6 files changed, 106 insertions(+), 42 deletions(-) rename hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/{QuickTXBaseIT.java => QuickTxBaseIT.java} (97%) delete mode 100644 hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/HDWalletUtxoSelectionStrategy.java diff --git a/function/src/main/java/com/bloxbean/cardano/client/function/helper/SignerProviders.java b/function/src/main/java/com/bloxbean/cardano/client/function/helper/SignerProviders.java index c3554e8d..dec76771 100644 --- a/function/src/main/java/com/bloxbean/cardano/client/function/helper/SignerProviders.java +++ b/function/src/main/java/com/bloxbean/cardano/client/function/helper/SignerProviders.java @@ -119,4 +119,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; + }; + } } diff --git a/hd-wallet/build.gradle b/hd-wallet/build.gradle index c9e12301..147d692c 100644 --- a/hd-wallet/build.gradle +++ b/hd-wallet/build.gradle @@ -6,10 +6,13 @@ dependencies { 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') } diff --git a/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTXBaseIT.java b/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTxBaseIT.java similarity index 97% rename from hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTXBaseIT.java rename to hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTxBaseIT.java index 95477cf7..df6fe250 100644 --- a/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTXBaseIT.java +++ b/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTxBaseIT.java @@ -14,12 +14,12 @@ import java.util.List; import java.util.Optional; -public class QuickTXBaseIT { +public class QuickTxBaseIT { protected String BLOCKFROST = "blockfrost"; protected String KOIOS = "koios"; protected String DEVKIT = "devkit"; - protected String backendType = BLOCKFROST; + protected String backendType = DEVKIT; public BackendService getBackendService() { if (BLOCKFROST.equals(backendType)) { diff --git a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/HDWalletUtxoSelectionStrategy.java b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/HDWalletUtxoSelectionStrategy.java deleted file mode 100644 index 1b740556..00000000 --- a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/HDWalletUtxoSelectionStrategy.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.bloxbean.cardano.hdwallet; - -import com.bloxbean.cardano.client.api.model.Amount; -import com.bloxbean.cardano.client.api.model.Utxo; -import com.bloxbean.cardano.client.plutus.spec.PlutusData; - -import java.util.List; -import java.util.Set; - -public interface HDWalletUtxoSelectionStrategy { - - Set select(List inputUtxos, List outputAmounts, String datumHash, PlutusData inlineDatum, Set utxosToExclude, int maxUtxoSelectionLimit); -} diff --git a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/Wallet.java b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/Wallet.java index d191e0a3..b239fcb7 100644 --- a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/Wallet.java +++ b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/Wallet.java @@ -2,8 +2,7 @@ import com.bloxbean.cardano.client.account.Account; import com.bloxbean.cardano.client.address.Address; -import com.bloxbean.cardano.client.api.UtxoSupplier; -import com.bloxbean.cardano.client.api.model.Amount; +import com.bloxbean.cardano.client.address.AddressProvider; import com.bloxbean.cardano.client.api.model.Utxo; import com.bloxbean.cardano.client.common.MnemonicUtil; import com.bloxbean.cardano.client.common.model.Network; @@ -13,19 +12,17 @@ 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.transaction.TransactionSigner; import com.bloxbean.cardano.client.transaction.spec.Transaction; import com.bloxbean.cardano.client.transaction.spec.TransactionInput; import lombok.Getter; import lombok.Setter; - -import java.math.BigInteger; import java.util.*; -import java.util.stream.Collectors; public class Wallet { - @Setter @Getter private int account = 0; @Getter @@ -36,6 +33,10 @@ public class Wallet { @Getter private WalletUtxoSupplier utxoSupplier; private static final int INDEX_SEARCH_RANGE = 20; + private String stakeAddress; + private Map cache; + private HdKeyPair rootKeys; + private HdKeyPair stakeKeys; public Wallet(WalletUtxoSupplier utxoSupplier) { this(Networks.mainnet(), utxoSupplier); @@ -54,6 +55,7 @@ public Wallet(Network network, Words noOfWords, int account, WalletUtxoSupplier this.mnemonic = MnemonicUtil.generateNew(noOfWords); this.account = account; this.utxoSupplier = utxoSupplier; + cache = new HashMap<>(); } public Wallet(String mnemonic, WalletUtxoSupplier utxoSupplier) { @@ -70,6 +72,7 @@ public Wallet(Network network, String mnemonic, int account, WalletUtxoSupplier this.account = account; MnemonicUtil.validateMnemonic(this.mnemonic); this.utxoSupplier = utxoSupplier; + cache = new HashMap<>(); } /** @@ -88,10 +91,29 @@ public Address getEntAddress(int index) { * @return */ private Address getEntAddress(int account, int index) { - DerivationPath derivationPath = DerivationPath.createExternalAddressDerivationPathForAccount(account); - derivationPath.getIndex().setValue(index); + return getAccountObjectFromCache(account, index).getEnterpriseAddress(); + } + + private Account getAccountObjectFromCache(int account, int index) { + if(account != this.account) { + DerivationPath derivationPath = DerivationPath.createExternalAddressDerivationPathForAccount(account); + derivationPath.getIndex().setValue(index); + return new Account(this.network, this.mnemonic, derivationPath); + } else { + if(cache.containsKey(index)) { + return cache.get(index); + } else { + Account acc = new Account(this.network, this.mnemonic, index); + cache.put(index, acc); + return acc; + } + } + } - return new Account(this.network, this.mnemonic, derivationPath).getEnterpriseAddress(); + public void setAccount(int account) { + this.account = account; + // invalidating cache since it is only held for an account + cache = new HashMap<>(); } /** @@ -103,6 +125,10 @@ public Address getBaseAddress(int index) { return getBaseAddress(this.account, index); } + public String getBaseAddressString(int index) { + return getBaseAddress(index).getAddress(); + } + /** * Get Baseaddress for derivationpath m/1852'/1815'/{account}'/0/{index} * @param account @@ -110,9 +136,7 @@ public Address getBaseAddress(int index) { * @return */ public Address getBaseAddress(int account, int index) { - DerivationPath derivationPath = DerivationPath.createExternalAddressDerivationPathForAccount(account); - derivationPath.getIndex().setValue(index); - return new Account(this.network, this.mnemonic, derivationPath).getBaseAddress(); + return getAccountObjectFromCache(account,index).getBaseAddress(); } /** @@ -120,17 +144,18 @@ public Address getBaseAddress(int account, int index) { * @return */ public HdKeyPair getHDWalletKeyPair() { - HdKeyGenerator hdKeyGenerator = new HdKeyGenerator(); - HdKeyPair rootKeys; - try { - byte[] entropy = MnemonicCode.INSTANCE.toEntropy(this.mnemonic); - rootKeys = hdKeyGenerator.getRootKeyPairFromEntropy(entropy); - } catch (MnemonicException.MnemonicLengthException e) { - throw new RuntimeException(e); - } catch (MnemonicException.MnemonicWordException e) { - throw new RuntimeException(e); - } catch (MnemonicException.MnemonicChecksumException e) { - throw new RuntimeException(e); + if(rootKeys == null) { + HdKeyGenerator hdKeyGenerator = new HdKeyGenerator(); + try { + byte[] entropy = MnemonicCode.INSTANCE.toEntropy(this.mnemonic); + rootKeys = hdKeyGenerator.getRootKeyPairFromEntropy(entropy); + } catch (MnemonicException.MnemonicLengthException e) { + throw new RuntimeException(e); + } catch (MnemonicException.MnemonicWordException e) { + throw new RuntimeException(e); + } catch (MnemonicException.MnemonicChecksumException e) { + throw new RuntimeException(e); + } } return rootKeys; } @@ -142,9 +167,7 @@ public HdKeyPair getHDWalletKeyPair() { * @return */ public Account getSigner(int account, int index) { - DerivationPath derivationPath = DerivationPath.createDRepKeyDerivationPathForAccount(account); - derivationPath.getIndex().setValue(index); - return new Account(this.network, this.mnemonic, derivationPath); + return getAccountObjectFromCache(account, index); } /** @@ -153,7 +176,7 @@ public Account getSigner(int account, int index) { * @return */ public Account getSigner(int index) { - return new Account(this.network, this.mnemonic, index); + return getAccountObjectFromCache(this.account, index); } /** @@ -211,4 +234,31 @@ private boolean matchUtxoWithInputs(List inputs, Utxo utxo, Li return remaining.isEmpty(); } + public String getStakeAddress() { + if (stakeAddress == null || stakeAddress.isEmpty()) { + HdKeyPair stakeKeyPair = getStakeKeyPair(); + Address address = AddressProvider.getRewardAddress(stakeKeyPair.getPublicKey(), network); + stakeAddress = address.toBech32(); + } + return stakeAddress; + } + + public Transaction signWithStakeKey(Transaction transaction) { + return TransactionSigner.INSTANCE.sign(transaction, getStakeKeyPair()); + } + + private HdKeyPair getStakeKeyPair() { + if(stakeKeys == null) { + DerivationPath stakeDerivationPath = DerivationPath.createStakeAddressDerivationPathForAccount(this.account); +// if (mnemonic == null || mnemonic.trim().length() == 0) { +// hdKeyPair = new CIP1852().getKeyPairFromAccountKey(this.accountKey, stakeDerivationPath); // TODO need to implement creation from key +// } else { +// hdKeyPair = new CIP1852().getKeyPairFromMnemonic(mnemonic, stakeDerivationPath); +// } + stakeKeys = new CIP1852().getKeyPairFromMnemonic(mnemonic, stakeDerivationPath); + } + return stakeKeys; + } + + } diff --git a/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/Tx.java b/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/Tx.java index 85fba29c..985583a7 100644 --- a/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/Tx.java +++ b/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/Tx.java @@ -163,6 +163,11 @@ public Tx registerStakeAddress(@NonNull String address) { return this; } + public Tx registerStakeAddress(@NonNull Wallet wallet) { + stakeTx.registerStakeAddress(new Address(wallet.getStakeAddress())); + return this; + } + /** * Register stake address * @param address address to register. Address should have delegation credential. So it should be a base address or stake address. @@ -193,6 +198,11 @@ public Tx deregisterStakeAddress(@NonNull Address address) { return this; } + public Tx deregisterStakeAddress(@NonNull Wallet wallet) { + stakeTx.deregisterStakeAddress(new Address(wallet.getStakeAddress()), null, null); + return this; + } + /** * De-register stake address. The key deposit will be refunded to the refund address. * @param address address to de-register. Address should have delegation credential. So it should be a base address or stake address. @@ -226,6 +236,11 @@ public Tx delegateTo(@NonNull String address, @NonNull String poolId) { return this; } + public Tx delegateTo(@NonNull Wallet wallet, @NonNull String poolId) { + stakeTx.delegateTo(new Address(wallet.getStakeAddress()), poolId, null); + return this; + } + /** * Delegate stake address to a stake pool * @param address address to delegate. Address should have delegation credential. So it should be a base address or stake address. From 596488ab17cd2329e03f4615e87100ea6fb2e35b Mon Sep 17 00:00:00 2001 From: Kammerlo Date: Fri, 19 Jan 2024 11:25:49 +0100 Subject: [PATCH 06/16] #213 added Tests, WalletUtxoSupplier interface and DefaultWalletUtxoSupplier.java --- .../cardano/hdwallet/QuickTxBuilderIT.java | 64 ++-- .../bloxbean/cardano/hdwallet/StakeTxIT.java | 311 ++++++++++++++++++ .../com/bloxbean/cardano/hdwallet/Wallet.java | 119 ++++--- .../DefaultWalletUtxoSupplier.java} | 44 ++- .../utxosupplier/WalletUtxoSupplier.java | 34 ++ .../bloxbean/cardano/hdwallet/WalletTest.java | 86 ++++- .../client/quicktx/QuickTxBuilder.java | 5 + 7 files changed, 547 insertions(+), 116 deletions(-) create mode 100644 hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/StakeTxIT.java rename hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/{WalletUtxoSupplier.java => utxosupplier/DefaultWalletUtxoSupplier.java} (87%) create mode 100644 hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/utxosupplier/WalletUtxoSupplier.java diff --git a/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTxBuilderIT.java b/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTxBuilderIT.java index d0108e46..bccd7e28 100644 --- a/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTxBuilderIT.java +++ b/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTxBuilderIT.java @@ -4,14 +4,19 @@ 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.coinselection.impl.DefaultUtxoSelectionStrategyImpl; +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.utxosupplier.DefaultWalletUtxoSupplier; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -22,11 +27,11 @@ import static org.junit.jupiter.api.Assertions.assertTrue; -public class QuickTxBuilderIT extends QuickTXBaseIT { +public class QuickTxBuilderIT extends QuickTxBaseIT { BackendService backendService; UtxoSupplier utxoSupplier; - WalletUtxoSupplier walletUtxoSupplier; + DefaultWalletUtxoSupplier walletUtxoSupplier; Wallet wallet1; Wallet wallet2; @@ -34,7 +39,8 @@ public class QuickTxBuilderIT extends QuickTXBaseIT { void setup() { backendService = getBackendService(); utxoSupplier = getUTXOSupplier(); - walletUtxoSupplier = new WalletUtxoSupplier(backendService.getUtxoService()); + + walletUtxoSupplier = new DefaultWalletUtxoSupplier(backendService.getUtxoService()); 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, walletUtxoSupplier); 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"; @@ -48,11 +54,11 @@ void simplePayment() { metadata.putNegative(200, -900); - UtxoSupplier walletUtxoSupplier = new WalletUtxoSupplier(backendService.getUtxoService(), wallet1); + 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)) + .payToAddress(wallet2.getBaseAddress(0).getAddress(), Amount.ada(5)) .from(wallet1); Result result = quickTxBuilder.compose(tx) @@ -67,50 +73,28 @@ void simplePayment() { } @Test - void simplePayment2() { - Metadata metadata = MetadataBuilder.createMetadata(); - metadata.put(BigInteger.valueOf(100), "This is first metadata"); - metadata.putNegative(200, -900); + void minting() throws CborSerializationException { + Policy policy = PolicyUtil.createMultiSigScriptAtLeastPolicy("test_policy", 1, 1); + String assetName = "MyAsset"; + BigInteger qty = BigInteger.valueOf(1000); - QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService); Tx tx = new Tx() - .payToAddress(wallet2.getBaseAddress(0).getAddress(), Amount.ada(3)) - - .from(wallet1.getBaseAddress(0).getAddress()); // TODO - Set a HDWallet here + .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 result = quickTxBuilder.compose(tx) - .withSigner(SignerProviders.signerFrom(wallet1.getSigner(0))) + .withSigner(SignerProviders.signerFrom(wallet1)) + .withSigner(SignerProviders.signerFrom(policy)) .complete(); System.out.println(result); assertTrue(result.isSuccessful()); waitForTransaction(result); - checkIfUtxoAvailable(result.getValue(), wallet2.getBaseAddress(0).getAddress()); - } - - @Test - void simplePayment3() { - Metadata metadata = MetadataBuilder.createMetadata(); - metadata.put(BigInteger.valueOf(100), "This is first metadata"); - metadata.putNegative(200, -900); - - QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService); - Tx tx = new Tx() - .payToAddress(wallet1.getBaseAddress(0).getAddress(), Amount.ada(7)) - .from(wallet2); // TODO - Set a HDWallet here - QuickTxBuilder.TxContext compose = quickTxBuilder.compose(tx); - - compose = compose.withSigner(SignerProviders.signerFrom(wallet2.getSigner(0))); - compose = compose.withSigner(SignerProviders.signerFrom(wallet2.getSigner(1))); - - Result result = compose.complete(); - - System.out.println(result); - assertTrue(result.isSuccessful()); - waitForTransaction(result); - - checkIfUtxoAvailable(result.getValue(), wallet2.getBaseAddress(0).getAddress()); + checkIfUtxoAvailable(result.getValue(), wallet1.getBaseAddress(0).getAddress()); } @Test diff --git a/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/StakeTxIT.java b/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/StakeTxIT.java new file mode 100644 index 00000000..38ad13f6 --- /dev/null +++ b/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/StakeTxIT.java @@ -0,0 +1,311 @@ +package com.bloxbean.cardano.hdwallet; + +import com.bloxbean.cardano.aiken.AikenTransactionEvaluator; +import com.bloxbean.cardano.client.address.Address; +import com.bloxbean.cardano.client.address.AddressProvider; +import com.bloxbean.cardano.client.api.ProtocolParamsSupplier; +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.backend.api.BackendService; +import com.bloxbean.cardano.client.backend.api.DefaultProtocolParamsSupplier; +import com.bloxbean.cardano.client.cip.cip20.MessageMetadata; +import com.bloxbean.cardano.client.common.model.Networks; +import com.bloxbean.cardano.client.function.helper.SignerProviders; +import com.bloxbean.cardano.client.plutus.blueprint.PlutusBlueprintUtil; +import com.bloxbean.cardano.client.plutus.blueprint.model.PlutusVersion; +import com.bloxbean.cardano.client.plutus.spec.BigIntPlutusData; +import com.bloxbean.cardano.client.plutus.spec.PlutusScript; +import com.bloxbean.cardano.client.quicktx.QuickTxBuilder; +import com.bloxbean.cardano.client.quicktx.ScriptTx; +import com.bloxbean.cardano.client.quicktx.Tx; +import com.bloxbean.cardano.client.util.JsonUtil; +import com.bloxbean.cardano.hdwallet.utxosupplier.DefaultWalletUtxoSupplier; +import com.bloxbean.cardano.hdwallet.utxosupplier.WalletUtxoSupplier; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.*; + +import static com.bloxbean.cardano.client.common.ADAConversionUtil.adaToLovelace; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class StakeTxIT extends QuickTxBaseIT { + BackendService backendService; + UtxoSupplier utxoSupplier; + WalletUtxoSupplier walletUtxoSupplier; + Wallet wallet1; + Wallet wallet2; + + String poolId; + ProtocolParamsSupplier protocolParamsSupplier; + + String aikenCompiledCode1 = "581801000032223253330043370e00290010a4c2c6eb40095cd1"; //redeemer = 1 + PlutusScript plutusScript1 = PlutusBlueprintUtil.getPlutusScriptFromCompiledCode(aikenCompiledCode1, PlutusVersion.v2); + + String aikenCompileCode2 = "581801000032223253330043370e00290020a4c2c6eb40095cd1"; //redeemer = 2 + PlutusScript plutusScript2 = PlutusBlueprintUtil.getPlutusScriptFromCompiledCode(aikenCompileCode2, PlutusVersion.v2); + + String scriptStakeAddress1 = AddressProvider.getRewardAddress(plutusScript1, Networks.testnet()).toBech32(); + String scriptStakeAddress2 = AddressProvider.getRewardAddress(plutusScript2, Networks.testnet()).toBech32(); + + + QuickTxBuilder quickTxBuilder; + ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeEach + void setup() { + backendService = getBackendService(); + utxoSupplier = getUTXOSupplier(); + + protocolParamsSupplier = new DefaultProtocolParamsSupplier(backendService.getEpochService()); + quickTxBuilder = new QuickTxBuilder(backendService); + + walletUtxoSupplier = new DefaultWalletUtxoSupplier(backendService.getUtxoService()); + 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, walletUtxoSupplier); + 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, walletUtxoSupplier); + + if (backendType.equals(DEVKIT)) { + poolId = "pool1wvqhvyrgwch4jq9aa84hc8q4kzvyq2z3xr6mpafkqmx9wce39zy"; + } else { + poolId = "pool1vqq4hdwrh442u97e2jh6k4xuscs3x5mqjjrn8daj36y7gt2rj85"; + } + } + + @Test + @Order(1) + void stakeAddressRegistration() { + //De-register all stake addresses if required + _deRegisterStakeKeys(); + + UtxoSupplier walletUtxoSupplier = new DefaultWalletUtxoSupplier(backendService.getUtxoService(), wallet1); + QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService, walletUtxoSupplier); + Tx tx = new Tx() + .payToAddress(wallet1.getBaseAddressString(1), Amount.ada(1.5)) + .payToAddress(wallet2.getBaseAddressString(1), Amount.ada(2.5)) + .payToAddress(wallet1.getBaseAddressString(0), Amount.ada(4.3)) + .registerStakeAddress(wallet2) + .registerStakeAddress(wallet1) + .attachMetadata(MessageMetadata.create().add("This is a stake registration tx")) + .from(wallet1); + + Result result = quickTxBuilder.compose(tx) + .withSigner(SignerProviders.signerFrom(wallet1)) + .withTxInspector((txn) -> System.out.println(JsonUtil.getPrettyJson(txn))) + .completeAndWait(msg -> System.out.println(msg)); + + System.out.println(result); + assertTrue(result.isSuccessful()); + + checkIfUtxoAvailable(result.getValue(), wallet1.getBaseAddressString(1)); + } + + @Test + @Order(2) + void stakeAddressDeRegistration() { + UtxoSupplier walletUtxoSupplier = new DefaultWalletUtxoSupplier(backendService.getUtxoService(), wallet1); // TODO WalletUTXOSupplier only works with one wallet - Is it a problem? + QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService, walletUtxoSupplier); + Tx tx = new Tx() + .payToAddress(wallet1.getBaseAddressString(1), Amount.ada(1.5)) + .payToAddress(wallet1.getBaseAddressString(0), Amount.ada(4.0)) + .deregisterStakeAddress(wallet1) + .attachMetadata(MessageMetadata.create().add("This is a stake registration tx")) + .from(wallet1); + + Result result = quickTxBuilder.compose(tx) + .withSigner(SignerProviders.signerFrom(wallet1)) + .withSigner(SignerProviders.stakeKeySignerFrom(wallet1)) + .withTxInspector((txn) -> System.out.println(JsonUtil.getPrettyJson(txn))) + .completeAndWait(msg -> System.out.println(msg)); + + System.out.println(result); + assertTrue(result.isSuccessful()); + + checkIfUtxoAvailable(result.getValue(), wallet1.getBaseAddressString(1)); + } + + @Test + @Order(3) + void stakeAddressRegistration_onlyRegistration() { + UtxoSupplier walletUtxoSupplier = new DefaultWalletUtxoSupplier(backendService.getUtxoService(), wallet1); + QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService, walletUtxoSupplier); + Tx tx = new Tx() + .registerStakeAddress(wallet1) + .attachMetadata(MessageMetadata.create().add("This is a stake registration tx")) + .from(wallet1); + + Result result = quickTxBuilder.compose(tx) + .withSigner(SignerProviders.signerFrom(wallet1)) + .withTxInspector((txn) -> System.out.println(JsonUtil.getPrettyJson(txn))) + .completeAndWait(msg -> System.out.println(msg)); + + System.out.println(result); + assertTrue(result.isSuccessful()); + + checkIfUtxoAvailable(result.getValue(), wallet1.getBaseAddressString(0)); + } + + @Test + @Order(4) + void stakeAddressDeRegistration_onlyRegistration() { + UtxoSupplier walletUtxoSupplier = new DefaultWalletUtxoSupplier(backendService.getUtxoService(), wallet1); + QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService, walletUtxoSupplier); + Tx tx = new Tx() + .deregisterStakeAddress(wallet1) + .attachMetadata(MessageMetadata.create().add("This is a stake deregistration tx")) + .from(wallet1); + + Result result = quickTxBuilder.compose(tx) + .withSigner(SignerProviders.signerFrom(wallet1)) + .withSigner(SignerProviders.stakeKeySignerFrom(wallet1)) + .withTxInspector((txn) -> System.out.println(JsonUtil.getPrettyJson(txn))) + .completeAndWait(msg -> System.out.println(msg)); + + System.out.println(result); + assertTrue(result.isSuccessful()); + + checkIfUtxoAvailable(result.getValue(), wallet1.getBaseAddressString(0)); + } + + @Test + @Order(5) + void scriptStakeAddress_registration() { +// deregisterScriptsStakeKeys(); + UtxoSupplier walletUtxoSupplier = new DefaultWalletUtxoSupplier(backendService.getUtxoService(), wallet1); + QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService, walletUtxoSupplier); + Tx tx = new Tx() + .registerStakeAddress(scriptStakeAddress1) + .attachMetadata(MessageMetadata.create().add("This is a script stake registration tx")) + .from(wallet1); + + Result result = quickTxBuilder.compose(tx) + .withSigner(SignerProviders.signerFrom(wallet1)) + .withTxInspector((txn) -> System.out.println(JsonUtil.getPrettyJson(txn))) + .completeAndWait(msg -> System.out.println(msg)); + + System.out.println(result); + assertTrue(result.isSuccessful()); + + checkIfUtxoAvailable(result.getValue(), wallet1.getBaseAddressString(0)); + } + + @Test + @Order(6) + void scriptStakeAddress_deRegistration() { + UtxoSupplier walletUtxoSupplier = new DefaultWalletUtxoSupplier(backendService.getUtxoService(), wallet1); + QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService, walletUtxoSupplier); + ScriptTx tx = new ScriptTx() + .deregisterStakeAddress(scriptStakeAddress1, BigIntPlutusData.of(1)) + .attachMetadata(MessageMetadata.create().add("This is a script stake address deregistration tx")) + .attachCertificateValidator(plutusScript1); + + Result result = quickTxBuilder.compose(tx) + .feePayer(wallet1.getBaseAddressString(0)) + .withSigner(SignerProviders.signerFrom(wallet1)) + .withTxInspector((txn) -> System.out.println(JsonUtil.getPrettyJson(txn))) + .withTxEvaluator(!backendType.equals(BLOCKFROST) ? + new AikenTransactionEvaluator(utxoSupplier, protocolParamsSupplier) : null) + .completeAndWait(msg -> System.out.println(msg)); + + System.out.println(result); + assertTrue(result.isSuccessful()); + + checkIfUtxoAvailable(result.getValue(), wallet1.getBaseAddressString(0)); + } + + @Test + @Order(9) + void stakeDelegation_scriptStakeKeys() { + registerScriptsStakeKeys(); + + UtxoSupplier walletUtxoSupplier = new DefaultWalletUtxoSupplier(backendService.getUtxoService(), wallet1); + QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService, walletUtxoSupplier); + + //Delegation + ScriptTx delegTx = new ScriptTx() + .delegateTo(new Address(scriptStakeAddress1), poolId, BigIntPlutusData.of(1)) + .attachMetadata(MessageMetadata.create().add("This is a delegation transaction")) + .attachCertificateValidator(plutusScript1); + + Result delgResult = quickTxBuilder.compose(delegTx) + .feePayer(wallet1.getBaseAddressString(0)) + .withSigner(SignerProviders.signerFrom(wallet1)) + .withTxEvaluator(!backendType.equals(BLOCKFROST) ? + new AikenTransactionEvaluator(utxoSupplier, protocolParamsSupplier) : null) + .withTxInspector((txn) -> System.out.println(JsonUtil.getPrettyJson(txn))) + .completeAndWait(msg -> System.out.println(msg)); + + System.out.println(delgResult); + assertTrue(delgResult.isSuccessful()); + + checkIfUtxoAvailable(delgResult.getValue(), wallet1.getBaseAddressString(0)); + + deregisterScriptsStakeKeys(); + } + + private void registerScriptsStakeKeys() { + UtxoSupplier walletUtxoSupplier = new DefaultWalletUtxoSupplier(backendService.getUtxoService(), wallet1); + QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService, walletUtxoSupplier); + + //stake Registration + Tx tx = new Tx() + .registerStakeAddress(scriptStakeAddress1) + .attachMetadata(MessageMetadata.create().add("This is a script stake registration tx")) + .from(wallet1); + + Result result = quickTxBuilder.compose(tx) + .withSigner(SignerProviders.signerFrom(wallet1)) + .withTxInspector((txn) -> System.out.println(JsonUtil.getPrettyJson(txn))) + .completeAndWait(msg -> System.out.println(msg)); + + if (result.isSuccessful()) + checkIfUtxoAvailable(result.getValue(), wallet1.getBaseAddressString(0)); + } + + private void deregisterScriptsStakeKeys() { + QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService); + + //stake Registration + ScriptTx tx = new ScriptTx() + .deregisterStakeAddress(scriptStakeAddress1, BigIntPlutusData.of(1)) + .attachCertificateValidator(plutusScript1); + + Result result = quickTxBuilder.compose(tx) + .feePayer(wallet1.getBaseAddressString(0)) + .withSigner(SignerProviders.signerFrom(wallet1)) + .withTxEvaluator(!backendType.equals(BLOCKFROST) ? + new AikenTransactionEvaluator(utxoSupplier, protocolParamsSupplier) : null) + .withTxInspector((txn) -> System.out.println(JsonUtil.getPrettyJson(txn))) + .completeAndWait(msg -> System.out.println(msg)); + + System.out.println(result); + assertTrue(result.isSuccessful()); + + checkIfUtxoAvailable(result.getValue(), wallet1.getBaseAddressString(0)); + } + + private Result _deRegisterStakeKeys() { + UtxoSupplier walletUtxoSupplier = new DefaultWalletUtxoSupplier(backendService.getUtxoService(), wallet1); + QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService, walletUtxoSupplier); + + //stake Registration + Tx tx = new Tx() + .deregisterStakeAddress(wallet1) + .deregisterStakeAddress(wallet2) + .from(wallet1); + + Result result = quickTxBuilder.compose(tx) + .withSigner(SignerProviders.signerFrom(wallet1)) + .withSigner(SignerProviders.stakeKeySignerFrom(wallet1)) + .withSigner(SignerProviders.stakeKeySignerFrom(wallet2)) + .withTxInspector((txn) -> System.out.println(JsonUtil.getPrettyJson(txn))) + .completeAndWait(msg -> System.out.println(msg)); + + System.out.println(result); + if (result.isSuccessful()) + checkIfUtxoAvailable(result.getValue(), wallet1.getBaseAddressString(0)); + return result; + } +} diff --git a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/Wallet.java b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/Wallet.java index b239fcb7..b40a990d 100644 --- a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/Wallet.java +++ b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/Wallet.java @@ -17,6 +17,8 @@ import com.bloxbean.cardano.client.transaction.TransactionSigner; import com.bloxbean.cardano.client.transaction.spec.Transaction; import com.bloxbean.cardano.client.transaction.spec.TransactionInput; +import com.bloxbean.cardano.hdwallet.utxosupplier.WalletUtxoSupplier; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Getter; import lombok.Setter; import java.util.*; @@ -26,9 +28,9 @@ public class Wallet { @Getter private int account = 0; @Getter - private Network network; + private final Network network; @Getter - private String mnemonic; + private final String mnemonic; @Setter @Getter private WalletUtxoSupplier utxoSupplier; @@ -91,29 +93,7 @@ public Address getEntAddress(int index) { * @return */ private Address getEntAddress(int account, int index) { - return getAccountObjectFromCache(account, index).getEnterpriseAddress(); - } - - private Account getAccountObjectFromCache(int account, int index) { - if(account != this.account) { - DerivationPath derivationPath = DerivationPath.createExternalAddressDerivationPathForAccount(account); - derivationPath.getIndex().setValue(index); - return new Account(this.network, this.mnemonic, derivationPath); - } else { - if(cache.containsKey(index)) { - return cache.get(index); - } else { - Account acc = new Account(this.network, this.mnemonic, index); - cache.put(index, acc); - return acc; - } - } - } - - public void setAccount(int account) { - this.account = account; - // invalidating cache since it is only held for an account - cache = new HashMap<>(); + return getAccountObject(account, index).getEnterpriseAddress(); } /** @@ -125,6 +105,11 @@ public Address getBaseAddress(int index) { return getBaseAddress(this.account, index); } + /** + * Get Baseaddress for current account as String. Account can be changed via the setter. + * @param index + * @return + */ public String getBaseAddressString(int index) { return getBaseAddress(index).getAddress(); } @@ -136,47 +121,67 @@ public String getBaseAddressString(int index) { * @return */ public Address getBaseAddress(int account, int index) { - return getAccountObjectFromCache(account,index).getBaseAddress(); + return getAccountObject(account,index).getBaseAddress(); } /** - * Returns the RootkeyPair + * Returns the Account object for the index and current account. Account can be changed via the setter. + * @param index * @return */ - public HdKeyPair getHDWalletKeyPair() { - if(rootKeys == null) { - HdKeyGenerator hdKeyGenerator = new HdKeyGenerator(); - try { - byte[] entropy = MnemonicCode.INSTANCE.toEntropy(this.mnemonic); - rootKeys = hdKeyGenerator.getRootKeyPairFromEntropy(entropy); - } catch (MnemonicException.MnemonicLengthException e) { - throw new RuntimeException(e); - } catch (MnemonicException.MnemonicWordException e) { - throw new RuntimeException(e); - } catch (MnemonicException.MnemonicChecksumException e) { - throw new RuntimeException(e); - } - } - return rootKeys; + public Account getAccountObject(int index) { + return getAccountObject(this.account, index); } /** - * TODO Renaming?? + * Returns the Account object for the index and account. * @param account * @param index * @return */ - public Account getSigner(int account, int index) { - return getAccountObjectFromCache(account, index); + public Account getAccountObject(int account, int index) { + if(account != this.account) { + DerivationPath derivationPath = DerivationPath.createExternalAddressDerivationPathForAccount(account); + derivationPath.getIndex().setValue(index); + return new Account(this.network, this.mnemonic, derivationPath); + } else { + if(cache.containsKey(index)) { + return cache.get(index); + } else { + Account acc = new Account(this.network, this.mnemonic, index); + cache.put(index, acc); + return acc; + } + } + } + + /** + * Setting the current account for derivation path. + * Setting the account will reset the cache. + * @param account + */ + public void setAccount(int account) { + this.account = account; + // invalidating cache since it is only held for one account + cache = new HashMap<>(); } /** - * TODO Renaming?? - * @param index + * Returns the RootkeyPair * @return */ - public Account getSigner(int index) { - return getAccountObjectFromCache(this.account, index); + public HdKeyPair getHDWalletKeyPair() { + if(rootKeys == null) { + HdKeyGenerator hdKeyGenerator = new HdKeyGenerator(); + try { + byte[] entropy = MnemonicCode.INSTANCE.toEntropy(this.mnemonic); + rootKeys = hdKeyGenerator.getRootKeyPairFromEntropy(entropy); + } catch (MnemonicException.MnemonicLengthException | MnemonicException.MnemonicWordException | + MnemonicException.MnemonicChecksumException e) { + throw new RuntimeException(e); + } + } + return rootKeys; } /** @@ -227,13 +232,17 @@ private List getSignersForInputs(List inputs) { private boolean matchUtxoWithInputs(List inputs, Utxo utxo, List signers, int index, List remaining) { for (TransactionInput input : inputs) { if(utxo.getTxHash().equals(input.getTransactionId()) && utxo.getOutputIndex() == input.getIndex()) { - signers.add(getSigner(index)); + signers.add(getAccountObject(index)); remaining.remove(input); } } return remaining.isEmpty(); } + /** + * Returns the stake address of the wallet. + * @return + */ public String getStakeAddress() { if (stakeAddress == null || stakeAddress.isEmpty()) { HdKeyPair stakeKeyPair = getStakeKeyPair(); @@ -243,6 +252,11 @@ public String getStakeAddress() { return stakeAddress; } + /** + * Signs the transaction with stake key from wallet. + * @param transaction + * @return + */ public Transaction signWithStakeKey(Transaction transaction) { return TransactionSigner.INSTANCE.sign(transaction, getStakeKeyPair()); } @@ -261,4 +275,9 @@ private HdKeyPair getStakeKeyPair() { } + @JsonIgnore + public String getBech32PrivateKey() { + HdKeyPair hdKeyPair = getHDWalletKeyPair(); + return hdKeyPair.getPrivateKey().toBech32(); + } } diff --git a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/WalletUtxoSupplier.java b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/utxosupplier/DefaultWalletUtxoSupplier.java similarity index 87% rename from hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/WalletUtxoSupplier.java rename to hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/utxosupplier/DefaultWalletUtxoSupplier.java index d8a42095..247418a2 100644 --- a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/WalletUtxoSupplier.java +++ b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/utxosupplier/DefaultWalletUtxoSupplier.java @@ -1,6 +1,5 @@ -package com.bloxbean.cardano.hdwallet; +package com.bloxbean.cardano.hdwallet.utxosupplier; -import com.bloxbean.cardano.client.address.Address; import com.bloxbean.cardano.client.api.UtxoSupplier; import com.bloxbean.cardano.client.api.common.OrderEnum; import com.bloxbean.cardano.client.api.exception.ApiException; @@ -8,6 +7,7 @@ import com.bloxbean.cardano.client.api.model.Result; import com.bloxbean.cardano.client.api.model.Utxo; import com.bloxbean.cardano.client.backend.api.UtxoService; +import com.bloxbean.cardano.hdwallet.Wallet; import lombok.Setter; import java.util.ArrayList; @@ -15,19 +15,19 @@ import java.util.List; import java.util.Optional; -public class WalletUtxoSupplier implements UtxoSupplier { +public class DefaultWalletUtxoSupplier implements WalletUtxoSupplier { private final UtxoService utxoService; @Setter private Wallet wallet; private static final int INDEX_SEARCH_RANGE = 20; // according to specifications - public WalletUtxoSupplier(UtxoService utxoService, Wallet wallet) { + public DefaultWalletUtxoSupplier(UtxoService utxoService, Wallet wallet) { this.utxoService = utxoService; this.wallet = wallet; } - public WalletUtxoSupplier(UtxoService utxoService) { + public DefaultWalletUtxoSupplier(UtxoService utxoService) { this.utxoService = utxoService; } @@ -52,12 +52,13 @@ public List getAll(String address) { return getAll(wallet); } + @Override public List getAll(Wallet wallet) { - List utxos = new ArrayList<>(); int index = 0; int noUtxoFound = 0; + List utxos = new ArrayList<>(); while(noUtxoFound < INDEX_SEARCH_RANGE) { - List utxoFromIndex = getUtxosForAccountAndIndex(wallet.getAccount(), index); + List utxoFromIndex = getUtxosForAccountAndIndex(wallet, wallet.getAccount(), index); utxos.addAll(utxoFromIndex); noUtxoFound = utxoFromIndex.isEmpty() ? noUtxoFound + 1 : 0; @@ -66,22 +67,7 @@ public List getAll(Wallet wallet) { return utxos; } - private void checkIfWalletIsSet() { - if(this.wallet == null) - throw new RuntimeException("Wallet has to be provided!"); - } - - /** - * Returns all UTXOs for a specific address m/1852'/1815'/{account}'/0/{index} - * @param account - * @param index - * @return - */ - public List getUtxosForAccountAndIndex(int account, int index) { - checkIfWalletIsSet(); - return getUtxosForAccountAndIndex(this.wallet, account, index); - } - + @Override public List getUtxosForAccountAndIndex(Wallet wallet, int account, int index) { String address = wallet.getBaseAddress(account, index).getAddress(); List utxos = new ArrayList<>(); @@ -99,7 +85,17 @@ public List getUtxosForAccountAndIndex(Wallet wallet, int account, int ind break; page++; } - return utxos; } + + @Override + public List getUtxosForAccountAndIndex(int account, int index) { + checkIfWalletIsSet(); + return getUtxosForAccountAndIndex(this.wallet, account, index); + } + + private void checkIfWalletIsSet() { + if(this.wallet == null) + throw new RuntimeException("Wallet has to be provided!"); + } } diff --git a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/utxosupplier/WalletUtxoSupplier.java b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/utxosupplier/WalletUtxoSupplier.java new file mode 100644 index 00000000..16d3c07f --- /dev/null +++ b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/utxosupplier/WalletUtxoSupplier.java @@ -0,0 +1,34 @@ +package com.bloxbean.cardano.hdwallet.utxosupplier; + +import com.bloxbean.cardano.client.api.UtxoSupplier; +import com.bloxbean.cardano.client.api.model.Utxo; +import com.bloxbean.cardano.hdwallet.Wallet; + +import java.util.List; + +public interface WalletUtxoSupplier extends UtxoSupplier { + + /** + * Returns all Utxos for provided wallets + * @param wallet + * @return + */ + List getAll(Wallet wallet); + + /** + * Returns all UTXOs for a specific address of a wallet m/1852'/1815'/{account}'/0/{index} + * @param wallet + * @param account + * @param index + * @return + */ + List getUtxosForAccountAndIndex(Wallet wallet, int account, int index); + + /** + * Returns all UTXOs for a specific address m/1852'/1815'/{account}'/0/{index} + * @param account + * @param index + * @return + */ + List getUtxosForAccountAndIndex(int account, int index); +} diff --git a/hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/WalletTest.java b/hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/WalletTest.java index fe5101ab..f1e84d6e 100644 --- a/hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/WalletTest.java +++ b/hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/WalletTest.java @@ -3,24 +3,106 @@ import com.bloxbean.cardano.client.account.Account; import com.bloxbean.cardano.client.address.Address; import com.bloxbean.cardano.client.common.model.Networks; +import com.bloxbean.cardano.client.crypto.bip39.Words; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; public class WalletTest { + String phrase24W = "coconut you order found animal inform tent anxiety pepper aisle web horse source indicate eyebrow viable lawsuit speak dragon scheme among animal slogan exchange"; + + String baseAddress0 = "addr1qxsaa6czesrzwp45rd5flg86n5hnwhz5setqfyt39natwvsl5mr3vkp82y2kcwxxtu4zjcxvm80ttmx2hyeyjka4v8ps7zwsra"; + String baseAddress1 = "addr1q93jwnn3hvgcuv02tqe08lpdkxxpmvapxgjxwewya47tqsgl5mr3vkp82y2kcwxxtu4zjcxvm80ttmx2hyeyjka4v8ps4zthxn"; + String baseAddress2 = "addr1q8pr30ykyfa3pw6qkkun3dyyxsvftq3xukuyxdt58pxcpxgl5mr3vkp82y2kcwxxtu4zjcxvm80ttmx2hyeyjka4v8ps4qp6cs"; + String baseAddress3 = "addr1qxa5pll82u8lqtzqjqhdr828medvfvezv4509nzyuhwt5aql5mr3vkp82y2kcwxxtu4zjcxvm80ttmx2hyeyjka4v8psy8jsmy"; + + String testnetBaseAddress0 = "addr_test1qzsaa6czesrzwp45rd5flg86n5hnwhz5setqfyt39natwvsl5mr3vkp82y2kcwxxtu4zjcxvm80ttmx2hyeyjka4v8psa5ns0z"; + String testnetBaseAddress1 = "addr_test1qp3jwnn3hvgcuv02tqe08lpdkxxpmvapxgjxwewya47tqsgl5mr3vkp82y2kcwxxtu4zjcxvm80ttmx2hyeyjka4v8psk5kh2v"; + String testnetBaseAddress2 = "addr_test1qrpr30ykyfa3pw6qkkun3dyyxsvftq3xukuyxdt58pxcpxgl5mr3vkp82y2kcwxxtu4zjcxvm80ttmx2hyeyjka4v8pskku650"; + + String entAddress0 = "addr1vxsaa6czesrzwp45rd5flg86n5hnwhz5setqfyt39natwvstf7k4n"; + String entAddress1 = "addr1v93jwnn3hvgcuv02tqe08lpdkxxpmvapxgjxwewya47tqsg7davae"; + String entAddress2 = "addr1v8pr30ykyfa3pw6qkkun3dyyxsvftq3xukuyxdt58pxcpxgvddj89"; + + String testnetEntAddress0 = "addr_test1vzsaa6czesrzwp45rd5flg86n5hnwhz5setqfyt39natwvssp226k"; + String testnetEntAddress1 = "addr_test1vp3jwnn3hvgcuv02tqe08lpdkxxpmvapxgjxwewya47tqsg99fsju"; + String testnetEntAddress2 = "addr_test1vrpr30ykyfa3pw6qkkun3dyyxsvftq3xukuyxdt58pxcpxgh9ewgq"; + @Test - void generateMnemonicTest() { + void generateMnemonic24w() { Wallet hdWallet = new Wallet(Networks.testnet(), null); String mnemonic = hdWallet.getMnemonic(); assertEquals(24, mnemonic.split(" ").length); } @Test - void getAccountFromIndex() { + void generateMnemonic15w() { + Wallet hdWallet = new Wallet(Networks.testnet(), Words.FIFTEEN, null); + String mnemonic = hdWallet.getMnemonic(); + assertEquals(15, mnemonic.split(" ").length); + } + + @Test + void WalletAddressToAccountAddressTest() { Wallet hdWallet = new Wallet(Networks.testnet(), null); Address address = hdWallet.getBaseAddress(0); Account a = new Account(hdWallet.getNetwork(), hdWallet.getMnemonic(), 0); assertEquals(address.getAddress(), a.getBaseAddress().getAddress()); } + + @Test + public void testGetBaseAddressFromMnemonicIndex_0() { + Wallet wallet = new Wallet(Networks.mainnet(), phrase24W, null); + Assertions.assertEquals(baseAddress0, wallet.getBaseAddressString(0)); + Assertions.assertEquals(baseAddress1, wallet.getBaseAddressString(1)); + Assertions.assertEquals(baseAddress2, wallet.getBaseAddressString(2)); + Assertions.assertEquals(baseAddress3, wallet.getBaseAddressString(3)); + } + + @Test + public void testGetBaseAddressFromMnemonicByNetworkInfoTestnet() { + Wallet wallet = new Wallet(Networks.testnet(), phrase24W, null); + Assertions.assertEquals(testnetBaseAddress0, wallet.getBaseAddressString(0)); + Assertions.assertEquals(testnetBaseAddress1, wallet.getBaseAddressString(1)); + Assertions.assertEquals(testnetBaseAddress2, wallet.getBaseAddressString(2)); + } + + @Test + public void testGetEnterpriseAddressFromMnemonicIndex() { + Wallet wallet = new Wallet(Networks.mainnet(), phrase24W, null); + Assertions.assertEquals(entAddress0, wallet.getEntAddress(0).getAddress()); + Assertions.assertEquals(entAddress1, wallet.getEntAddress(1).getAddress()); + Assertions.assertEquals(entAddress2, wallet.getEntAddress(2).getAddress()); + } + + @Test + public void testGetEnterpriseAddressFromMnemonicIndexByNetwork() { + Wallet wallet = new Wallet(Networks.testnet(), phrase24W, null); + Assertions.assertEquals(testnetEntAddress0, wallet.getEntAddress(0).getAddress()); + Assertions.assertEquals(testnetEntAddress1, wallet.getEntAddress(1).getAddress()); + Assertions.assertEquals(testnetEntAddress2, wallet.getEntAddress(2).getAddress()); + } + + @Test + public void testGetPrivateKeyFromMnemonic() { + String pvtKey = new Wallet(phrase24W, null).getBech32PrivateKey(); + System.out.println(pvtKey); + Assertions.assertTrue(pvtKey.length() > 5); + } + + @Test + public void testGetPublicKeyBytesFromMnemonic() { + byte[] pubKey = new Wallet(phrase24W, null).getHDWalletKeyPair().getPublicKey().getKeyData(); + Assertions.assertEquals(32, pubKey.length); + } + + @Test + public void testGetPrivateKeyBytesFromMnemonic() { + byte[] pvtKey = new Wallet(phrase24W, null).getHDWalletKeyPair().getPrivateKey().getBytes(); + Assertions.assertEquals(96, pvtKey.length); + } + + } diff --git a/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/QuickTxBuilder.java b/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/QuickTxBuilder.java index f08a890f..3c758ae1 100644 --- a/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/QuickTxBuilder.java +++ b/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/QuickTxBuilder.java @@ -27,6 +27,7 @@ import com.bloxbean.cardano.client.function.helper.ScriptBalanceTxProviders; import com.bloxbean.cardano.client.transaction.spec.Transaction; import com.bloxbean.cardano.client.transaction.spec.TransactionInput; +import com.bloxbean.cardano.hdwallet.utxosupplier.WalletUtxoSupplier; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; @@ -384,6 +385,10 @@ public Result complete() { if (txList.length == 0) throw new TxBuildException("At least one tx is required"); + boolean txListContainsWallet = Arrays.stream(txList).anyMatch(abstractTx -> abstractTx.getFromWallet() != null); + if(txListContainsWallet && !(utxoSupplier instanceof WalletUtxoSupplier)) + throw new TxBuildException("Provide a WalletUtxoSupplier when using a sender wallet"); + Transaction transaction = buildAndSign(); if (txInspector != null) From 64df79e1d22061053ab43fe0afd0ada1e421a1c3 Mon Sep 17 00:00:00 2001 From: Kammerlo Date: Mon, 22 Jan 2024 13:51:33 +0100 Subject: [PATCH 07/16] #213 implemented signing with wallet. A UTXOSupplier will be passed. Thus removing the unneeded utxosupplier value in Wallet. --- .../function/helper/SignerProviders.java | 12 +++-- .../cardano/hdwallet/QuickTxBuilderIT.java | 13 +++--- .../bloxbean/cardano/hdwallet/StakeTxIT.java | 29 ++++++------ .../com/bloxbean/cardano/hdwallet/Wallet.java | 45 +++++++++---------- .../DefaultWalletUtxoSupplier.java | 2 +- .../WalletUtxoSupplier.java | 2 +- .../bloxbean/cardano/hdwallet/WalletTest.java | 20 ++++----- .../client/quicktx/QuickTxBuilder.java | 18 +++++++- 8 files changed, 79 insertions(+), 62 deletions(-) rename hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/{utxosupplier => supplier}/DefaultWalletUtxoSupplier.java (98%) rename hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/{utxosupplier => supplier}/WalletUtxoSupplier.java (94%) diff --git a/function/src/main/java/com/bloxbean/cardano/client/function/helper/SignerProviders.java b/function/src/main/java/com/bloxbean/cardano/client/function/helper/SignerProviders.java index dec76771..aadc77d3 100644 --- a/function/src/main/java/com/bloxbean/cardano/client/function/helper/SignerProviders.java +++ b/function/src/main/java/com/bloxbean/cardano/client/function/helper/SignerProviders.java @@ -8,6 +8,7 @@ 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 {@link Transaction} object @@ -31,10 +32,15 @@ public static TxSigner signerFrom(Account... signers) { }; } - public static TxSigner signerFrom(Wallet wallet) { + /** + * Function to sign a transaction using one or more Wallet + * @param wallet wallet(s) to sign the transaction + * @param walletUtxoSupplier WalletUtxoSupplier is needed to sign with the right addresses + * @return TxSigner function which returns a Transaction object with witnesses when invoked + */ + public static TxSigner signerFrom(Wallet wallet, WalletUtxoSupplier walletUtxoSupplier) { return transaction -> { -// transaction.getBody().getRequiredSigners(); // TODO - look into using this field - it is normally used for smart contracts. Downside it will increase TX size. - Transaction outputTxn = wallet.sign(transaction); // TODO - check if it's possible to get the context here to avoid fetching all utxos over and over again + Transaction outputTxn = wallet.sign(transaction, walletUtxoSupplier); return outputTxn; }; } diff --git a/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTxBuilderIT.java b/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTxBuilderIT.java index bccd7e28..b809e0bf 100644 --- a/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTxBuilderIT.java +++ b/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTxBuilderIT.java @@ -16,7 +16,7 @@ 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.utxosupplier.DefaultWalletUtxoSupplier; +import com.bloxbean.cardano.hdwallet.supplier.DefaultWalletUtxoSupplier; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -40,11 +40,10 @@ void setup() { backendService = getBackendService(); utxoSupplier = getUTXOSupplier(); - walletUtxoSupplier = new DefaultWalletUtxoSupplier(backendService.getUtxoService()); 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, walletUtxoSupplier); + 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, walletUtxoSupplier); + wallet2 = new Wallet(Networks.testnet(), wallet2Mnemonic); } @Test @@ -58,11 +57,11 @@ void simplePayment() { QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService, walletUtxoSupplier); Tx tx = new Tx() - .payToAddress(wallet2.getBaseAddress(0).getAddress(), Amount.ada(5)) + .payToAddress(wallet2.getBaseAddress(0).getAddress(), Amount.ada(4)) .from(wallet1); Result result = quickTxBuilder.compose(tx) - .withSigner(SignerProviders.signerFrom(wallet1)) + .withSigner(wallet1) .complete(); System.out.println(result); @@ -86,7 +85,7 @@ void minting() throws CborSerializationException { UtxoSupplier walletUtxoSupplier = new DefaultWalletUtxoSupplier(backendService.getUtxoService(), wallet1); QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService, walletUtxoSupplier); Result result = quickTxBuilder.compose(tx) - .withSigner(SignerProviders.signerFrom(wallet1)) + .withSigner(wallet1) .withSigner(SignerProviders.signerFrom(policy)) .complete(); diff --git a/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/StakeTxIT.java b/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/StakeTxIT.java index 38ad13f6..5d4f39b9 100644 --- a/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/StakeTxIT.java +++ b/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/StakeTxIT.java @@ -20,8 +20,8 @@ import com.bloxbean.cardano.client.quicktx.ScriptTx; import com.bloxbean.cardano.client.quicktx.Tx; import com.bloxbean.cardano.client.util.JsonUtil; -import com.bloxbean.cardano.hdwallet.utxosupplier.DefaultWalletUtxoSupplier; -import com.bloxbean.cardano.hdwallet.utxosupplier.WalletUtxoSupplier; +import com.bloxbean.cardano.hdwallet.supplier.DefaultWalletUtxoSupplier; +import com.bloxbean.cardano.hdwallet.supplier.WalletUtxoSupplier; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.*; @@ -61,11 +61,10 @@ void setup() { protocolParamsSupplier = new DefaultProtocolParamsSupplier(backendService.getEpochService()); quickTxBuilder = new QuickTxBuilder(backendService); - walletUtxoSupplier = new DefaultWalletUtxoSupplier(backendService.getUtxoService()); 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, walletUtxoSupplier); + 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, walletUtxoSupplier); + wallet2 = new Wallet(Networks.testnet(), wallet2Mnemonic); if (backendType.equals(DEVKIT)) { poolId = "pool1wvqhvyrgwch4jq9aa84hc8q4kzvyq2z3xr6mpafkqmx9wce39zy"; @@ -92,7 +91,7 @@ void stakeAddressRegistration() { .from(wallet1); Result result = quickTxBuilder.compose(tx) - .withSigner(SignerProviders.signerFrom(wallet1)) + .withSigner(wallet1) .withTxInspector((txn) -> System.out.println(JsonUtil.getPrettyJson(txn))) .completeAndWait(msg -> System.out.println(msg)); @@ -115,7 +114,7 @@ void stakeAddressDeRegistration() { .from(wallet1); Result result = quickTxBuilder.compose(tx) - .withSigner(SignerProviders.signerFrom(wallet1)) + .withSigner(wallet1) .withSigner(SignerProviders.stakeKeySignerFrom(wallet1)) .withTxInspector((txn) -> System.out.println(JsonUtil.getPrettyJson(txn))) .completeAndWait(msg -> System.out.println(msg)); @@ -137,7 +136,7 @@ void stakeAddressRegistration_onlyRegistration() { .from(wallet1); Result result = quickTxBuilder.compose(tx) - .withSigner(SignerProviders.signerFrom(wallet1)) + .withSigner(wallet1) .withTxInspector((txn) -> System.out.println(JsonUtil.getPrettyJson(txn))) .completeAndWait(msg -> System.out.println(msg)); @@ -158,7 +157,7 @@ void stakeAddressDeRegistration_onlyRegistration() { .from(wallet1); Result result = quickTxBuilder.compose(tx) - .withSigner(SignerProviders.signerFrom(wallet1)) + .withSigner(wallet1) .withSigner(SignerProviders.stakeKeySignerFrom(wallet1)) .withTxInspector((txn) -> System.out.println(JsonUtil.getPrettyJson(txn))) .completeAndWait(msg -> System.out.println(msg)); @@ -181,7 +180,7 @@ void scriptStakeAddress_registration() { .from(wallet1); Result result = quickTxBuilder.compose(tx) - .withSigner(SignerProviders.signerFrom(wallet1)) + .withSigner(wallet1) .withTxInspector((txn) -> System.out.println(JsonUtil.getPrettyJson(txn))) .completeAndWait(msg -> System.out.println(msg)); @@ -203,7 +202,7 @@ void scriptStakeAddress_deRegistration() { Result result = quickTxBuilder.compose(tx) .feePayer(wallet1.getBaseAddressString(0)) - .withSigner(SignerProviders.signerFrom(wallet1)) + .withSigner(wallet1) .withTxInspector((txn) -> System.out.println(JsonUtil.getPrettyJson(txn))) .withTxEvaluator(!backendType.equals(BLOCKFROST) ? new AikenTransactionEvaluator(utxoSupplier, protocolParamsSupplier) : null) @@ -231,7 +230,7 @@ void stakeDelegation_scriptStakeKeys() { Result delgResult = quickTxBuilder.compose(delegTx) .feePayer(wallet1.getBaseAddressString(0)) - .withSigner(SignerProviders.signerFrom(wallet1)) + .withSigner(wallet1) .withTxEvaluator(!backendType.equals(BLOCKFROST) ? new AikenTransactionEvaluator(utxoSupplier, protocolParamsSupplier) : null) .withTxInspector((txn) -> System.out.println(JsonUtil.getPrettyJson(txn))) @@ -256,7 +255,7 @@ private void registerScriptsStakeKeys() { .from(wallet1); Result result = quickTxBuilder.compose(tx) - .withSigner(SignerProviders.signerFrom(wallet1)) + .withSigner(wallet1) .withTxInspector((txn) -> System.out.println(JsonUtil.getPrettyJson(txn))) .completeAndWait(msg -> System.out.println(msg)); @@ -274,7 +273,7 @@ private void deregisterScriptsStakeKeys() { Result result = quickTxBuilder.compose(tx) .feePayer(wallet1.getBaseAddressString(0)) - .withSigner(SignerProviders.signerFrom(wallet1)) + .withSigner(wallet1) .withTxEvaluator(!backendType.equals(BLOCKFROST) ? new AikenTransactionEvaluator(utxoSupplier, protocolParamsSupplier) : null) .withTxInspector((txn) -> System.out.println(JsonUtil.getPrettyJson(txn))) @@ -297,7 +296,7 @@ private Result _deRegisterStakeKeys() { .from(wallet1); Result result = quickTxBuilder.compose(tx) - .withSigner(SignerProviders.signerFrom(wallet1)) + .withSigner(wallet1) .withSigner(SignerProviders.stakeKeySignerFrom(wallet1)) .withSigner(SignerProviders.stakeKeySignerFrom(wallet2)) .withTxInspector((txn) -> System.out.println(JsonUtil.getPrettyJson(txn))) diff --git a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/Wallet.java b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/Wallet.java index b40a990d..8849946d 100644 --- a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/Wallet.java +++ b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/Wallet.java @@ -17,10 +17,10 @@ import com.bloxbean.cardano.client.transaction.TransactionSigner; import com.bloxbean.cardano.client.transaction.spec.Transaction; import com.bloxbean.cardano.client.transaction.spec.TransactionInput; -import com.bloxbean.cardano.hdwallet.utxosupplier.WalletUtxoSupplier; +import com.bloxbean.cardano.hdwallet.supplier.WalletUtxoSupplier; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Getter; -import lombok.Setter; + import java.util.*; public class Wallet { @@ -31,49 +31,44 @@ public class Wallet { private final Network network; @Getter private final String mnemonic; - @Setter - @Getter - private WalletUtxoSupplier utxoSupplier; private static final int INDEX_SEARCH_RANGE = 20; private String stakeAddress; private Map cache; private HdKeyPair rootKeys; private HdKeyPair stakeKeys; - public Wallet(WalletUtxoSupplier utxoSupplier) { - this(Networks.mainnet(), utxoSupplier); + public Wallet() { + this(Networks.mainnet()); } - public Wallet(Network network, WalletUtxoSupplier utxoSupplier) { - this(network, Words.TWENTY_FOUR, utxoSupplier); + public Wallet(Network network) { + this(network, Words.TWENTY_FOUR); } - public Wallet(Network network, Words noOfWords, WalletUtxoSupplier utxoSupplier) { - this(network, noOfWords, 0, utxoSupplier); + public Wallet(Network network, Words noOfWords) { + this(network, noOfWords, 0); } - public Wallet(Network network, Words noOfWords, int account, WalletUtxoSupplier utxoSupplier) { + public Wallet(Network network, Words noOfWords, int account) { this.network = network; this.mnemonic = MnemonicUtil.generateNew(noOfWords); this.account = account; - this.utxoSupplier = utxoSupplier; cache = new HashMap<>(); } - public Wallet(String mnemonic, WalletUtxoSupplier utxoSupplier) { - this(Networks.mainnet(), mnemonic, utxoSupplier); + public Wallet(String mnemonic) { + this(Networks.mainnet(), mnemonic); } - public Wallet(Network network, String mnemonic, WalletUtxoSupplier utxoSupplier) { - this(network,mnemonic, 0, utxoSupplier); + public Wallet(Network network, String mnemonic) { + this(network,mnemonic, 0); } - public Wallet(Network network, String mnemonic, int account, WalletUtxoSupplier utxoSupplier) { + public Wallet(Network network, String mnemonic, int account) { this.network = network; this.mnemonic = mnemonic; this.account = account; MnemonicUtil.validateMnemonic(this.mnemonic); - this.utxoSupplier = utxoSupplier; cache = new HashMap<>(); } @@ -189,8 +184,8 @@ public HdKeyPair getHDWalletKeyPair() { * @param txToSign * @return signed Transaction */ - public Transaction sign(Transaction txToSign) { - List signers = getSignersForTransaction(txToSign); + public Transaction sign(Transaction txToSign, WalletUtxoSupplier utxoSupplier) { + List signers = getSignersForTransaction(txToSign, utxoSupplier); if(signers.isEmpty()) throw new RuntimeException("No signers found!"); @@ -202,14 +197,16 @@ public Transaction sign(Transaction txToSign) { /** * Returns a list with signers needed for this transaction + * * @param tx + * @param utxoSupplier * @return */ - public List getSignersForTransaction(Transaction tx) { - return getSignersForInputs(tx.getBody().getInputs()); + public List getSignersForTransaction(Transaction tx, WalletUtxoSupplier utxoSupplier) { + return getSignersForInputs(tx.getBody().getInputs(), utxoSupplier); } - private List getSignersForInputs(List inputs) { + private List getSignersForInputs(List inputs, WalletUtxoSupplier utxoSupplier) { // searching for address to sign List signers = new ArrayList<>(); List remaining = new ArrayList<>(inputs); diff --git a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/utxosupplier/DefaultWalletUtxoSupplier.java b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/supplier/DefaultWalletUtxoSupplier.java similarity index 98% rename from hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/utxosupplier/DefaultWalletUtxoSupplier.java rename to hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/supplier/DefaultWalletUtxoSupplier.java index 247418a2..26cd66bb 100644 --- a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/utxosupplier/DefaultWalletUtxoSupplier.java +++ b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/supplier/DefaultWalletUtxoSupplier.java @@ -1,4 +1,4 @@ -package com.bloxbean.cardano.hdwallet.utxosupplier; +package com.bloxbean.cardano.hdwallet.supplier; import com.bloxbean.cardano.client.api.UtxoSupplier; import com.bloxbean.cardano.client.api.common.OrderEnum; diff --git a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/utxosupplier/WalletUtxoSupplier.java b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/supplier/WalletUtxoSupplier.java similarity index 94% rename from hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/utxosupplier/WalletUtxoSupplier.java rename to hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/supplier/WalletUtxoSupplier.java index 16d3c07f..6a8259a8 100644 --- a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/utxosupplier/WalletUtxoSupplier.java +++ b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/supplier/WalletUtxoSupplier.java @@ -1,4 +1,4 @@ -package com.bloxbean.cardano.hdwallet.utxosupplier; +package com.bloxbean.cardano.hdwallet.supplier; import com.bloxbean.cardano.client.api.UtxoSupplier; import com.bloxbean.cardano.client.api.model.Utxo; diff --git a/hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/WalletTest.java b/hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/WalletTest.java index f1e84d6e..48f90ec0 100644 --- a/hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/WalletTest.java +++ b/hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/WalletTest.java @@ -32,21 +32,21 @@ public class WalletTest { @Test void generateMnemonic24w() { - Wallet hdWallet = new Wallet(Networks.testnet(), null); + Wallet hdWallet = new Wallet(Networks.testnet()); String mnemonic = hdWallet.getMnemonic(); assertEquals(24, mnemonic.split(" ").length); } @Test void generateMnemonic15w() { - Wallet hdWallet = new Wallet(Networks.testnet(), Words.FIFTEEN, null); + Wallet hdWallet = new Wallet(Networks.testnet(), Words.FIFTEEN); String mnemonic = hdWallet.getMnemonic(); assertEquals(15, mnemonic.split(" ").length); } @Test void WalletAddressToAccountAddressTest() { - Wallet hdWallet = new Wallet(Networks.testnet(), null); + Wallet hdWallet = new Wallet(Networks.testnet()); Address address = hdWallet.getBaseAddress(0); Account a = new Account(hdWallet.getNetwork(), hdWallet.getMnemonic(), 0); assertEquals(address.getAddress(), a.getBaseAddress().getAddress()); @@ -54,7 +54,7 @@ void WalletAddressToAccountAddressTest() { @Test public void testGetBaseAddressFromMnemonicIndex_0() { - Wallet wallet = new Wallet(Networks.mainnet(), phrase24W, null); + Wallet wallet = new Wallet(Networks.mainnet(), phrase24W); Assertions.assertEquals(baseAddress0, wallet.getBaseAddressString(0)); Assertions.assertEquals(baseAddress1, wallet.getBaseAddressString(1)); Assertions.assertEquals(baseAddress2, wallet.getBaseAddressString(2)); @@ -63,7 +63,7 @@ public void testGetBaseAddressFromMnemonicIndex_0() { @Test public void testGetBaseAddressFromMnemonicByNetworkInfoTestnet() { - Wallet wallet = new Wallet(Networks.testnet(), phrase24W, null); + Wallet wallet = new Wallet(Networks.testnet(), phrase24W); Assertions.assertEquals(testnetBaseAddress0, wallet.getBaseAddressString(0)); Assertions.assertEquals(testnetBaseAddress1, wallet.getBaseAddressString(1)); Assertions.assertEquals(testnetBaseAddress2, wallet.getBaseAddressString(2)); @@ -71,7 +71,7 @@ public void testGetBaseAddressFromMnemonicByNetworkInfoTestnet() { @Test public void testGetEnterpriseAddressFromMnemonicIndex() { - Wallet wallet = new Wallet(Networks.mainnet(), phrase24W, null); + Wallet wallet = new Wallet(Networks.mainnet(), phrase24W); Assertions.assertEquals(entAddress0, wallet.getEntAddress(0).getAddress()); Assertions.assertEquals(entAddress1, wallet.getEntAddress(1).getAddress()); Assertions.assertEquals(entAddress2, wallet.getEntAddress(2).getAddress()); @@ -79,7 +79,7 @@ public void testGetEnterpriseAddressFromMnemonicIndex() { @Test public void testGetEnterpriseAddressFromMnemonicIndexByNetwork() { - Wallet wallet = new Wallet(Networks.testnet(), phrase24W, null); + Wallet wallet = new Wallet(Networks.testnet(), phrase24W); Assertions.assertEquals(testnetEntAddress0, wallet.getEntAddress(0).getAddress()); Assertions.assertEquals(testnetEntAddress1, wallet.getEntAddress(1).getAddress()); Assertions.assertEquals(testnetEntAddress2, wallet.getEntAddress(2).getAddress()); @@ -87,20 +87,20 @@ public void testGetEnterpriseAddressFromMnemonicIndexByNetwork() { @Test public void testGetPrivateKeyFromMnemonic() { - String pvtKey = new Wallet(phrase24W, null).getBech32PrivateKey(); + String pvtKey = new Wallet(phrase24W).getBech32PrivateKey(); System.out.println(pvtKey); Assertions.assertTrue(pvtKey.length() > 5); } @Test public void testGetPublicKeyBytesFromMnemonic() { - byte[] pubKey = new Wallet(phrase24W, null).getHDWalletKeyPair().getPublicKey().getKeyData(); + byte[] pubKey = new Wallet(phrase24W).getHDWalletKeyPair().getPublicKey().getKeyData(); Assertions.assertEquals(32, pubKey.length); } @Test public void testGetPrivateKeyBytesFromMnemonic() { - byte[] pvtKey = new Wallet(phrase24W, null).getHDWalletKeyPair().getPrivateKey().getBytes(); + byte[] pvtKey = new Wallet(phrase24W).getHDWalletKeyPair().getPrivateKey().getBytes(); Assertions.assertEquals(96, pvtKey.length); } diff --git a/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/QuickTxBuilder.java b/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/QuickTxBuilder.java index 3c758ae1..d9072f1a 100644 --- a/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/QuickTxBuilder.java +++ b/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/QuickTxBuilder.java @@ -27,7 +27,8 @@ import com.bloxbean.cardano.client.function.helper.ScriptBalanceTxProviders; import com.bloxbean.cardano.client.transaction.spec.Transaction; import com.bloxbean.cardano.client.transaction.spec.TransactionInput; -import com.bloxbean.cardano.hdwallet.utxosupplier.WalletUtxoSupplier; +import com.bloxbean.cardano.hdwallet.Wallet; +import com.bloxbean.cardano.hdwallet.supplier.WalletUtxoSupplier; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; @@ -140,6 +141,7 @@ public class TxContext { private Verifier txVerifier; private boolean ignoreScriptCostEvaluationError = true; + private Wallet signerWallet; TxContext(AbstractTx... txs) { this.txList = txs; @@ -342,6 +344,11 @@ public Transaction buildAndSign() { Transaction transaction = build(); if (signers != null) transaction = signers.sign(transaction); + if(signerWallet != null) { + if(!(utxoSupplier instanceof WalletUtxoSupplier)) + throw new TxBuildException("Provide a WalletUtxoSupplier when using a sender wallet"); + transaction = signerWallet.sign(transaction, (WalletUtxoSupplier) utxoSupplier); + } return transaction; } @@ -514,6 +521,15 @@ public TxContext withSigner(@NonNull TxSigner signer) { this.signers = this.signers.andThen(signer); return this; } + /** + * Sign transaction with the given wallet + * @param wallet + * @return TxContext + */ + public TxContext withSigner(Wallet wallet) { + this.signerWallet = wallet; + return this; + } /** * Add validity start slot to the transaction. This value is set in "validity start from" field of the transaction. From 588d3aeff9c51e68b9d5ae7f6c6a4586e3917921 Mon Sep 17 00:00:00 2001 From: Satya Date: Sun, 24 Nov 2024 13:36:55 +0800 Subject: [PATCH 08/16] 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. --- .../java/com/bloxbean/cardano/client/quicktx/AbstractTx.java | 5 +---- .../main/java/com/bloxbean/cardano/client/quicktx/Tx.java | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/AbstractTx.java b/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/AbstractTx.java index d991c2bc..d4a8d17b 100644 --- a/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/AbstractTx.java +++ b/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/AbstractTx.java @@ -38,7 +38,6 @@ public abstract class AbstractTx { //custom change address protected String changeAddress; protected List inputUtxos; - protected List amounts; //Required for script protected PlutusData changeData; @@ -214,9 +213,7 @@ protected T payToAddress(String address, List amounts, byte[] datumHash, .address(address) .value(Value.builder().coin(BigInteger.ZERO).build()) .build(); - if(this.amounts == null) - this.amounts = new ArrayList<>(); - this.amounts.addAll(amounts); + for (Amount amount : amounts) { String unit = amount.getUnit(); if (unit.equals(LOVELACE)) { diff --git a/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/Tx.java b/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/Tx.java index ebe1ec53..1310f0ef 100644 --- a/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/Tx.java +++ b/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/Tx.java @@ -522,7 +522,7 @@ protected Wallet getFromWallet() { if(senderWallet != null) return senderWallet; else - throw new TxBuildException("No sender wallet defined"); + return null; } @Override From 3894bee5699909264234f8ecd88789cd82fc5d8a Mon Sep 17 00:00:00 2001 From: Satya Date: Sun, 24 Nov 2024 14:00:53 +0800 Subject: [PATCH 09/16] 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. --- .../client/function/TxBuilderContext.java | 17 ++++-- .../client/quicktx/QuickTxBuilder.java | 58 +++++++++---------- 2 files changed, 40 insertions(+), 35 deletions(-) diff --git a/function/src/main/java/com/bloxbean/cardano/client/function/TxBuilderContext.java b/function/src/main/java/com/bloxbean/cardano/client/function/TxBuilderContext.java index f4d3b4cf..3fa54d02 100644 --- a/function/src/main/java/com/bloxbean/cardano/client/function/TxBuilderContext.java +++ b/function/src/main/java/com/bloxbean/cardano/client/function/TxBuilderContext.java @@ -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; } @@ -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; } /** @@ -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(); diff --git a/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/QuickTxBuilder.java b/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/QuickTxBuilder.java index 42be7129..bb58ecb6 100644 --- a/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/QuickTxBuilder.java +++ b/quicktx/src/main/java/com/bloxbean/cardano/client/quicktx/QuickTxBuilder.java @@ -22,7 +22,7 @@ import com.bloxbean.cardano.client.transaction.spec.Transaction; import com.bloxbean.cardano.client.transaction.spec.TransactionInput; import com.bloxbean.cardano.client.util.JsonUtil; -import com.bloxbean.cardano.hdwallet.Wallet; +import com.bloxbean.cardano.client.util.Tuple; import com.bloxbean.cardano.hdwallet.supplier.WalletUtxoSupplier; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; @@ -116,6 +116,12 @@ public QuickTxBuilder(BackendService backendService) { } } + /** + * Create a QuickTxBuilder instance with specified BackendService and UtxoSupplier. + * + * @param backendService backend service to get protocol params and submit transactions + * @param utxoSupplier utxo supplier to get utxos + */ public QuickTxBuilder(BackendService backendService, UtxoSupplier utxoSupplier) { this(utxoSupplier, new DefaultProtocolParamsSupplier(backendService.getEpochService()), @@ -166,7 +172,6 @@ public class TxContext { private boolean ignoreScriptCostEvaluationError = true; private Era serializationEra; private boolean removeDuplicateScriptWitnesses = false; - private Wallet signerWallet; TxContext(AbstractTx... txs) { this.txList = txs; @@ -234,6 +239,25 @@ public TxContext additionalSignersCount(int additionalSigners) { * @return Transaction */ public Transaction build() { + Tuple tuple = _build(); + return tuple._1.build(tuple._2); + } + + /** + * Build and sign transaction + * + * @return Transaction + */ + public Transaction buildAndSign() { + Tuple tuple = _build(); + + if (signers != null) + return tuple._1.buildAndSign(tuple._2, signers); + else + throw new IllegalStateException("No signers found"); + } + + private Tuple _build() { TxBuilder txBuilder = (context, txn) -> { }; boolean containsScriptTx = false; @@ -383,7 +407,8 @@ public Transaction build() { tx.postBalanceTx(transaction); })); } - return txBuilderContext.build(txBuilder); + + return new Tuple<>(txBuilderContext, txBuilder); } private int getTotalSigners() { @@ -394,24 +419,6 @@ private int getTotalSigners() { return totalSigners; } - /** - * Build and sign transaction - * - * @return Transaction - */ - public Transaction buildAndSign() { - Transaction transaction = build(); - if (signers != null) - transaction = signers.sign(transaction); - if(signerWallet != null) { - if(!(utxoSupplier instanceof WalletUtxoSupplier)) - throw new TxBuildException("Provide a WalletUtxoSupplier when using a sender wallet"); - transaction = signerWallet.sign(transaction, (WalletUtxoSupplier) utxoSupplier); - } - - return transaction; - } - private TxBuilder buildCollateralOutput(String feePayer) { if (collateralInputs != null && !collateralInputs.isEmpty()) { List collateralUtxos = collateralInputs.stream() @@ -580,15 +587,6 @@ public TxContext withSigner(@NonNull TxSigner signer) { this.signers = this.signers.andThen(signer); return this; } - /** - * Sign transaction with the given wallet - * @param wallet - * @return TxContext - */ - public TxContext withSigner(Wallet wallet) { - this.signerWallet = wallet; - return this; - } /** * Add validity start slot to the transaction. This value is set in "validity start from" field of the transaction. From bc3ee91fb736e033d718fab2aa06eca9d0a29694 Mon Sep 17 00:00:00 2001 From: Satya Date: Sun, 24 Nov 2024 14:10:50 +0800 Subject: [PATCH 10/16] Refactor TxSigner to include TxBuilderContext --- .../cardano/client/function/TxSigner.java | 7 ++-- .../function/helper/SignerProviders.java | 32 +++++++++---------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/function/src/main/java/com/bloxbean/cardano/client/function/TxSigner.java b/function/src/main/java/com/bloxbean/cardano/client/function/TxSigner.java index 5a250f41..39fdc1a0 100644 --- a/function/src/main/java/com/bloxbean/cardano/client/function/TxSigner.java +++ b/function/src/main/java/com/bloxbean/cardano/client/function/TxSigner.java @@ -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 @@ -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)); } } diff --git a/function/src/main/java/com/bloxbean/cardano/client/function/helper/SignerProviders.java b/function/src/main/java/com/bloxbean/cardano/client/function/helper/SignerProviders.java index 7ebedbe4..e5715508 100644 --- a/function/src/main/java/com/bloxbean/cardano/client/function/helper/SignerProviders.java +++ b/function/src/main/java/com/bloxbean/cardano/client/function/helper/SignerProviders.java @@ -8,7 +8,6 @@ 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 {@link Transaction} object @@ -22,7 +21,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); @@ -33,14 +32,15 @@ public static TxSigner signerFrom(Account... signers) { } /** - * Function to sign a transaction using one or more Wallet - * @param wallet wallet(s) to sign the transaction - * @param walletUtxoSupplier WalletUtxoSupplier is needed to sign with the right addresses + * Function to sign a transaction with a wallet + * + * @param wallet wallet to sign the transaction * @return TxSigner function which returns a Transaction object with witnesses when invoked */ - public static TxSigner signerFrom(Wallet wallet, WalletUtxoSupplier walletUtxoSupplier) { - return transaction -> { - Transaction outputTxn = wallet.sign(transaction, walletUtxoSupplier); + public static TxSigner signerFrom(Wallet wallet) { + return (context, transaction) -> { + var utxos = context.getUtxos(); + Transaction outputTxn = wallet.sign(transaction, utxos); return outputTxn; }; } @@ -52,7 +52,7 @@ public static TxSigner signerFrom(Wallet wallet, WalletUtxoSupplier walletUtxoSu */ public static TxSigner signerFrom(SecretKey... secretKeys) { - return transaction -> { + return (context, transaction) -> { Transaction outputTxn = transaction; for (SecretKey sk : secretKeys) { outputTxn = TransactionSigner.INSTANCE.sign(outputTxn, sk); @@ -69,7 +69,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()) { @@ -88,7 +88,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); @@ -105,7 +105,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); @@ -121,7 +121,7 @@ public static TxSigner stakeKeySignerFrom(Account... signers) { * @return TxSigner function which returns a Transaction 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); @@ -132,7 +132,7 @@ public static TxSigner drepKeySignerFrom(Account... signers) { } public static TxSigner stakeKeySignerFrom(Wallet... wallets) { - return transaction -> { + return (context, transaction) -> { Transaction outputTxn = transaction; for (Wallet wallet : wallets) outputTxn = wallet.signWithStakeKey(outputTxn); @@ -147,7 +147,7 @@ public static TxSigner stakeKeySignerFrom(Wallet... wallets) { * @return TxSigner function which returns a Transaction 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); @@ -164,7 +164,7 @@ public static TxSigner committeeColdKeySignerFrom(Account... signers) { * @return TxSigner function which returns a Transaction 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); From d44473713b76680c67338b1bf3bb0600f3df6fc7 Mon Sep 17 00:00:00 2001 From: Satya Date: Sun, 24 Nov 2024 14:12:08 +0800 Subject: [PATCH 11/16] Normalize mnemonic phrase whitespaces --- .../com/bloxbean/cardano/client/crypto}/MnemonicUtil.java | 2 +- .../bloxbean/cardano/client/crypto/bip39/MnemonicCode.java | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) rename {core/src/main/java/com/bloxbean/cardano/client/common => crypto/src/main/java/com/bloxbean/cardano/client/crypto}/MnemonicUtil.java (96%) diff --git a/core/src/main/java/com/bloxbean/cardano/client/common/MnemonicUtil.java b/crypto/src/main/java/com/bloxbean/cardano/client/crypto/MnemonicUtil.java similarity index 96% rename from core/src/main/java/com/bloxbean/cardano/client/common/MnemonicUtil.java rename to crypto/src/main/java/com/bloxbean/cardano/client/crypto/MnemonicUtil.java index 7e250517..1fbbe4f5 100644 --- a/core/src/main/java/com/bloxbean/cardano/client/common/MnemonicUtil.java +++ b/crypto/src/main/java/com/bloxbean/cardano/client/crypto/MnemonicUtil.java @@ -1,4 +1,4 @@ -package com.bloxbean.cardano.client.common; +package com.bloxbean.cardano.client.crypto; import com.bloxbean.cardano.client.crypto.bip39.MnemonicCode; import com.bloxbean.cardano.client.crypto.bip39.MnemonicException; diff --git a/crypto/src/main/java/com/bloxbean/cardano/client/crypto/bip39/MnemonicCode.java b/crypto/src/main/java/com/bloxbean/cardano/client/crypto/bip39/MnemonicCode.java index ed44abc3..223341fb 100644 --- a/crypto/src/main/java/com/bloxbean/cardano/client/crypto/bip39/MnemonicCode.java +++ b/crypto/src/main/java/com/bloxbean/cardano/client/crypto/bip39/MnemonicCode.java @@ -142,7 +142,9 @@ public static byte[] toSeed(List 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)); } From fdf1c4a14ca7823ece4f880bee2cc1d44ec9c9d9 Mon Sep 17 00:00:00 2001 From: Satya Date: Sun, 24 Nov 2024 14:12:29 +0800 Subject: [PATCH 12/16] Update logging dependency to reload4j in build.gradle --- hd-wallet/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hd-wallet/build.gradle b/hd-wallet/build.gradle index 147d692c..8900a927 100644 --- a/hd-wallet/build.gradle +++ b/hd-wallet/build.gradle @@ -6,7 +6,7 @@ dependencies { api project(':backend') implementation(libs.bouncycastle.bcprov) - integrationTestImplementation(libs.slf4j.log4j) + integrationTestImplementation(libs.slf4j.reload4j) integrationTestImplementation(libs.aiken.java.binding) integrationTestImplementation project(':') integrationTestImplementation project(':backend-modules:blockfrost') From a65b76d684b1ee9d0214de9deb0a8f0a5d2913af Mon Sep 17 00:00:00 2001 From: Satya Date: Sun, 24 Nov 2024 14:13:49 +0800 Subject: [PATCH 13/16] Refactor: Change MnemonicUtil import path --- .../main/java/com/bloxbean/cardano/client/account/Account.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/com/bloxbean/cardano/client/account/Account.java b/core/src/main/java/com/bloxbean/cardano/client/account/Account.java index 8d5611a8..e6d954fd 100644 --- a/core/src/main/java/com/bloxbean/cardano/client/account/Account.java +++ b/core/src/main/java/com/bloxbean/cardano/client/account/Account.java @@ -2,7 +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.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; From 187351f49e67431896f97a27a3e1ad4a26320948 Mon Sep 17 00:00:00 2001 From: Satya Date: Sun, 24 Nov 2024 23:14:43 +0800 Subject: [PATCH 14/16] Refactor Wallet signing and UTXO handling --- .../function/helper/SignerProviders.java | 8 +- .../cardano/hdwallet/QuickTxBaseIT.java | 52 ++++++-- .../cardano/hdwallet/QuickTxBuilderIT.java | 71 ++++++++++- .../bloxbean/cardano/hdwallet/StakeTxIT.java | 62 ++++----- .../com/bloxbean/cardano/hdwallet/Wallet.java | 119 ++++++++++-------- .../cardano/hdwallet/model/WalletUtxo.java | 29 +++++ .../supplier/DefaultWalletUtxoSupplier.java | 46 ++++--- .../hdwallet/supplier/WalletUtxoSupplier.java | 17 +-- 8 files changed, 278 insertions(+), 126 deletions(-) create mode 100644 hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/model/WalletUtxo.java diff --git a/function/src/main/java/com/bloxbean/cardano/client/function/helper/SignerProviders.java b/function/src/main/java/com/bloxbean/cardano/client/function/helper/SignerProviders.java index e5715508..3eeed8f5 100644 --- a/function/src/main/java/com/bloxbean/cardano/client/function/helper/SignerProviders.java +++ b/function/src/main/java/com/bloxbean/cardano/client/function/helper/SignerProviders.java @@ -8,6 +8,9 @@ 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 {@link Transaction} object @@ -39,7 +42,10 @@ public static TxSigner signerFrom(Account... signers) { */ public static TxSigner signerFrom(Wallet wallet) { return (context, transaction) -> { - var utxos = context.getUtxos(); + var utxos = context.getUtxos() + .stream().filter(utxo -> utxo instanceof WalletUtxo) + .map(utxo -> (WalletUtxo) utxo) + .collect(Collectors.toSet()); Transaction outputTxn = wallet.sign(transaction, utxos); return outputTxn; }; diff --git a/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTxBaseIT.java b/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTxBaseIT.java index df6fe250..3d9ca406 100644 --- a/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTxBaseIT.java +++ b/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTxBaseIT.java @@ -11,17 +11,21 @@ import com.bloxbean.cardano.client.backend.model.TransactionContent; import com.bloxbean.cardano.client.util.JsonUtil; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Optional; public class QuickTxBaseIT { + public static final String DEVKIT_ADMIN_BASE_URL = "http://localhost:10000/"; + protected static String BLOCKFROST = "blockfrost"; + protected static String KOIOS = "koios"; + protected static String DEVKIT = "devkit"; + protected static String backendType = DEVKIT; - protected String BLOCKFROST = "blockfrost"; - protected String KOIOS = "koios"; - protected String DEVKIT = "devkit"; - protected String backendType = DEVKIT; - - public BackendService getBackendService() { + public static BackendService getBackendService() { if (BLOCKFROST.equals(backendType)) { String bfProjectId = System.getProperty("BF_PROJECT_ID"); if (bfProjectId == null || bfProjectId.isEmpty()) { @@ -37,10 +41,44 @@ public BackendService getBackendService() { return null; } - public UtxoSupplier getUTXOSupplier() { + public static UtxoSupplier getUTXOSupplier() { return new DefaultUtxoSupplier(getBackendService().getUtxoService()); } + protected static void topUpFund(String address, long adaAmount) { + try { + // URL to the top-up API + String url = DEVKIT_ADMIN_BASE_URL + "local-cluster/api/addresses/topup"; + URL obj = new URL(url); + HttpURLConnection connection = (HttpURLConnection) obj.openConnection(); + + // Set request method to POST + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", "application/json; utf-8"); + connection.setRequestProperty("Accept", "application/json"); + connection.setDoOutput(true); + + // Create JSON payload + String jsonInputString = String.format("{\"address\": \"%s\", \"adaAmount\": %d}", address, adaAmount); + + // Send the request + try (OutputStream os = connection.getOutputStream()) { + byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8); + os.write(input, 0, input.length); + } + + // Check the response code + int responseCode = connection.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_OK) { + System.out.println("Funds topped up successfully."); + } else { + System.out.println("Failed to top up funds. Response code: " + responseCode); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + public void waitForTransaction(Result result) { try { if (result.isSuccessful()) { //Wait for transaction to be mined diff --git a/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTxBuilderIT.java b/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTxBuilderIT.java index b809e0bf..e0d8c8b1 100644 --- a/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTxBuilderIT.java +++ b/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTxBuilderIT.java @@ -1,5 +1,6 @@ package com.bloxbean.cardano.hdwallet; +import com.bloxbean.cardano.client.account.Account; import com.bloxbean.cardano.client.api.UtxoSupplier; import com.bloxbean.cardano.client.api.model.Amount; import com.bloxbean.cardano.client.api.model.Result; @@ -16,7 +17,10 @@ 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.client.util.JsonUtil; +import com.bloxbean.cardano.hdwallet.model.WalletUtxo; import com.bloxbean.cardano.hdwallet.supplier.DefaultWalletUtxoSupplier; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -24,6 +28,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Random; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -35,6 +40,17 @@ public class QuickTxBuilderIT extends QuickTxBaseIT { Wallet wallet1; Wallet wallet2; + static Account topupAccount; + + @BeforeAll + static void beforeAll() { + String topupAccountMnemonic = "weapon news intact viable rigid hope ginger defy remove enemy dog volume belt clay shuffle angle crunch eye end asthma arctic sphere arm limit"; + topupAccount = new Account(Networks.testnet(), topupAccountMnemonic); + + topUpFund(topupAccount.baseAddress(), BigInteger.valueOf(100000).longValue()); + System.out.println("Topup address : " + topupAccount.baseAddress()); + } + @BeforeEach void setup() { backendService = getBackendService(); @@ -44,6 +60,8 @@ void setup() { 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); + + walletUtxoSupplier = new DefaultWalletUtxoSupplier(backendService.getUtxoService(), wallet1); } @Test @@ -52,16 +70,22 @@ void simplePayment() { metadata.put(BigInteger.valueOf(100), "This is first metadata"); metadata.putNegative(200, -900); + //topup wallet + splitPaymentBetweenAddress(topupAccount, wallet1, 20, Double.valueOf(50000)); + 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)) + .payToAddress(wallet2.getBaseAddress(0).getAddress(), Amount.ada(50000)) .from(wallet1); Result result = quickTxBuilder.compose(tx) - .withSigner(wallet1) + .withSigner(SignerProviders.signerFrom(wallet1)) + .withTxInspector( txn -> { + System.out.println(JsonUtil.getPrettyJson(txn)); + }) .complete(); System.out.println(result); @@ -85,7 +109,7 @@ void minting() throws CborSerializationException { UtxoSupplier walletUtxoSupplier = new DefaultWalletUtxoSupplier(backendService.getUtxoService(), wallet1); QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService, walletUtxoSupplier); Result result = quickTxBuilder.compose(tx) - .withSigner(wallet1) + .withSigner(SignerProviders.signerFrom(wallet1)) .withSigner(SignerProviders.signerFrom(policy)) .complete(); @@ -98,7 +122,7 @@ void minting() throws CborSerializationException { @Test void utxoTest() { - List utxos = walletUtxoSupplier.getAll(wallet1); + List utxos = walletUtxoSupplier.getAll(); Map amountMap = new HashMap<>(); for (Utxo utxo : utxos) { int totalAmount = 0; @@ -112,4 +136,41 @@ void utxoTest() { assertTrue(!utxos.isEmpty()); } -} \ No newline at end of file + + void splitPaymentBetweenAddress(Account topupAccount, Wallet receiverWallet, int totalAddresses, Double adaAmount) { + // Create an amount array with no of totalAddresses with random distribution of split amounts + Double[] amounts = new Double[totalAddresses]; + Double remainingAmount = adaAmount; + Random rand = new Random(); + + for (int i = 0; i < totalAddresses - 1; i++) { + Double randomAmount = Double.valueOf(rand.nextInt(remainingAmount.intValue())); + amounts[i] = randomAmount; + remainingAmount = remainingAmount - randomAmount; + } + amounts[totalAddresses - 1] = remainingAmount; + + String[] addresses = new String[totalAddresses]; + Random random = new Random(); + int currentIndex = 0; + + for (int i = 0; i < totalAddresses; i++) { + addresses[i] = receiverWallet.getBaseAddressString(currentIndex); + currentIndex += random.nextInt(20) + 1; + } + + Tx tx = new Tx(); + for (int i= 0; i < addresses.length; i++) { + tx.payToAddress(addresses[i], Amount.ada(amounts[i])); + } + + tx.from(topupAccount.baseAddress()); + + var result = new QuickTxBuilder(backendService) + .compose(tx) + .withSigner(SignerProviders.signerFrom(topupAccount)) + .completeAndWait(); + + System.out.println(result); + } +} diff --git a/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/StakeTxIT.java b/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/StakeTxIT.java index 5d4f39b9..174a5d9f 100644 --- a/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/StakeTxIT.java +++ b/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/StakeTxIT.java @@ -25,36 +25,33 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.*; -import static com.bloxbean.cardano.client.common.ADAConversionUtil.adaToLovelace; -import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class StakeTxIT extends QuickTxBaseIT { - BackendService backendService; - UtxoSupplier utxoSupplier; - WalletUtxoSupplier walletUtxoSupplier; - Wallet wallet1; - Wallet wallet2; + static BackendService backendService; + static UtxoSupplier utxoSupplier; + static WalletUtxoSupplier walletUtxoSupplier; + static Wallet wallet1; + static Wallet wallet2; - String poolId; - ProtocolParamsSupplier protocolParamsSupplier; + static String poolId; + static ProtocolParamsSupplier protocolParamsSupplier; - String aikenCompiledCode1 = "581801000032223253330043370e00290010a4c2c6eb40095cd1"; //redeemer = 1 - PlutusScript plutusScript1 = PlutusBlueprintUtil.getPlutusScriptFromCompiledCode(aikenCompiledCode1, PlutusVersion.v2); + static String aikenCompiledCode1 = "581801000032223253330043370e00290010a4c2c6eb40095cd1"; //redeemer = 1 + static PlutusScript plutusScript1 = PlutusBlueprintUtil.getPlutusScriptFromCompiledCode(aikenCompiledCode1, PlutusVersion.v2); - String aikenCompileCode2 = "581801000032223253330043370e00290020a4c2c6eb40095cd1"; //redeemer = 2 - PlutusScript plutusScript2 = PlutusBlueprintUtil.getPlutusScriptFromCompiledCode(aikenCompileCode2, PlutusVersion.v2); + static String aikenCompileCode2 = "581801000032223253330043370e00290020a4c2c6eb40095cd1"; //redeemer = 2 + static PlutusScript plutusScript2 = PlutusBlueprintUtil.getPlutusScriptFromCompiledCode(aikenCompileCode2, PlutusVersion.v2); - String scriptStakeAddress1 = AddressProvider.getRewardAddress(plutusScript1, Networks.testnet()).toBech32(); - String scriptStakeAddress2 = AddressProvider.getRewardAddress(plutusScript2, Networks.testnet()).toBech32(); + static String scriptStakeAddress1 = AddressProvider.getRewardAddress(plutusScript1, Networks.testnet()).toBech32(); + static String scriptStakeAddress2 = AddressProvider.getRewardAddress(plutusScript2, Networks.testnet()).toBech32(); + static QuickTxBuilder quickTxBuilder; + static ObjectMapper objectMapper = new ObjectMapper(); - QuickTxBuilder quickTxBuilder; - ObjectMapper objectMapper = new ObjectMapper(); - - @BeforeEach - void setup() { + @BeforeAll + static void beforeAll() { backendService = getBackendService(); utxoSupplier = getUTXOSupplier(); @@ -71,6 +68,12 @@ void setup() { } else { poolId = "pool1vqq4hdwrh442u97e2jh6k4xuscs3x5mqjjrn8daj36y7gt2rj85"; } + + topUpFund(wallet1.getBaseAddressString(0), 10000L); + } + + @BeforeEach + void setup() { } @Test @@ -91,7 +94,7 @@ void stakeAddressRegistration() { .from(wallet1); Result result = quickTxBuilder.compose(tx) - .withSigner(wallet1) + .withSigner(SignerProviders.signerFrom(wallet1)) .withTxInspector((txn) -> System.out.println(JsonUtil.getPrettyJson(txn))) .completeAndWait(msg -> System.out.println(msg)); @@ -114,7 +117,7 @@ void stakeAddressDeRegistration() { .from(wallet1); Result result = quickTxBuilder.compose(tx) - .withSigner(wallet1) + .withSigner(SignerProviders.signerFrom(wallet1)) .withSigner(SignerProviders.stakeKeySignerFrom(wallet1)) .withTxInspector((txn) -> System.out.println(JsonUtil.getPrettyJson(txn))) .completeAndWait(msg -> System.out.println(msg)); @@ -136,7 +139,7 @@ void stakeAddressRegistration_onlyRegistration() { .from(wallet1); Result result = quickTxBuilder.compose(tx) - .withSigner(wallet1) + .withSigner(SignerProviders.signerFrom(wallet1)) .withTxInspector((txn) -> System.out.println(JsonUtil.getPrettyJson(txn))) .completeAndWait(msg -> System.out.println(msg)); @@ -157,7 +160,7 @@ void stakeAddressDeRegistration_onlyRegistration() { .from(wallet1); Result result = quickTxBuilder.compose(tx) - .withSigner(wallet1) + .withSigner(SignerProviders.signerFrom(wallet1)) .withSigner(SignerProviders.stakeKeySignerFrom(wallet1)) .withTxInspector((txn) -> System.out.println(JsonUtil.getPrettyJson(txn))) .completeAndWait(msg -> System.out.println(msg)); @@ -180,7 +183,7 @@ void scriptStakeAddress_registration() { .from(wallet1); Result result = quickTxBuilder.compose(tx) - .withSigner(wallet1) + .withSigner(SignerProviders.signerFrom(wallet1)) .withTxInspector((txn) -> System.out.println(JsonUtil.getPrettyJson(txn))) .completeAndWait(msg -> System.out.println(msg)); @@ -202,7 +205,7 @@ void scriptStakeAddress_deRegistration() { Result result = quickTxBuilder.compose(tx) .feePayer(wallet1.getBaseAddressString(0)) - .withSigner(wallet1) + .withSigner(SignerProviders.signerFrom(wallet1)) .withTxInspector((txn) -> System.out.println(JsonUtil.getPrettyJson(txn))) .withTxEvaluator(!backendType.equals(BLOCKFROST) ? new AikenTransactionEvaluator(utxoSupplier, protocolParamsSupplier) : null) @@ -230,7 +233,7 @@ void stakeDelegation_scriptStakeKeys() { Result delgResult = quickTxBuilder.compose(delegTx) .feePayer(wallet1.getBaseAddressString(0)) - .withSigner(wallet1) + .withSigner(SignerProviders.signerFrom(wallet1)) .withTxEvaluator(!backendType.equals(BLOCKFROST) ? new AikenTransactionEvaluator(utxoSupplier, protocolParamsSupplier) : null) .withTxInspector((txn) -> System.out.println(JsonUtil.getPrettyJson(txn))) @@ -255,7 +258,7 @@ private void registerScriptsStakeKeys() { .from(wallet1); Result result = quickTxBuilder.compose(tx) - .withSigner(wallet1) + .withSigner(SignerProviders.signerFrom(wallet1)) .withTxInspector((txn) -> System.out.println(JsonUtil.getPrettyJson(txn))) .completeAndWait(msg -> System.out.println(msg)); @@ -273,7 +276,7 @@ private void deregisterScriptsStakeKeys() { Result result = quickTxBuilder.compose(tx) .feePayer(wallet1.getBaseAddressString(0)) - .withSigner(wallet1) + .withSigner(SignerProviders.signerFrom(wallet1)) .withTxEvaluator(!backendType.equals(BLOCKFROST) ? new AikenTransactionEvaluator(utxoSupplier, protocolParamsSupplier) : null) .withTxInspector((txn) -> System.out.println(JsonUtil.getPrettyJson(txn))) @@ -296,7 +299,6 @@ private Result _deRegisterStakeKeys() { .from(wallet1); Result result = quickTxBuilder.compose(tx) - .withSigner(wallet1) .withSigner(SignerProviders.stakeKeySignerFrom(wallet1)) .withSigner(SignerProviders.stakeKeySignerFrom(wallet2)) .withTxInspector((txn) -> System.out.println(JsonUtil.getPrettyJson(txn))) diff --git a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/Wallet.java b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/Wallet.java index 8849946d..dfcc07ea 100644 --- a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/Wallet.java +++ b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/Wallet.java @@ -3,10 +3,9 @@ import com.bloxbean.cardano.client.account.Account; import com.bloxbean.cardano.client.address.Address; import com.bloxbean.cardano.client.address.AddressProvider; -import com.bloxbean.cardano.client.api.model.Utxo; -import com.bloxbean.cardano.client.common.MnemonicUtil; import com.bloxbean.cardano.client.common.model.Network; import com.bloxbean.cardano.client.common.model.Networks; +import com.bloxbean.cardano.client.crypto.MnemonicUtil; import com.bloxbean.cardano.client.crypto.bip32.HdKeyGenerator; import com.bloxbean.cardano.client.crypto.bip32.HdKeyPair; import com.bloxbean.cardano.client.crypto.bip39.MnemonicCode; @@ -16,13 +15,17 @@ import com.bloxbean.cardano.client.crypto.cip1852.DerivationPath; import com.bloxbean.cardano.client.transaction.TransactionSigner; import com.bloxbean.cardano.client.transaction.spec.Transaction; -import com.bloxbean.cardano.client.transaction.spec.TransactionInput; -import com.bloxbean.cardano.hdwallet.supplier.WalletUtxoSupplier; +import com.bloxbean.cardano.hdwallet.model.WalletUtxo; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Getter; import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; +/** + * The Wallet class represents wallet with functionalities to manage accounts, addresses. + */ public class Wallet { @Getter @@ -31,7 +34,6 @@ public class Wallet { private final Network network; @Getter private final String mnemonic; - private static final int INDEX_SEARCH_RANGE = 20; private String stakeAddress; private Map cache; private HdKeyPair rootKeys; @@ -184,57 +186,74 @@ public HdKeyPair getHDWalletKeyPair() { * @param txToSign * @return signed Transaction */ - public Transaction sign(Transaction txToSign, WalletUtxoSupplier utxoSupplier) { - List signers = getSignersForTransaction(txToSign, utxoSupplier); - - if(signers.isEmpty()) + public Transaction sign(Transaction txToSign, Set utxos) { + Map accountMap = utxos.stream() + .map(WalletUtxo::getDerivationPath) + .filter(Objects::nonNull) + .map(derivationPath -> getAccountObject( + derivationPath.getAccount().getValue(), + derivationPath.getIndex().getValue())) + .collect(Collectors.toMap( + Account::baseAddress, + Function.identity(), + (existing, replacement) -> existing)); // Handle duplicates if necessary + + var accounts = accountMap.values(); + + if(accounts.isEmpty()) throw new RuntimeException("No signers found!"); - for (Account signer : signers) txToSign = signer.sign(txToSign); + for (Account account : accounts) + txToSign = account.sign(txToSign); return txToSign; } - /** - * Returns a list with signers needed for this transaction - * - * @param tx - * @param utxoSupplier - * @return - */ - public List getSignersForTransaction(Transaction tx, WalletUtxoSupplier utxoSupplier) { - return getSignersForInputs(tx.getBody().getInputs(), utxoSupplier); - } - - private List getSignersForInputs(List inputs, WalletUtxoSupplier utxoSupplier) { - // searching for address to sign - List signers = new ArrayList<>(); - List remaining = new ArrayList<>(inputs); - - int index = 0; - int emptyCounter = 0; - while (!remaining.isEmpty() || emptyCounter >= INDEX_SEARCH_RANGE) { - List utxos = utxoSupplier.getUtxosForAccountAndIndex(this, this.account, index); - emptyCounter = utxos.isEmpty() ? emptyCounter + 1 : 0; - - for (Utxo utxo : utxos) { - if(matchUtxoWithInputs(inputs, utxo, signers, index, remaining)) - break; - } - index++; - } - return signers; - } - - private boolean matchUtxoWithInputs(List inputs, Utxo utxo, List signers, int index, List remaining) { - for (TransactionInput input : inputs) { - if(utxo.getTxHash().equals(input.getTransactionId()) && utxo.getOutputIndex() == input.getIndex()) { - signers.add(getAccountObject(index)); - remaining.remove(input); - } - } - return remaining.isEmpty(); - } +// +// /** +// * Returns a list with signers needed for this transaction +// * +// * @param tx +// * @param utxoSupplier +// * @return +// */ +// public List getSignersForTransaction(Transaction tx, WalletUtxoSupplier utxoSupplier) { +// return getSignersForInputs(tx.getBody().getInputs(), utxoSupplier); +// } +// +// private List getSignersForInputs(List inputs, WalletUtxoSupplier utxoSupplier) { +// // searching for address to sign +// List signers = new ArrayList<>(); +// List remaining = new ArrayList<>(inputs); +// +// int index = 0; +// int emptyCounter = 0; +// while (!remaining.isEmpty() || emptyCounter >= INDEX_SEARCH_RANGE) { +// List utxos = utxoSupplier.getUtxosForAccountAndIndex(this.account, index); +// emptyCounter = utxos.isEmpty() ? emptyCounter + 1 : 0; +// +// for (Utxo utxo : utxos) { +// if(matchUtxoWithInputs(inputs, utxo, signers, index, remaining)) +// break; +// } +// index++; +// } +// return signers; +// } +// +// private boolean matchUtxoWithInputs(List inputs, Utxo utxo, List signers, int index, List remaining) { +// for (TransactionInput input : inputs) { +// if(utxo.getTxHash().equals(input.getTransactionId()) && utxo.getOutputIndex() == input.getIndex()) { +// var account = getAccountObject(index); +// var accNotFound = signers.stream() +// .noneMatch(acc -> account.baseAddress().equals(acc.baseAddress())); +// if (accNotFound) +// signers.add(getAccountObject(index)); +// remaining.remove(input); +// } +// } +// return remaining.isEmpty(); +// } /** * Returns the stake address of the wallet. diff --git a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/model/WalletUtxo.java b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/model/WalletUtxo.java new file mode 100644 index 00000000..94a7962f --- /dev/null +++ b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/model/WalletUtxo.java @@ -0,0 +1,29 @@ +package com.bloxbean.cardano.hdwallet.model; + +import com.bloxbean.cardano.client.api.model.Utxo; +import com.bloxbean.cardano.client.crypto.cip1852.DerivationPath; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class WalletUtxo extends Utxo { + private DerivationPath derivationPath; + + public static WalletUtxo from(Utxo utxo) { + WalletUtxo walletUtxo = new WalletUtxo(); + walletUtxo.setTxHash(utxo.getTxHash()); + walletUtxo.setOutputIndex(utxo.getOutputIndex()); + walletUtxo.setAddress(utxo.getAddress()); + walletUtxo.setAmount(utxo.getAmount()); + walletUtxo.setDataHash(utxo.getDataHash()); + walletUtxo.setInlineDatum(utxo.getInlineDatum()); + walletUtxo.setReferenceScriptHash(utxo.getReferenceScriptHash()); + return walletUtxo; + } +} diff --git a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/supplier/DefaultWalletUtxoSupplier.java b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/supplier/DefaultWalletUtxoSupplier.java index 26cd66bb..216da3c7 100644 --- a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/supplier/DefaultWalletUtxoSupplier.java +++ b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/supplier/DefaultWalletUtxoSupplier.java @@ -7,30 +7,30 @@ import com.bloxbean.cardano.client.api.model.Result; import com.bloxbean.cardano.client.api.model.Utxo; import com.bloxbean.cardano.client.backend.api.UtxoService; +import com.bloxbean.cardano.client.crypto.cip1852.DerivationPath; +import com.bloxbean.cardano.client.crypto.cip1852.Segment; import com.bloxbean.cardano.hdwallet.Wallet; +import com.bloxbean.cardano.hdwallet.model.WalletUtxo; import lombok.Setter; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; public class DefaultWalletUtxoSupplier implements WalletUtxoSupplier { + private static final int INDEX_SEARCH_RANGE = 20; // according to specifications private final UtxoService utxoService; @Setter private Wallet wallet; - private static final int INDEX_SEARCH_RANGE = 20; // according to specifications public DefaultWalletUtxoSupplier(UtxoService utxoService, Wallet wallet) { this.utxoService = utxoService; this.wallet = wallet; } - public DefaultWalletUtxoSupplier(UtxoService utxoService) { - this.utxoService = utxoService; - } - @Override public List getPage(String address, Integer nrOfItems, Integer page, OrderEnum order) { return getAll(address); // todo get Page of utxo over multipe addresses - find a good way to aktually do something with page, nrOfItems and order @@ -40,7 +40,9 @@ public List getPage(String address, Integer nrOfItems, Integer page, Order public Optional getTxOutput(String txHash, int outputIndex) { try { var result = utxoService.getTxOutput(txHash, outputIndex); - return result != null && result.getValue() != null ? Optional.of(result.getValue()) : Optional.empty(); + return result != null && result.getValue() != null + ? Optional.of(WalletUtxo.from(result.getValue())) + : Optional.empty(); } catch (ApiException e) { throw new ApiRuntimeException(e); } @@ -49,16 +51,17 @@ public Optional getTxOutput(String txHash, int outputIndex) { @Override public List getAll(String address) { checkIfWalletIsSet(); - return getAll(wallet); + return new ArrayList<>(getAll()); } @Override - public List getAll(Wallet wallet) { + public List getAll() { int index = 0; int noUtxoFound = 0; - List utxos = new ArrayList<>(); + List utxos = new ArrayList<>(); while(noUtxoFound < INDEX_SEARCH_RANGE) { - List utxoFromIndex = getUtxosForAccountAndIndex(wallet, wallet.getAccount(), index); + List utxoFromIndex = getUtxosForAccountAndIndex(wallet.getAccount(), index); + utxos.addAll(utxoFromIndex); noUtxoFound = utxoFromIndex.isEmpty() ? noUtxoFound + 1 : 0; @@ -68,9 +71,10 @@ public List getAll(Wallet wallet) { } @Override - public List getUtxosForAccountAndIndex(Wallet wallet, int account, int index) { + public List getUtxosForAccountAndIndex(int account, int index) { + checkIfWalletIsSet(); String address = wallet.getBaseAddress(account, index).getAddress(); - List utxos = new ArrayList<>(); + List utxos = new ArrayList<>(); int page = 1; while(true) { Result> result = null; @@ -80,7 +84,17 @@ public List getUtxosForAccountAndIndex(Wallet wallet, int account, int ind throw new ApiRuntimeException(e); } List utxoPage = result != null && result.getValue() != null ? result.getValue() : Collections.emptyList(); - utxos.addAll(utxoPage); + + DerivationPath derivationPath = DerivationPath.createExternalAddressDerivationPathForAccount(account); + derivationPath.setIndex(Segment.builder().value(index).build()); + + var utxoList = utxoPage.stream().map(utxo -> { + var walletUtxo = WalletUtxo.from(utxo); + walletUtxo.setDerivationPath(derivationPath); + return walletUtxo; + }).collect(Collectors.toList()); + + utxos.addAll(utxoList); if(utxoPage.size() < 100) break; page++; @@ -88,12 +102,6 @@ public List getUtxosForAccountAndIndex(Wallet wallet, int account, int ind return utxos; } - @Override - public List getUtxosForAccountAndIndex(int account, int index) { - checkIfWalletIsSet(); - return getUtxosForAccountAndIndex(this.wallet, account, index); - } - private void checkIfWalletIsSet() { if(this.wallet == null) throw new RuntimeException("Wallet has to be provided!"); diff --git a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/supplier/WalletUtxoSupplier.java b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/supplier/WalletUtxoSupplier.java index 6a8259a8..2aa5b260 100644 --- a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/supplier/WalletUtxoSupplier.java +++ b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/supplier/WalletUtxoSupplier.java @@ -1,8 +1,7 @@ package com.bloxbean.cardano.hdwallet.supplier; import com.bloxbean.cardano.client.api.UtxoSupplier; -import com.bloxbean.cardano.client.api.model.Utxo; -import com.bloxbean.cardano.hdwallet.Wallet; +import com.bloxbean.cardano.hdwallet.model.WalletUtxo; import java.util.List; @@ -10,19 +9,9 @@ public interface WalletUtxoSupplier extends UtxoSupplier { /** * Returns all Utxos for provided wallets - * @param wallet * @return */ - List getAll(Wallet wallet); - - /** - * Returns all UTXOs for a specific address of a wallet m/1852'/1815'/{account}'/0/{index} - * @param wallet - * @param account - * @param index - * @return - */ - List getUtxosForAccountAndIndex(Wallet wallet, int account, int index); + List getAll(); /** * Returns all UTXOs for a specific address m/1852'/1815'/{account}'/0/{index} @@ -30,5 +19,5 @@ public interface WalletUtxoSupplier extends UtxoSupplier { * @param index * @return */ - List getUtxosForAccountAndIndex(int account, int index); + List getUtxosForAccountAndIndex(int account, int index); } From 5316e909503bcf9276d9dd8f5676758babf4240c Mon Sep 17 00:00:00 2001 From: Satya Date: Mon, 25 Nov 2024 12:21:19 +0800 Subject: [PATCH 15/16] 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. --- .../function/helper/SignerProvidersTest.java | 10 +- .../cardano/hdwallet/QuickTxBuilderIT.java | 42 ++++- .../com/bloxbean/cardano/hdwallet/Wallet.java | 39 ++-- .../supplier/DefaultWalletUtxoSupplier.java | 25 ++- .../bloxbean/cardano/hdwallet/WalletTest.java | 11 +- .../DefaultWalletUtxoSupplierTest.java | 171 ++++++++++++++++++ 6 files changed, 250 insertions(+), 48 deletions(-) create mode 100644 hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/supplier/DefaultWalletUtxoSupplierTest.java diff --git a/function/src/test/java/com/bloxbean/cardano/client/function/helper/SignerProvidersTest.java b/function/src/test/java/com/bloxbean/cardano/client/function/helper/SignerProvidersTest.java index 525536dc..a411c53a 100644 --- a/function/src/test/java/com/bloxbean/cardano/client/function/helper/SignerProvidersTest.java +++ b/function/src/test/java/com/bloxbean/cardano/client/function/helper/SignerProvidersTest.java @@ -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); } @@ -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); } @@ -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); } @@ -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); } @@ -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); } diff --git a/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTxBuilderIT.java b/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTxBuilderIT.java index e0d8c8b1..ccd0055d 100644 --- a/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTxBuilderIT.java +++ b/hd-wallet/src/it/java/com/bloxbean/cardano/hdwallet/QuickTxBuilderIT.java @@ -47,7 +47,8 @@ static void beforeAll() { String topupAccountMnemonic = "weapon news intact viable rigid hope ginger defy remove enemy dog volume belt clay shuffle angle crunch eye end asthma arctic sphere arm limit"; topupAccount = new Account(Networks.testnet(), topupAccountMnemonic); - topUpFund(topupAccount.baseAddress(), BigInteger.valueOf(100000).longValue()); + topUpFund(topupAccount.baseAddress(), 100000); + topUpFund("addr_test1qz5t8wq55e09usmh07ymxry8atzwxwt2nwwzfngg6esffxvw2pfap6uqmkj3n6zmlrsgz397md2gt7yqs5p255uygaesx608y5", 5); System.out.println("Topup address : " + topupAccount.baseAddress()); } @@ -83,7 +84,38 @@ void simplePayment() { Result result = quickTxBuilder.compose(tx) .withSigner(SignerProviders.signerFrom(wallet1)) - .withTxInspector( txn -> { + .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()); + } + + @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.setIndexesToScan(new int[]{5, 30, 45}); + + //topup index 5, 45 + topUpFund(wallet.getBaseAddressString(5), 5); + topUpFund(wallet.getBaseAddressString(45), 15); + + UtxoSupplier walletUtxoSupplier = new DefaultWalletUtxoSupplier(backendService.getUtxoService(), wallet); + QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService, walletUtxoSupplier); + + Tx tx = new Tx() + .payToAddress(wallet2.getBaseAddress(0).getAddress(), Amount.ada(18)) + .from(wallet); + + Result result = quickTxBuilder.compose(tx) + .withSigner(SignerProviders.signerFrom(wallet)) + .withTxInspector(txn -> { System.out.println(JsonUtil.getPrettyJson(txn)); }) .complete(); @@ -126,10 +158,10 @@ void utxoTest() { Map amountMap = new HashMap<>(); for (Utxo utxo : utxos) { int totalAmount = 0; - if(amountMap.containsKey(utxo.getAddress())) { + 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(); + totalAmount = amount + utxo.getAmount().get(0).getQuantity().intValue(); } amountMap.put(utxo.getAddress(), totalAmount); } @@ -160,7 +192,7 @@ void splitPaymentBetweenAddress(Account topupAccount, Wallet receiverWallet, int } Tx tx = new Tx(); - for (int i= 0; i < addresses.length; i++) { + for (int i = 0; i < addresses.length; i++) { tx.payToAddress(addresses[i], Amount.ada(amounts[i])); } diff --git a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/Wallet.java b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/Wallet.java index dfcc07ea..1ec01c77 100644 --- a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/Wallet.java +++ b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/Wallet.java @@ -18,6 +18,7 @@ import com.bloxbean.cardano.hdwallet.model.WalletUtxo; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Getter; +import lombok.Setter; import java.util.*; import java.util.function.Function; @@ -39,6 +40,14 @@ public class Wallet { private HdKeyPair rootKeys; private HdKeyPair stakeKeys; + @Getter + @Setter + private int[] indexesToScan; //If set, only scan these indexes and avoid gap limit during address scanning + + @Getter + @Setter + private int gapLimit = 20; //No of unused addresses to scan. + public Wallet() { this(Networks.mainnet()); } @@ -75,7 +84,7 @@ public Wallet(Network network, String mnemonic, int account) { } /** - * Get Enterpriseaddress for current account. Account can be changed via the setter. + * Get Enterprise address for current account. Account can be changed via the setter. * @param index * @return */ @@ -84,13 +93,13 @@ public Address getEntAddress(int index) { } /** - * Get Enterpriseaddress for derivationpath m/1852'/1815'/{account}'/0/{index} + * Get Enterprise address for derivation path m/1852'/1815'/{account}'/0/{index} * @param account * @param index * @return */ private Address getEntAddress(int account, int index) { - return getAccountObject(account, index).getEnterpriseAddress(); + return getAccount(account, index).getEnterpriseAddress(); } /** @@ -118,7 +127,7 @@ public String getBaseAddressString(int index) { * @return */ public Address getBaseAddress(int account, int index) { - return getAccountObject(account,index).getBaseAddress(); + return getAccount(account,index).getBaseAddress(); } /** @@ -126,8 +135,8 @@ public Address getBaseAddress(int account, int index) { * @param index * @return */ - public Account getAccountObject(int index) { - return getAccountObject(this.account, index); + public Account getAccount(int index) { + return getAccount(this.account, index); } /** @@ -136,7 +145,7 @@ public Account getAccountObject(int index) { * @param index * @return */ - public Account getAccountObject(int account, int index) { + public Account getAccount(int account, int index) { if(account != this.account) { DerivationPath derivationPath = DerivationPath.createExternalAddressDerivationPathForAccount(account); derivationPath.getIndex().setValue(index); @@ -167,7 +176,8 @@ public void setAccount(int account) { * Returns the RootkeyPair * @return */ - public HdKeyPair getHDWalletKeyPair() { + @JsonIgnore + public HdKeyPair getRootKeyPair() { if(rootKeys == null) { HdKeyGenerator hdKeyGenerator = new HdKeyGenerator(); try { @@ -190,7 +200,7 @@ public Transaction sign(Transaction txToSign, Set utxos) { Map accountMap = utxos.stream() .map(WalletUtxo::getDerivationPath) .filter(Objects::nonNull) - .map(derivationPath -> getAccountObject( + .map(derivationPath -> getAccount( derivationPath.getAccount().getValue(), derivationPath.getIndex().getValue())) .collect(Collectors.toMap( @@ -280,20 +290,9 @@ public Transaction signWithStakeKey(Transaction transaction) { private HdKeyPair getStakeKeyPair() { if(stakeKeys == null) { DerivationPath stakeDerivationPath = DerivationPath.createStakeAddressDerivationPathForAccount(this.account); -// if (mnemonic == null || mnemonic.trim().length() == 0) { -// hdKeyPair = new CIP1852().getKeyPairFromAccountKey(this.accountKey, stakeDerivationPath); // TODO need to implement creation from key -// } else { -// hdKeyPair = new CIP1852().getKeyPairFromMnemonic(mnemonic, stakeDerivationPath); -// } stakeKeys = new CIP1852().getKeyPairFromMnemonic(mnemonic, stakeDerivationPath); } return stakeKeys; } - - @JsonIgnore - public String getBech32PrivateKey() { - HdKeyPair hdKeyPair = getHDWalletKeyPair(); - return hdKeyPair.getPrivateKey().toBech32(); - } } diff --git a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/supplier/DefaultWalletUtxoSupplier.java b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/supplier/DefaultWalletUtxoSupplier.java index 216da3c7..515bf76c 100644 --- a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/supplier/DefaultWalletUtxoSupplier.java +++ b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/supplier/DefaultWalletUtxoSupplier.java @@ -20,8 +20,6 @@ import java.util.stream.Collectors; public class DefaultWalletUtxoSupplier implements WalletUtxoSupplier { - private static final int INDEX_SEARCH_RANGE = 20; // according to specifications - private final UtxoService utxoService; @Setter private Wallet wallet; @@ -56,16 +54,25 @@ public List getAll(String address) { @Override public List getAll() { - int index = 0; - int noUtxoFound = 0; List utxos = new ArrayList<>(); - while(noUtxoFound < INDEX_SEARCH_RANGE) { - List utxoFromIndex = getUtxosForAccountAndIndex(wallet.getAccount(), index); - utxos.addAll(utxoFromIndex); - noUtxoFound = utxoFromIndex.isEmpty() ? noUtxoFound + 1 : 0; + if (wallet.getIndexesToScan() == null || wallet.getIndexesToScan().length == 0) { + int index = 0; + int noUtxoFound = 0; + + while (noUtxoFound < wallet.getGapLimit()) { + List utxoFromIndex = getUtxosForAccountAndIndex(wallet.getAccount(), index); + + utxos.addAll(utxoFromIndex); + noUtxoFound = utxoFromIndex.isEmpty() ? noUtxoFound + 1 : 0; - index++; // increasing search index + index++; // increasing search index + } + } else { + for (int idx: wallet.getIndexesToScan()) { + List utxoFromIndex = getUtxosForAccountAndIndex(wallet.getAccount(), idx); + utxos.addAll(utxoFromIndex); + } } return utxos; } diff --git a/hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/WalletTest.java b/hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/WalletTest.java index 48f90ec0..10183505 100644 --- a/hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/WalletTest.java +++ b/hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/WalletTest.java @@ -85,22 +85,15 @@ public void testGetEnterpriseAddressFromMnemonicIndexByNetwork() { Assertions.assertEquals(testnetEntAddress2, wallet.getEntAddress(2).getAddress()); } - @Test - public void testGetPrivateKeyFromMnemonic() { - String pvtKey = new Wallet(phrase24W).getBech32PrivateKey(); - System.out.println(pvtKey); - Assertions.assertTrue(pvtKey.length() > 5); - } - @Test public void testGetPublicKeyBytesFromMnemonic() { - byte[] pubKey = new Wallet(phrase24W).getHDWalletKeyPair().getPublicKey().getKeyData(); + byte[] pubKey = new Wallet(phrase24W).getRootKeyPair().getPublicKey().getKeyData(); Assertions.assertEquals(32, pubKey.length); } @Test public void testGetPrivateKeyBytesFromMnemonic() { - byte[] pvtKey = new Wallet(phrase24W).getHDWalletKeyPair().getPrivateKey().getBytes(); + byte[] pvtKey = new Wallet(phrase24W).getRootKeyPair().getPrivateKey().getBytes(); Assertions.assertEquals(96, pvtKey.length); } diff --git a/hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/supplier/DefaultWalletUtxoSupplierTest.java b/hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/supplier/DefaultWalletUtxoSupplierTest.java new file mode 100644 index 00000000..7aafeb80 --- /dev/null +++ b/hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/supplier/DefaultWalletUtxoSupplierTest.java @@ -0,0 +1,171 @@ +package com.bloxbean.cardano.hdwallet.supplier; + +import com.bloxbean.cardano.client.api.common.OrderEnum; +import com.bloxbean.cardano.client.api.exception.ApiException; +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.backend.api.UtxoService; +import com.bloxbean.cardano.hdwallet.Wallet; +import com.bloxbean.cardano.hdwallet.model.WalletUtxo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import java.math.BigInteger; +import java.util.List; +import java.util.stream.Collectors; + +import static com.bloxbean.cardano.client.common.CardanoConstants.LOVELACE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class DefaultWalletUtxoSupplierTest { + + @Mock + private UtxoService utxoService; + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + void getAll() throws ApiException { + Wallet wallet = new Wallet(); + var addr1 = wallet.getAccount(3).baseAddress(); + var addr2 = wallet.getAccount(7).baseAddress(); + var addr3 = wallet.getAccount(25).baseAddress(); + var addr4 = wallet.getAccount(50).baseAddress(); + + DefaultWalletUtxoSupplier utxoSupplier = new DefaultWalletUtxoSupplier(utxoService, wallet); + + given(utxoService.getUtxos(addr1, 100, 1, OrderEnum.asc)) + .willReturn(Result.success("ok").withValue(List.of( + Utxo.builder() + .address(addr1) + .txHash("tx1") + .outputIndex(0) + .amount(List.of(Amount.builder() + .quantity(BigInteger.valueOf(100)) + .unit(LOVELACE) + .build())).build() + ))); + + given(utxoService.getUtxos(addr2, 100, 1, OrderEnum.asc)) + .willReturn(Result.success("ok").withValue(List.of( + Utxo.builder() + .address(addr2) + .txHash("tx2") + .outputIndex(0) + .amount(List.of(Amount.builder() + .quantity(BigInteger.valueOf(200)) + .unit(LOVELACE) + .build())).build() + ))); + given(utxoService.getUtxos(addr3, 100, 1, OrderEnum.asc)) + .willReturn(Result.success("ok").withValue(List.of( + Utxo.builder() + .address(addr3) + .txHash("tx3") + .outputIndex(10) + .amount(List.of(Amount.builder() + .quantity(BigInteger.valueOf(300)) + .unit(LOVELACE) + .build())).build() + ))); + given(utxoService.getUtxos(addr4, 40, 1, OrderEnum.asc)) + .willReturn(Result.success("ok").withValue(List.of( + Utxo.builder() + .address(addr4) + .txHash("tx4") + .outputIndex(4) + .amount(List.of(Amount.builder() + .quantity(BigInteger.valueOf(400)) + .unit(LOVELACE) + .build())).build() + ))); + + + + List utxoList = utxoSupplier.getAll(); + assertThat(utxoList).hasSize(3); + assertThat(utxoList.stream().map(utxo -> utxo.getAddress()).collect(Collectors.toList())) + .contains(addr1, addr2, addr3) + .doesNotContain(addr4); + } + + @Test + void getAllWhenIndexesToScan() throws ApiException { + Wallet wallet = new Wallet(); + wallet.setIndexesToScan(new int[]{25, 50}); + var addr1 = wallet.getAccount(3).baseAddress(); + var addr2 = wallet.getAccount(7).baseAddress(); + var addr3 = wallet.getAccount(25).baseAddress(); + var addr4 = wallet.getAccount(50).baseAddress(); + + DefaultWalletUtxoSupplier utxoSupplier = new DefaultWalletUtxoSupplier(utxoService, wallet); + + given(utxoService.getUtxos(addr1, 100, 1, OrderEnum.asc)) + .willReturn(Result.success("ok").withValue(List.of( + Utxo.builder() + .address(addr1) + .txHash("tx1") + .outputIndex(0) + .amount(List.of(Amount.builder() + .quantity(BigInteger.valueOf(100)) + .unit(LOVELACE) + .build())).build() + ))); + + given(utxoService.getUtxos(addr2, 100, 1, OrderEnum.asc)) + .willReturn(Result.success("ok").withValue(List.of( + Utxo.builder() + .address(addr2) + .txHash("tx2") + .outputIndex(0) + .amount(List.of(Amount.builder() + .quantity(BigInteger.valueOf(200)) + .unit(LOVELACE) + .build())).build() + ))); + given(utxoService.getUtxos(addr3, 100, 1, OrderEnum.asc)) + .willReturn(Result.success("ok").withValue(List.of( + Utxo.builder() + .address(addr3) + .txHash("tx3") + .outputIndex(10) + .amount(List.of(Amount.builder() + .quantity(BigInteger.valueOf(300)) + .unit(LOVELACE) + .build())).build() + ))); + given(utxoService.getUtxos(addr4, 100, 1, OrderEnum.asc)) + .willReturn(Result.success("ok").withValue(List.of( + Utxo.builder() + .address(addr4) + .txHash("tx4") + .outputIndex(4) + .amount(List.of(Amount.builder() + .quantity(BigInteger.valueOf(400)) + .unit(LOVELACE) + .build())).build() + ))); + + + + List utxoList = utxoSupplier.getAll(); + assertThat(utxoList).hasSize(2); + assertThat(utxoList.stream().map(utxo -> utxo.getAddress()).collect(Collectors.toList())) + .contains(addr3, addr4) + .doesNotContain(addr1, addr2); + } + +} From 0f10f3f1066e73a81f258d58b8dc8239e9a72dba Mon Sep 17 00:00:00 2001 From: Satya Date: Mon, 25 Nov 2024 12:43:56 +0800 Subject: [PATCH 16/16] fix: changes to fix SonarQube errors --- .../cardano/client/account/Account.java | 32 ----------- .../cardano/client/crypto/MnemonicUtil.java | 6 +- .../function/helper/SignerProviders.java | 3 +- .../com/bloxbean/cardano/hdwallet/Wallet.java | 8 +-- .../cardano/hdwallet/WalletException.java | 19 +++++++ .../cardano/hdwallet/WalletInterface.java | 55 ------------------- .../cardano/hdwallet/model/WalletUtxo.java | 7 ++- .../supplier/DefaultWalletUtxoSupplier.java | 3 +- .../bloxbean/cardano/hdwallet/WalletTest.java | 12 ++-- 9 files changed, 42 insertions(+), 103 deletions(-) create mode 100644 hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/WalletException.java delete mode 100644 hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/WalletInterface.java diff --git a/core/src/main/java/com/bloxbean/cardano/client/account/Account.java b/core/src/main/java/com/bloxbean/cardano/client/account/Account.java index e6d954fd..f6ed7c7a 100644 --- a/core/src/main/java/com/bloxbean/cardano/client/account/Account.java +++ b/core/src/main/java/com/bloxbean/cardano/client/account/Account.java @@ -7,12 +7,9 @@ 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; @@ -23,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. */ @@ -487,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) { diff --git a/crypto/src/main/java/com/bloxbean/cardano/client/crypto/MnemonicUtil.java b/crypto/src/main/java/com/bloxbean/cardano/client/crypto/MnemonicUtil.java index 1fbbe4f5..c13cad99 100644 --- a/crypto/src/main/java/com/bloxbean/cardano/client/crypto/MnemonicUtil.java +++ b/crypto/src/main/java/com/bloxbean/cardano/client/crypto/MnemonicUtil.java @@ -10,6 +10,10 @@ public class MnemonicUtil { + private MnemonicUtil() { + + } + public static void validateMnemonic(String mnemonic) { if (mnemonic == null) { throw new AddressRuntimeException("Mnemonic cannot be null"); @@ -30,7 +34,7 @@ public static String generateNew(Words noOfWords) { try { mnemonic = MnemonicCode.INSTANCE.createMnemonic(noOfWords).stream().collect(Collectors.joining(" ")); } catch (MnemonicException.MnemonicLengthException e) { - throw new RuntimeException("Mnemonic generation failed", e); + throw new AddressRuntimeException("Mnemonic generation failed", e); } return mnemonic; } diff --git a/function/src/main/java/com/bloxbean/cardano/client/function/helper/SignerProviders.java b/function/src/main/java/com/bloxbean/cardano/client/function/helper/SignerProviders.java index 3eeed8f5..cc242cb1 100644 --- a/function/src/main/java/com/bloxbean/cardano/client/function/helper/SignerProviders.java +++ b/function/src/main/java/com/bloxbean/cardano/client/function/helper/SignerProviders.java @@ -46,8 +46,7 @@ public static TxSigner signerFrom(Wallet wallet) { .stream().filter(utxo -> utxo instanceof WalletUtxo) .map(utxo -> (WalletUtxo) utxo) .collect(Collectors.toSet()); - Transaction outputTxn = wallet.sign(transaction, utxos); - return outputTxn; + return wallet.sign(transaction, utxos); }; } diff --git a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/Wallet.java b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/Wallet.java index 1ec01c77..be02a7b9 100644 --- a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/Wallet.java +++ b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/Wallet.java @@ -185,7 +185,7 @@ public HdKeyPair getRootKeyPair() { rootKeys = hdKeyGenerator.getRootKeyPairFromEntropy(entropy); } catch (MnemonicException.MnemonicLengthException | MnemonicException.MnemonicWordException | MnemonicException.MnemonicChecksumException e) { - throw new RuntimeException(e); + throw new WalletException("Unable to derive root key pair", e); } } return rootKeys; @@ -211,10 +211,10 @@ public Transaction sign(Transaction txToSign, Set utxos) { var accounts = accountMap.values(); if(accounts.isEmpty()) - throw new RuntimeException("No signers found!"); + throw new WalletException("No signers found!"); - for (Account account : accounts) - txToSign = account.sign(txToSign); + for (Account signerAcc : accounts) + txToSign = signerAcc.sign(txToSign); return txToSign; } diff --git a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/WalletException.java b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/WalletException.java new file mode 100644 index 00000000..4b5c1884 --- /dev/null +++ b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/WalletException.java @@ -0,0 +1,19 @@ +package com.bloxbean.cardano.hdwallet; + +public class WalletException extends RuntimeException { + + public WalletException() { + } + + public WalletException(String msg) { + super(msg); + } + + public WalletException(Throwable cause) { + super(cause); + } + + public WalletException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/WalletInterface.java b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/WalletInterface.java deleted file mode 100644 index 19a38e83..00000000 --- a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/WalletInterface.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.bloxbean.cardano.hdwallet; - -import com.bloxbean.cardano.client.account.Account; -import com.bloxbean.cardano.client.api.model.Amount; - -import java.util.List; - -public interface WalletInterface { - -// private String mnemonic = ""; -// public HDWalletInterface(Network network, String mnemonic); -// public HDWalletInterface(Network network); - - // sum up all amounts within this wallet - // scanning strategy is as described in specification.md - public List getWalletBalance(); - - /** - * Returns the account for the given index - * @param index - * @return - */ - public Account getAccount(int index); - - /** - * Creates a new Account for the first empty index. - * @return - */ - public Account newAccount(); - - /** - * Returns the stake address - * @return - */ - public String getStakeAddress(); - - /** - * returns the master private key - * @return - */ - public byte[] getMasterPrivateKey(); - - /** - * Returns the master public key - * @return - */ - public String getMasterPublicKey(); - - /** - * returns the master adress from where other addresses can be derived - * @return - */ - public String getMasterAddress(); // prio 2 - -} diff --git a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/model/WalletUtxo.java b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/model/WalletUtxo.java index 94a7962f..8c670063 100644 --- a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/model/WalletUtxo.java +++ b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/model/WalletUtxo.java @@ -5,10 +5,12 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; -import lombok.Data; +import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; -@Data +@Getter +@Setter @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) @@ -26,4 +28,5 @@ public static WalletUtxo from(Utxo utxo) { walletUtxo.setReferenceScriptHash(utxo.getReferenceScriptHash()); return walletUtxo; } + } diff --git a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/supplier/DefaultWalletUtxoSupplier.java b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/supplier/DefaultWalletUtxoSupplier.java index 515bf76c..095573bc 100644 --- a/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/supplier/DefaultWalletUtxoSupplier.java +++ b/hd-wallet/src/main/java/com/bloxbean/cardano/hdwallet/supplier/DefaultWalletUtxoSupplier.java @@ -10,6 +10,7 @@ import com.bloxbean.cardano.client.crypto.cip1852.DerivationPath; import com.bloxbean.cardano.client.crypto.cip1852.Segment; import com.bloxbean.cardano.hdwallet.Wallet; +import com.bloxbean.cardano.hdwallet.WalletException; import com.bloxbean.cardano.hdwallet.model.WalletUtxo; import lombok.Setter; @@ -111,6 +112,6 @@ public List getUtxosForAccountAndIndex(int account, int index) { private void checkIfWalletIsSet() { if(this.wallet == null) - throw new RuntimeException("Wallet has to be provided!"); + throw new WalletException("Wallet has to be provided!"); } } diff --git a/hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/WalletTest.java b/hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/WalletTest.java index 10183505..d123692e 100644 --- a/hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/WalletTest.java +++ b/hd-wallet/src/test/java/com/bloxbean/cardano/hdwallet/WalletTest.java @@ -53,7 +53,7 @@ void WalletAddressToAccountAddressTest() { } @Test - public void testGetBaseAddressFromMnemonicIndex_0() { + void testGetBaseAddressFromMnemonicIndex_0() { Wallet wallet = new Wallet(Networks.mainnet(), phrase24W); Assertions.assertEquals(baseAddress0, wallet.getBaseAddressString(0)); Assertions.assertEquals(baseAddress1, wallet.getBaseAddressString(1)); @@ -62,7 +62,7 @@ public void testGetBaseAddressFromMnemonicIndex_0() { } @Test - public void testGetBaseAddressFromMnemonicByNetworkInfoTestnet() { + void testGetBaseAddressFromMnemonicByNetworkInfoTestnet() { Wallet wallet = new Wallet(Networks.testnet(), phrase24W); Assertions.assertEquals(testnetBaseAddress0, wallet.getBaseAddressString(0)); Assertions.assertEquals(testnetBaseAddress1, wallet.getBaseAddressString(1)); @@ -70,7 +70,7 @@ public void testGetBaseAddressFromMnemonicByNetworkInfoTestnet() { } @Test - public void testGetEnterpriseAddressFromMnemonicIndex() { + void testGetEnterpriseAddressFromMnemonicIndex() { Wallet wallet = new Wallet(Networks.mainnet(), phrase24W); Assertions.assertEquals(entAddress0, wallet.getEntAddress(0).getAddress()); Assertions.assertEquals(entAddress1, wallet.getEntAddress(1).getAddress()); @@ -78,7 +78,7 @@ public void testGetEnterpriseAddressFromMnemonicIndex() { } @Test - public void testGetEnterpriseAddressFromMnemonicIndexByNetwork() { + void testGetEnterpriseAddressFromMnemonicIndexByNetwork() { Wallet wallet = new Wallet(Networks.testnet(), phrase24W); Assertions.assertEquals(testnetEntAddress0, wallet.getEntAddress(0).getAddress()); Assertions.assertEquals(testnetEntAddress1, wallet.getEntAddress(1).getAddress()); @@ -86,13 +86,13 @@ public void testGetEnterpriseAddressFromMnemonicIndexByNetwork() { } @Test - public void testGetPublicKeyBytesFromMnemonic() { + void testGetPublicKeyBytesFromMnemonic() { byte[] pubKey = new Wallet(phrase24W).getRootKeyPair().getPublicKey().getKeyData(); Assertions.assertEquals(32, pubKey.length); } @Test - public void testGetPrivateKeyBytesFromMnemonic() { + void testGetPrivateKeyBytesFromMnemonic() { byte[] pvtKey = new Wallet(phrase24W).getRootKeyPair().getPrivateKey().getBytes(); Assertions.assertEquals(96, pvtKey.length); }