diff --git a/address/src/main/java/com/bloxbean/cardano/client/address/Address.java b/address/src/main/java/com/bloxbean/cardano/client/address/Address.java index a0711d39..f0d97754 100644 --- a/address/src/main/java/com/bloxbean/cardano/client/address/Address.java +++ b/address/src/main/java/com/bloxbean/cardano/client/address/Address.java @@ -12,6 +12,7 @@ * Address class represents Shelley address */ public class Address { + public static final String ADDR_VKH_PREFIX = "addr_vkh"; private String prefix; private byte[] bytes; private String address; @@ -173,4 +174,18 @@ public boolean isStakeKeyHashInDelegationPart() { public boolean isScriptHashInDelegationPart() { return AddressProvider.isScriptHashInDelegationPart(this); } + + /** + * Retrieves the Bech32-encoded address verification key hash of the payment credential + * associated with the address, if available. + * + * + * @return An {@link Optional} containing the Bech32-encoded verification key hash + * if the payment credential hash is available, or an empty {@link Optional} + * if the payment credential hash is absent. + */ + public Optional getBech32VerificationKeyHash() { + return getPaymentCredentialHash() + .map(paymentCred -> Bech32.encode(paymentCred, ADDR_VKH_PREFIX)); + } } 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 a2743b61..5299817b 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 @@ -20,6 +20,8 @@ import com.bloxbean.cardano.client.util.HexUtil; import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.Optional; + /** * Create and manage secrets, and perform account-based work such as signing transactions. */ @@ -28,6 +30,9 @@ public class Account { private String mnemonic; @JsonIgnore private byte[] accountKey; //Pvt key at account level m/1852'/1815'/x + @JsonIgnore + private byte[] rootKey; //Pvt key at root level m/ + private String baseAddress; private String changeAddress; private String enterpriseAddress; @@ -90,7 +95,9 @@ public Account(Network network, DerivationPath derivationPath, Words noOfWords) * Create a mainnet account from a mnemonic * * @param mnemonic + * @deprecated Use factory method `createFromMnemonic` to create an Account from mnemonic */ + @Deprecated(since = "0.7.0-beta2") public Account(String mnemonic) { this(Networks.mainnet(), mnemonic, 0); } @@ -99,7 +106,9 @@ public Account(String mnemonic) { * Create a mainnet account from a mnemonic at index * * @param mnemonic + * @deprecated Use factory method `createFromMnemonic` to create an Account from mnemonic */ + @Deprecated(since = "0.7.0-beta2") public Account(String mnemonic, int index) { this(Networks.mainnet(), mnemonic, index); } @@ -109,7 +118,9 @@ public Account(String mnemonic, int index) { * * @param network * @param mnemonic + * @deprecated Use factory method `createFromMnemonic` to create an Account from mnemonic */ + @Deprecated(since = "0.7.0-beta2") public Account(Network network, String mnemonic) { this(network, mnemonic, 0); } @@ -120,7 +131,9 @@ public Account(Network network, String mnemonic) { * @param network * @param mnemonic * @param index + * @deprecated Use factory method `createFromMnemonic` to create an Account from mnemonic */ + @Deprecated(since = "0.7.0-beta2") public Account(Network network, String mnemonic, int index) { this(network, mnemonic, DerivationPath.createExternalAddressDerivationPath(index)); } @@ -131,7 +144,9 @@ public Account(Network network, String mnemonic, int index) { * @param network * @param mnemonic * @param derivationPath + * @deprecated Use factory method `createFromMnemonic` to create an Account from mnemonic */ + @Deprecated(since = "0.7.0-beta2") public Account(Network network, String mnemonic, DerivationPath derivationPath) { this.network = network; this.mnemonic = mnemonic; @@ -145,8 +160,10 @@ public Account(Network network, String mnemonic, DerivationPath derivationPath) * Create an account from a private key for a specified network for account = 0, index = 0 * * @param network - * @param accountKey accountKey is a private key of 96 bytes or 128 bytes (with pubkey and chaincode) at account level + * @param accountKey accountKey is a private key of 96 bytes (with priv key and chaincode) at account level + * @deprecated Use factory method `createFromAccountKey` to create an Account from account level key */ + @Deprecated(since = "0.7.0-beta2") public Account(Network network, byte[] accountKey) { this(network, accountKey, 0, 0); } @@ -155,21 +172,19 @@ public Account(Network network, byte[] accountKey) { * Create an account from a private key for a specified network * * @param network - * @param accountKey is a private key of 96 bytes or 128 bytes (with pubkey and chaincode) at account level + * @param accountKey is a private key of 96 bytes (with priv key and chaincode) at account level * @param account account * @param index address index + * @deprecated Use factory method `createFromAccountKey` to create an Account from account level key */ + @Deprecated(since = "0.7.0-beta2") public Account(Network network, byte[] accountKey, int account, int index) { this.network = network; this.mnemonic = null; if (accountKey.length == 96) this.accountKey = accountKey; - else if(accountKey.length == 128){ - byte[] key = new byte[96]; - System.arraycopy(accountKey, 0, key, 0, 64); - System.arraycopy(accountKey, 96, key, 64, 32); - } else - throw new RuntimeException("Invalid length (Account Private Key): " + accountKey.length); + else + throw new AccountException("Invalid length (Account Private Key): " + accountKey.length); this.derivationPath = DerivationPath.createExternalAddressDerivationPathForAccount(account); this.derivationPath.getIndex().setValue(index); @@ -177,8 +192,153 @@ else if(accountKey.length == 128){ baseAddress(); } + private Account(Network network, String mnemonic, byte[] rootKey, byte[] accountKey, DerivationPath derivationPath) { + this.network = network; + this.derivationPath = derivationPath; + + if (mnemonic != null && !mnemonic.isEmpty()) { + this.mnemonic = mnemonic; + this.accountKey = null; + MnemonicUtil.validateMnemonic(this.mnemonic); + } else if (rootKey != null && rootKey.length > 0) { + this.mnemonic = null; + this.accountKey = null; + + if (rootKey.length == 96) + this.rootKey = rootKey; + else + throw new AccountException("Invalid length (Root Pvt Key): " + rootKey.length); + } else if (accountKey != null && accountKey.length > 0) { + this.mnemonic = null; + if (accountKey.length == 96) + this.accountKey = accountKey; + else + throw new AccountException("Invalid length (Account Private Key): " + accountKey.length); + } + + baseAddress(); + } + /** - * @return string a 24 word mnemonic + * Creates an Account object from a given mnemonic phrase at m/1852'/1815'/0/0/0 + * + * @param network the network for the account. Possible values: Networks.mainnet() or Networks.testnet() + * @param mnemonic the mnemonic phrase used to generate the account + * @return an Account object generated from the mnemonic phrase + */ + public static Account createFromMnemonic(Network network, String mnemonic) { + return createFromMnemonic(network, mnemonic, 0, 0); + } + + /** + * Creates an Account instance from the given mnemonic, network at derivation path m/1852'/1815'/account/0/index + * + * @param network the network for the account. Possible values: Networks.mainnet() or Networks.testnet() + * @param mnemonic the mnemonic phrase used for generating the account. + * @param account the account number in the derivation path. + * @param index the index for the address in the derivation path. + * @return an Account object generated from the mnemonic phrase + */ + public static Account createFromMnemonic(Network network, String mnemonic, int account, int index) { + var derivationPath = DerivationPath.createExternalAddressDerivationPathForAccount(account); + derivationPath.getIndex().setValue(index); + + return createFromMnemonic(network, mnemonic, derivationPath); + } + + /** + * Creates an Account instance from the provided mnemonic at given derivation path + * + * @param network the network for the account. Possible values: Networks.mainnet() or Networks.testnet() + * @param mnemonic the mnemonic phrase used to generate the account + * @param derivationPath the derivation path used for key generation + * @return an Account object generated from the mnemonic phrase + */ + public static Account createFromMnemonic(Network network, String mnemonic, DerivationPath derivationPath) { + return new Account(network, mnemonic, null, null, derivationPath); + } + + /** + * Creates an Account instance from a root key at derivation path: m/1852'/1815'/0/0/0 + * + * @param network The network for the account. Possible values: Networks.mainnet() or Networks.testnet() + * @param rootKey the root key used to derive the account + * @return a new Account object derived from the provided root key + */ + public static Account createFromRootKey(Network network, byte[] rootKey) { + return createFromRootKey(network, rootKey, 0, 0); + } + + /** + * Creates an Account instance using the provided network, rootKey, account number, and index + * at derivation path m/1852'/1815'/account/0/index + * + * @param network the network for the account. Possible values: Networks.mainnet() or Networks.testnet() + * @param rootKey the root key used to derive the account. Possible values: Networks.mainnet() or Networks.testnet() + * @param account the account number used in the derivation path. + * @param index the index used in the derivation path. + * @return A new Account object derived from the specified root key + */ + public static Account createFromRootKey(Network network, byte[] rootKey, int account, int index) { + var derivationPath = DerivationPath.createExternalAddressDerivationPathForAccount(account); + derivationPath.getIndex().setValue(index); + + return createFromRootKey(network, rootKey, derivationPath); + } + + /** + * Creates an Account instance from the provided root key and derivation path. + * + * @param network the network for the account. Possible values: Networks.mainnet() or Networks.testnet() + * @param rootKey the root key used to derive the account. + * @param derivationPath the derivation path used for key generation. + * @return A new Account object derived from the provided root key + */ + public static Account createFromRootKey(Network network, byte[] rootKey, DerivationPath derivationPath) { + return new Account(network, null, rootKey, null, derivationPath); + } + + /** + * Creates an Account instance using the provided account level key and at address index = 0 + * + * @param network the network for the account. Possible values: Networks.mainnet() or Networks.testnet() + * @param accountKey the account key used to derive the account + * @return A new Account object derived from the provided account level key + */ + public static Account createFromAccountKey(Network network, byte[] accountKey) { + return createFromAccountKey(network, accountKey, 0, 0); + } + + /** + * Creates an Account instance from the given account key, network, account number, and index. (m/1852'/1815'/account/0/index) + * + * @param network the network for the account. Possible values: Networks.mainnet() or Networks.testnet() + * @param accountKey the account key byte array + * @param account the account number + * @param index the index value for the account derivation + * @return an Account object created using account key + */ + public static Account createFromAccountKey(Network network, byte[] accountKey, int account, int index) { + var derivationPath = DerivationPath.createExternalAddressDerivationPathForAccount(account); + derivationPath.getIndex().setValue(index); + + return createFromAccountKey(network, accountKey, derivationPath); + } + + /** + * Creates an Account instance from the given account key, network, derivation path + * + * @param network the network for the account. Possible values: Networks.mainnet() or Networks.testnet() + * @param accountKey the account key byte array + * @param derivationPath + * @return an Account object created using account key + */ + public static Account createFromAccountKey(Network network, byte[] accountKey, DerivationPath derivationPath) { + return new Account(network, null, null, accountKey, derivationPath); + } + + /** + * @return string a 24 word mnemonic or null if the Account is derived from root key or account key */ public String mnemonic() { return mnemonic; @@ -495,78 +655,58 @@ public Transaction signWithCommitteeHotKey(Transaction transaction) { return TransactionSigner.INSTANCE.sign(transaction, getCommitteeHotKeyPair()); } + public Optional getRootKeyPair() { + if (mnemonic != null && !mnemonic.isEmpty()) { + return Optional.of(new CIP1852().getRootKeyPairFromMnemonic(mnemonic)); + } else if (rootKey != null && rootKey.length > 0) { + return Optional.of(new CIP1852().getRootKeyPairFromRootKey(rootKey)); + } else + return Optional.empty(); + } + private HdKeyPair getHdKeyPair() { - HdKeyPair hdKeyPair; - if (mnemonic == null || mnemonic.trim().length() == 0) { - hdKeyPair = new CIP1852().getKeyPairFromAccountKey(this.accountKey, derivationPath); - } - else { - hdKeyPair = new CIP1852().getKeyPairFromMnemonic(mnemonic, derivationPath); - } - return hdKeyPair; + return getHdKeyPairFromDerivationPath(derivationPath); } private HdKeyPair getChangeKeyPair() { - HdKeyPair hdKeyPair; DerivationPath internalDerivationPath = DerivationPath.createInternalAddressDerivationPathForAccount(derivationPath.getAccount().getValue()); - if (mnemonic == null || mnemonic.trim().length() == 0) { - hdKeyPair = new CIP1852().getKeyPairFromAccountKey(this.accountKey, internalDerivationPath); - } - else { - hdKeyPair = new CIP1852().getKeyPairFromMnemonic(mnemonic, internalDerivationPath); - } - - return hdKeyPair; + return getHdKeyPairFromDerivationPath(internalDerivationPath); } private HdKeyPair getStakeKeyPair() { - HdKeyPair hdKeyPair; DerivationPath stakeDerivationPath = DerivationPath.createStakeAddressDerivationPathForAccount(derivationPath.getAccount().getValue()); - if (mnemonic == null || mnemonic.trim().length() == 0) { - hdKeyPair = new CIP1852().getKeyPairFromAccountKey(this.accountKey, stakeDerivationPath); - } else { - hdKeyPair = new CIP1852().getKeyPairFromMnemonic(mnemonic, stakeDerivationPath); - } - - return hdKeyPair; + return getHdKeyPairFromDerivationPath(stakeDerivationPath); } private HdKeyPair getDRepKeyPair() { - HdKeyPair hdKeyPair; DerivationPath drepDerivationPath = DerivationPath.createDRepKeyDerivationPathForAccount(derivationPath.getAccount().getValue()); - - if (mnemonic == null || mnemonic.trim().length() == 0) { - hdKeyPair = new CIP1852().getKeyPairFromAccountKey(this.accountKey, drepDerivationPath); - } else { - hdKeyPair = new CIP1852().getKeyPairFromMnemonic(mnemonic, drepDerivationPath); - } - - return hdKeyPair; + return getHdKeyPairFromDerivationPath(drepDerivationPath); } private HdKeyPair getCommitteeColdKeyPair() { - HdKeyPair hdKeyPair; - DerivationPath drepDerivationPath = + DerivationPath ccColdDerivationPath = DerivationPath.createCommitteeColdKeyDerivationPathForAccount(derivationPath.getAccount().getValue()); - if (mnemonic == null || mnemonic.trim().length() == 0) { - hdKeyPair = new CIP1852().getKeyPairFromAccountKey(this.accountKey, drepDerivationPath); - } else { - hdKeyPair = new CIP1852().getKeyPairFromMnemonic(mnemonic, drepDerivationPath); - } - - return hdKeyPair; + return getHdKeyPairFromDerivationPath(ccColdDerivationPath); } private HdKeyPair getCommitteeHotKeyPair() { - HdKeyPair hdKeyPair; - DerivationPath drepDerivationPath = + DerivationPath ccHotDerivationPath = DerivationPath.createCommitteeHotKeyDerivationPathForAccount(derivationPath.getAccount().getValue()); - if (mnemonic == null || mnemonic.trim().length() == 0) { - hdKeyPair = new CIP1852().getKeyPairFromAccountKey(this.accountKey, drepDerivationPath); + return getHdKeyPairFromDerivationPath(ccHotDerivationPath); + } + + private HdKeyPair getHdKeyPairFromDerivationPath(DerivationPath derivationPath) { + HdKeyPair hdKeyPair; + if (mnemonic != null && !mnemonic.isEmpty()) { + hdKeyPair = new CIP1852().getKeyPairFromMnemonic(mnemonic, derivationPath); + } else if (accountKey != null && accountKey.length > 0) { + hdKeyPair = new CIP1852().getKeyPairFromAccountKey(this.accountKey, derivationPath); + } else if (rootKey != null && rootKey.length > 0) { + hdKeyPair = new CIP1852().getKeyPairFromRootKey(this.rootKey, derivationPath); } else { - hdKeyPair = new CIP1852().getKeyPairFromMnemonic(mnemonic, drepDerivationPath); + throw new AccountException("HDKeyPair derivation failed. Only one of mnemonic, rootKey, or accountKey should be set."); } return hdKeyPair; diff --git a/core/src/main/java/com/bloxbean/cardano/client/account/AccountException.java b/core/src/main/java/com/bloxbean/cardano/client/account/AccountException.java new file mode 100644 index 00000000..bc76d740 --- /dev/null +++ b/core/src/main/java/com/bloxbean/cardano/client/account/AccountException.java @@ -0,0 +1,18 @@ +package com.bloxbean.cardano.client.account; + +public class AccountException extends RuntimeException { + public AccountException() { + } + + public AccountException(String msg) { + super(msg); + } + + public AccountException(Throwable cause) { + super(cause); + } + + public AccountException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/core/src/test/java/com/bloxbean/cardano/client/account/AccountTest.java b/core/src/test/java/com/bloxbean/cardano/client/account/AccountTest.java index e4388fd0..ec78c762 100644 --- a/core/src/test/java/com/bloxbean/cardano/client/account/AccountTest.java +++ b/core/src/test/java/com/bloxbean/cardano/client/account/AccountTest.java @@ -4,9 +4,11 @@ import com.bloxbean.cardano.client.address.util.AddressUtil; import com.bloxbean.cardano.client.common.model.Network; import com.bloxbean.cardano.client.common.model.Networks; +import com.bloxbean.cardano.client.crypto.Bech32; import com.bloxbean.cardano.client.crypto.bip32.HdKeyGenerator; import com.bloxbean.cardano.client.crypto.bip32.HdKeyPair; import com.bloxbean.cardano.client.crypto.bip32.key.HdPublicKey; +import com.bloxbean.cardano.client.crypto.cip1852.DerivationPath; import com.bloxbean.cardano.client.exception.AddressExcepion; import com.bloxbean.cardano.client.exception.AddressRuntimeException; import com.bloxbean.cardano.client.exception.CborSerializationException; @@ -432,4 +434,225 @@ void testDRepId() { String legacyDRepId = account.legacyDRepId(); assertThat(legacyDRepId).isEqualTo("drep18hf6wcv9aaq426duj8kcc5kp9pauz9ac8znh8jmckm80sf7fetw"); } + + @Test + void testAccountFromRootKey() { + String rootKey = "xprv1frqqvtmax6a5lqv5h6e8vt2wxglasnweglnap8dclz69fd62zp2kqccn08nmjah5rct9zvuh3mx4dln9z984hf42474q6jp2frn3ahkxxaau9y2yfvrr7ex4nw24g37flvarqfhy87g99kp20yknqn7kgs04h87k"; + byte[] rootKeyBytes = Bech32.decode(rootKey).data; + Account account = Account.createFromRootKey(Networks.testnet(), rootKeyBytes); + + //expected + //tragic movie pulp rely quick damage spoil case bubble forget banana bomb pilot fresh trumpet learn basic melt curtain defy erode soccer race oil + + assertThat(account.baseAddress()).isEqualTo("addr_test1qzm0439fe55aynh58qcn4jnh4mwuqwr5n5fez7j0hck9ds8j3nmg5pkqfur4gyupppuu82r83s5eheewzmf6fwlzfz7qzsp6rc"); + assertThat(account.changeAddress()).isEqualTo("addr_test1qqkqwker9785sna30vmjggynjxzce6sdg2th7w3w0sgfvr8j3nmg5pkqfur4gyupppuu82r83s5eheewzmf6fwlzfz7q5r7hzu"); + assertThat(account.stakeAddress()).isEqualTo("stake_test1uregea52qmqy7p65zwqss7wr4pncc2vmuuhpd5ayh03y30q7ag6w7"); + assertThat(account.drepId()).isEqualTo("drep1y2vwwy93jq0gttdsrux3eum43cl2c4umzxd04pcmqmdvgwg8rnc4h"); + assertThat(account.committeeColdKey().id()).isEqualTo("cc_cold1zg6ed528z5xc8x9wnnjnyw5gu26a69j8es2cff6nq6799vszr7ukm"); + assertThat(account.committeeHotKey().id()).isEqualTo("cc_hot1qth000uamffyddnlqlsjw55f8gthy4nr96rtdrvkvuhd5gsjxuyjw"); + + } + + @Test + void testGetRootKeyPair() { + var seedPhrase = + "tragic movie pulp rely quick damage spoil case bubble forget banana bomb pilot fresh trumpet learn basic melt curtain defy erode soccer race oil"; + HdKeyPair rootKeyPair = new Account(Networks.testnet(), seedPhrase).getRootKeyPair().get(); + String rootPvtKeyBech32 = rootKeyPair.getPrivateKey().toBech32(); + + System.out.println(HexUtil.encodeHexString(rootKeyPair.getPrivateKey().getBytes())); + + assertThat(rootPvtKeyBech32).isEqualTo("xprv1frqqvtmax6a5lqv5h6e8vt2wxglasnweglnap8dclz69fd62zp2kqccn08nmjah5rct9zvuh3mx4dln9z984hf42474q6jp2frn3ahkxxaau9y2yfvrr7ex4nw24g37flvarqfhy87g99kp20yknqn7kgs04h87k"); + } + + @Test + void testAccountFromRootKey_128Bytes_throwsException() { + //Last 32 bytes in 128 bytes array are arbitary bytes + String rootKey128Bytes = "48c0062f7d36bb4f8194beb2762d4e323fd84dd947e7d09db8f8b454b74a105560631379e7b976f41e165133978ecd56fe65114f5ba6aaafaa0d482a48e71edec6377bc291444b063f64d59b955447c9fb3a3026e43f9052d82a792d304fd644c333ef7429361bdb1414e15e054f6654bce419d26057d0e38d76993f9c3ab71f"; + byte[] rootKey = HexUtil.decodeHexString(rootKey128Bytes); + + assertThrows(Exception.class, () -> { + Account.createFromRootKey(Networks.testnet(), rootKey); + }); + } + + @Test + void testAccountFromAccountKey_0() { + //original phrase + //fresh apple bus punch dynamic what arctic elevator logic hole survey hunt better adapt helmet fat refuse season enter category tomato mule capable faith + + //at account=0 + String accountPrvKey = "acct_xsk14zau3uj79pxh2wplfnezeetj3ms5wfgvyltg3jap0ch5cuswq3qe39l4aty2wjgtyzagzc8squ0hz6hrej6ypqdrj4yhxynapsf462ypgv3clpf74q56k6r32847a4cp9dlx6n8ew8hyqdv6ydv5q8yt9vhn8ktv"; + byte[] accountPrvKeyBytes = Bech32.decode(accountPrvKey).data; + + System.out.println(accountPrvKeyBytes.length); + + Account account = Account.createFromAccountKey(Networks.testnet(), accountPrvKeyBytes); + + assertThat(account.baseAddress()).isEqualTo("addr_test1qq7x3pklemucwtw6qcym6trkcfmenslhnsq7e8cag7h82507mkje2hyjgc8zr00z835as0kw5hwa48h3vnrctjpu4a8qy8lf5r"); + assertThat(account.changeAddress()).isEqualTo("addr_test1qr430yp4r7mgsj5t34pgp2zcv37w8arj62gyqtdayheejz87mkje2hyjgc8zr00z835as0kw5hwa48h3vnrctjpu4a8qy27g5x"); + assertThat(account.stakeAddress()).isEqualTo("stake_test1urldmfv4tjfyvr3phh3rc6wc8m82thw6nmckf3u9eq727ns3kwf9u"); + assertThat(account.drepId()).isEqualTo("drep1y2zjw9adazlmychc6wlz4k8qa8g5jcjqwujg0jws9r6v9egvasg8r"); + assertThat(account.committeeColdKey().id()).isEqualTo("cc_cold1ztslrgxse5awd9yx9csqrcmystzw6rd88tva4tw7lqkjrtc6d4ezh"); + assertThat(account.committeeHotKey().id()).isEqualTo("cc_hot1qgmfvk4g7vfquys52cdrx59ez948q337jhp5ycde3umlprqulcyff"); + } + + @Test + void testAccountFromRootKey_0() { + //original phrase + //fresh apple bus punch dynamic what arctic elevator logic hole survey hunt better adapt helmet fat refuse season enter category tomato mule capable faith + + String rootKey = "root_xsk1xrvg8kfpdlaluwstt0twcajgavqcgkczmav6lffvgfmrxeqwq3qer3rasrjdj9f663xa98xcu4a28zuv5cks5lytdvfezn49ycrndz2mptat9v0t5eafsdj9rpe4lcxndvys0v6qahq8v0flv9ycpav8ks46k8xh"; + byte[] rootKeyBytes = Bech32.decode(rootKey).data; + + Account account = Account.createFromRootKey(Networks.testnet(), rootKeyBytes); + + assertThat(account.baseAddress()).isEqualTo("addr_test1qq7x3pklemucwtw6qcym6trkcfmenslhnsq7e8cag7h82507mkje2hyjgc8zr00z835as0kw5hwa48h3vnrctjpu4a8qy8lf5r"); + assertThat(account.changeAddress()).isEqualTo("addr_test1qr430yp4r7mgsj5t34pgp2zcv37w8arj62gyqtdayheejz87mkje2hyjgc8zr00z835as0kw5hwa48h3vnrctjpu4a8qy27g5x"); + assertThat(account.stakeAddress()).isEqualTo("stake_test1urldmfv4tjfyvr3phh3rc6wc8m82thw6nmckf3u9eq727ns3kwf9u"); + assertThat(account.drepId()).isEqualTo("drep1y2zjw9adazlmychc6wlz4k8qa8g5jcjqwujg0jws9r6v9egvasg8r"); + assertThat(account.committeeColdKey().id()).isEqualTo("cc_cold1ztslrgxse5awd9yx9csqrcmystzw6rd88tva4tw7lqkjrtc6d4ezh"); + assertThat(account.committeeHotKey().id()).isEqualTo("cc_hot1qgmfvk4g7vfquys52cdrx59ez948q337jhp5ycde3umlprqulcyff"); + } + + @Test + void testAccountFromAccountKey_2() { + //original phrase + //fresh apple bus punch dynamic what arctic elevator logic hole survey hunt better adapt helmet fat refuse season enter category tomato mule capable faith + + //at account=2 1852H/1815H/2H + String accountPrvKey = "acct_xsk1tz659rq7zjytmgycce72k2xsswzv5uy6rzaydam2au6ksagwq3qh4pykgcaw6u7swpqxgyf2sugc2vrjjqzrpnjldwxk3gn7h4q8h38s83nlhc5y33ptlqwqhvg3k2hxyte9avdav9jvu9qz6j6tgsnuugkv3cea"; + byte[] accountPrvKeyBytes = Bech32.decode(accountPrvKey).data; + + System.out.println(accountPrvKeyBytes.length); + + Account account = Account.createFromAccountKey(Networks.testnet(), accountPrvKeyBytes); + + assertThat(account.baseAddress()).isEqualTo("addr_test1qpyf7633hxe5t5lwr20dre9xy54nuhl4qf53rvpcs3geq5mgypzw9uc2ug5dsdet3gtvkd7f32f4q6a5x3afwk5m6y7sfpmuak"); + assertThat(account.changeAddress()).isEqualTo("addr_test1qqphrf9wtwwhghevuy0gd95cldmdzn3gyrganlh7g8d8hvrgypzw9uc2ug5dsdet3gtvkd7f32f4q6a5x3afwk5m6y7s6xvl48"); + assertThat(account.stakeAddress()).isEqualTo("stake_test1up5zq38z7v9wy2xcxu4c59ktxlyc4y6sdw6rg75ht2daz0guncpsl"); + assertThat(account.drepId()).isEqualTo("drep1ygmmj4nqjzvaa39qxj6w7pxpx4u62f8lvnu4xzjwjk8segg56qful"); + assertThat(account.committeeColdKey().id()).isEqualTo("cc_cold1zftrxuz0u938d8mu5ndawz8yv6rkurfq8flrus6mcr32fhspk4ju3"); + assertThat(account.committeeHotKey().id()).isEqualTo("cc_hot1qtx827hr45paddz5h6w7k0rg40vpc79262dff8r42rlwxgq4jj6dg"); + } + + @Test + void testAccountFromRootKey_2() { + //original phrase + //fresh apple bus punch dynamic what arctic elevator logic hole survey hunt better adapt helmet fat refuse season enter category tomato mule capable faith + + String rootKey = "root_xsk1xrvg8kfpdlaluwstt0twcajgavqcgkczmav6lffvgfmrxeqwq3qer3rasrjdj9f663xa98xcu4a28zuv5cks5lytdvfezn49ycrndz2mptat9v0t5eafsdj9rpe4lcxndvys0v6qahq8v0flv9ycpav8ks46k8xh"; + byte[] rootKeyBytes = Bech32.decode(rootKey).data; + + Account account2 = Account.createFromRootKey(Networks.testnet(), rootKeyBytes, 2, 0); + Account account28 = Account.createFromRootKey(Networks.testnet(), rootKeyBytes, 2, 8); + + assertThat(account2.baseAddress()).isEqualTo("addr_test1qpyf7633hxe5t5lwr20dre9xy54nuhl4qf53rvpcs3geq5mgypzw9uc2ug5dsdet3gtvkd7f32f4q6a5x3afwk5m6y7sfpmuak"); + assertThat(account2.changeAddress()).isEqualTo("addr_test1qqphrf9wtwwhghevuy0gd95cldmdzn3gyrganlh7g8d8hvrgypzw9uc2ug5dsdet3gtvkd7f32f4q6a5x3afwk5m6y7s6xvl48"); + assertThat(account2.stakeAddress()).isEqualTo("stake_test1up5zq38z7v9wy2xcxu4c59ktxlyc4y6sdw6rg75ht2daz0guncpsl"); + assertThat(account2.drepId()).isEqualTo("drep1ygmmj4nqjzvaa39qxj6w7pxpx4u62f8lvnu4xzjwjk8segg56qful"); + assertThat(account2.committeeColdKey().id()).isEqualTo("cc_cold1zftrxuz0u938d8mu5ndawz8yv6rkurfq8flrus6mcr32fhspk4ju3"); + assertThat(account2.committeeHotKey().id()).isEqualTo("cc_hot1qtx827hr45paddz5h6w7k0rg40vpc79262dff8r42rlwxgq4jj6dg"); + + assertThat(account28.baseAddress()).isEqualTo("addr_test1qqacfd33e3qa20vqmv00qx6xftxhcqwhlr905l4rchf5j0rgypzw9uc2ug5dsdet3gtvkd7f32f4q6a5x3afwk5m6y7sa0sjfq"); + } + + @Test + void testAccountFromRootKey_derivationPath_2() { + //original phrase + //fresh apple bus punch dynamic what arctic elevator logic hole survey hunt better adapt helmet fat refuse season enter category tomato mule capable faith + + String rootKey = "root_xsk1xrvg8kfpdlaluwstt0twcajgavqcgkczmav6lffvgfmrxeqwq3qer3rasrjdj9f663xa98xcu4a28zuv5cks5lytdvfezn49ycrndz2mptat9v0t5eafsdj9rpe4lcxndvys0v6qahq8v0flv9ycpav8ks46k8xh"; + byte[] rootKeyBytes = Bech32.decode(rootKey).data; + + var path0 = DerivationPath.createExternalAddressDerivationPathForAccount(2); + var path8 = DerivationPath.createExternalAddressDerivationPathForAccount(2); + path8.getIndex().setValue(8); + + Account account2 = Account.createFromRootKey(Networks.testnet(), rootKeyBytes, path0); + Account account28 = Account.createFromRootKey(Networks.testnet(), rootKeyBytes, path8); + + assertThat(account2.baseAddress()).isEqualTo("addr_test1qpyf7633hxe5t5lwr20dre9xy54nuhl4qf53rvpcs3geq5mgypzw9uc2ug5dsdet3gtvkd7f32f4q6a5x3afwk5m6y7sfpmuak"); + assertThat(account2.changeAddress()).isEqualTo("addr_test1qqphrf9wtwwhghevuy0gd95cldmdzn3gyrganlh7g8d8hvrgypzw9uc2ug5dsdet3gtvkd7f32f4q6a5x3afwk5m6y7s6xvl48"); + assertThat(account2.stakeAddress()).isEqualTo("stake_test1up5zq38z7v9wy2xcxu4c59ktxlyc4y6sdw6rg75ht2daz0guncpsl"); + assertThat(account2.drepId()).isEqualTo("drep1ygmmj4nqjzvaa39qxj6w7pxpx4u62f8lvnu4xzjwjk8segg56qful"); + assertThat(account2.committeeColdKey().id()).isEqualTo("cc_cold1zftrxuz0u938d8mu5ndawz8yv6rkurfq8flrus6mcr32fhspk4ju3"); + assertThat(account2.committeeHotKey().id()).isEqualTo("cc_hot1qtx827hr45paddz5h6w7k0rg40vpc79262dff8r42rlwxgq4jj6dg"); + + assertThat(account28.baseAddress()).isEqualTo("addr_test1qqacfd33e3qa20vqmv00qx6xftxhcqwhlr905l4rchf5j0rgypzw9uc2ug5dsdet3gtvkd7f32f4q6a5x3afwk5m6y7sa0sjfq"); + } + + @Test + void testAccountFromMnemonic_15words_0() { + String mnemonic = "top exact spice seed cloud birth orient bracket happy cat section girl such outside elder"; + + Account account = Account.createFromMnemonic(Networks.testnet(), mnemonic); + + assertThat(account.baseAddress()).isEqualTo("addr_test1qqy4c938alydc5sa4w46dnprn8f0hrjcd6jrhfte8rfvgewc6r9xxc78l7x6h8tjvm8emnlewwxk2a6tae88h50etkcqkx92yf"); + assertThat(account.changeAddress()).isEqualTo("addr_test1qredvj90u0arndxjeggntqrynn7l5waszuy7g47qkd0yhtwc6r9xxc78l7x6h8tjvm8emnlewwxk2a6tae88h50etkcqxhe48d"); + assertThat(account.stakeAddress()).isEqualTo("stake_test1urvdpjnrv0rllrdtn4exdnuaeluh8rt9wa97unnm68u4mvq00d2tz"); + assertThat(account.drepId()).isEqualTo("drep1y2uys0eg8cvvszyxua6g8wz04jpgscgjelym9pvwthzg25q0ssk4v"); + assertThat(account.committeeColdKey().id()).isEqualTo("cc_cold1zgz6epprs27zmptt5ermfd89s8uvp7tzzm744sw7hkpedtgywh6r9"); + assertThat(account.committeeHotKey().id()).isEqualTo("cc_hot1qtlsh6swxn9dc30n2lx0u4qpu0cq0czrlccrueduk590dycmegtnc"); + } + + @Test + void testAccountFromMnemonic_15words_acc4_index3() { + String mnemonic = "top exact spice seed cloud birth orient bracket happy cat section girl such outside elder"; + + Account account = Account.createFromMnemonic(Networks.testnet(), mnemonic, 4, 3); + + assertThat(account.baseAddress()).isEqualTo("addr_test1qz3cw2uuwjwhdjwyf32pre79kca5mf722nm09a6welje4edx0xezf9ug8educ8pv3gp2ujflk6jcatz9muf4jckak3fshq2mdz"); + assertThat(account.changeAddress()).isEqualTo("addr_test1qq8x3y83vfsx6zrcnuyseact0z85fyev6vjhrk5nfxzm57dx0xezf9ug8educ8pv3gp2ujflk6jcatz9muf4jckak3fsf894c3"); + assertThat(account.stakeAddress()).isEqualTo("stake_test1uzn8nv3yj7yruk7vrskg5q4wfylmdfvw43za7y6evtwmg5c47c0hf"); + assertThat(account.drepId()).isEqualTo("drep1yt07pz022vfqzwr40jrch8pwcd09g2s2nqqsffdjag9d33g2czftn"); + assertThat(account.committeeColdKey().id()).isEqualTo("cc_cold1ztw27g74fpg4n5wl556c4spzx8n5gz6njtf8lqcrehgvqaswaasts"); + assertThat(account.committeeHotKey().id()).isEqualTo("cc_hot1qgjg5c946hgkjh5mvpw8x4q7v9m0zykjvw0wrq5pfm04tvqqh35qa"); + } + + @Test + void testAccountFromRootKey_15words_acc4_index3() { + + //Original mnemonic + //top exact spice seed cloud birth orient bracket happy cat section girl such outside elder + + String rootKey = "root_xsk1zza6z52v8gelnaqdhuny3ywlccud5dtm8rvvyem4utnfwzcaa9pspsmdm99qfpy2qz7sw9sts59mrkegmdqyjen5ykm4z3ccyrkn8g5mm0qw35arvwxclfh6tj3s4x7t2q85wenvppjpxckcxgnf8vd80ug0l6rw"; + + Account account = Account.createFromRootKey(Networks.testnet(), Bech32.decode(rootKey).data, 4, 3); + + assertThat(account.baseAddress()).isEqualTo("addr_test1qz3cw2uuwjwhdjwyf32pre79kca5mf722nm09a6welje4edx0xezf9ug8educ8pv3gp2ujflk6jcatz9muf4jckak3fshq2mdz"); + assertThat(account.changeAddress()).isEqualTo("addr_test1qq8x3y83vfsx6zrcnuyseact0z85fyev6vjhrk5nfxzm57dx0xezf9ug8educ8pv3gp2ujflk6jcatz9muf4jckak3fsf894c3"); + assertThat(account.stakeAddress()).isEqualTo("stake_test1uzn8nv3yj7yruk7vrskg5q4wfylmdfvw43za7y6evtwmg5c47c0hf"); + assertThat(account.drepId()).isEqualTo("drep1yt07pz022vfqzwr40jrch8pwcd09g2s2nqqsffdjag9d33g2czftn"); + assertThat(account.committeeColdKey().id()).isEqualTo("cc_cold1ztw27g74fpg4n5wl556c4spzx8n5gz6njtf8lqcrehgvqaswaasts"); + assertThat(account.committeeHotKey().id()).isEqualTo("cc_hot1qgjg5c946hgkjh5mvpw8x4q7v9m0zykjvw0wrq5pfm04tvqqh35qa"); + } + + @Test + void testAccountFromAccountKey_15words_acc4_index3() { + //Original mnemonic + //top exact spice seed cloud birth orient bracket happy cat section girl such outside elder + + String accountKey = "acct_xsk1azc6gn5zkdprp4gkapmhdckykphjl62rm9224699ut5z6xcaa9p4hv5hmjfgcrzk72tnsqh6dw0njekdjpsv8nv5h5hk6lpd4ag62zenwhzqs205kfurd7kgs8fm5gx4l4j8htutwj060kyp5y5kgw55qc8lsltd"; + + Account account = Account.createFromAccountKey(Networks.testnet(), Bech32.decode(accountKey).data, 4, 3); + + assertThat(account.baseAddress()).isEqualTo("addr_test1qz3cw2uuwjwhdjwyf32pre79kca5mf722nm09a6welje4edx0xezf9ug8educ8pv3gp2ujflk6jcatz9muf4jckak3fshq2mdz"); + assertThat(account.changeAddress()).isEqualTo("addr_test1qq8x3y83vfsx6zrcnuyseact0z85fyev6vjhrk5nfxzm57dx0xezf9ug8educ8pv3gp2ujflk6jcatz9muf4jckak3fsf894c3"); + assertThat(account.stakeAddress()).isEqualTo("stake_test1uzn8nv3yj7yruk7vrskg5q4wfylmdfvw43za7y6evtwmg5c47c0hf"); + assertThat(account.drepId()).isEqualTo("drep1yt07pz022vfqzwr40jrch8pwcd09g2s2nqqsffdjag9d33g2czftn"); + assertThat(account.committeeColdKey().id()).isEqualTo("cc_cold1ztw27g74fpg4n5wl556c4spzx8n5gz6njtf8lqcrehgvqaswaasts"); + assertThat(account.committeeHotKey().id()).isEqualTo("cc_hot1qgjg5c946hgkjh5mvpw8x4q7v9m0zykjvw0wrq5pfm04tvqqh35qa"); + } + + @Test + void testAccountFromAccountKey_128bytes_throwsException() { + //Added random 32 bytes at the end to test with a 128 bytes key + String accountKey = "e8b1a44e82b34230d516e87776e2c4b06f2fe943d954aae8a5e2e82d1b1de9435bb297dc928c0c56f2973802fa6b9f3966cd9060c3cd94bd2f6d7c2daf51a50b3375c40829f4b27836fac881d3ba20d5fd647baf8b749fa7d881a129643a9406c333ef7429361bdb1414e15e054f6654bce419d26057d0e38d76993f9c3ab71f"; + System.out.println(HexUtil.decodeHexString(accountKey)); + + assertThrows(Exception.class, () -> { + Account.createFromAccountKey(Networks.testnet(), HexUtil.decodeHexString(accountKey), 4, 3); + }); + } } diff --git a/crypto/src/main/java/com/bloxbean/cardano/client/crypto/bip32/HdKeyGenerator.java b/crypto/src/main/java/com/bloxbean/cardano/client/crypto/bip32/HdKeyGenerator.java index 5f6d0205..76b64a30 100644 --- a/crypto/src/main/java/com/bloxbean/cardano/client/crypto/bip32/HdKeyGenerator.java +++ b/crypto/src/main/java/com/bloxbean/cardano/client/crypto/bip32/HdKeyGenerator.java @@ -56,7 +56,7 @@ public HdKeyPair getAccountKeyPairFromSecretKey(byte[] xprv, DerivationPath deri return getKeyPairFromSecretKey(xprv, accountPath); } - private HdKeyPair getKeyPairFromSecretKey(byte[] xprv, String path) { + public HdKeyPair getKeyPairFromSecretKey(byte[] xprv, String path) { byte[] IL = Arrays.copyOfRange(xprv, 0, 64); byte[] IR = Arrays.copyOfRange(xprv, 64, 96); diff --git a/crypto/src/main/java/com/bloxbean/cardano/client/crypto/cip1852/CIP1852.java b/crypto/src/main/java/com/bloxbean/cardano/client/crypto/cip1852/CIP1852.java index 1251b970..52e5942b 100644 --- a/crypto/src/main/java/com/bloxbean/cardano/client/crypto/cip1852/CIP1852.java +++ b/crypto/src/main/java/com/bloxbean/cardano/client/crypto/cip1852/CIP1852.java @@ -27,6 +27,25 @@ public HdKeyPair getKeyPairFromMnemonic(String mnemonicPhrase, DerivationPath de } } + /** + * Generates the root HdKeyPair from the given mnemonic phrase. + * + * @param mnemonicPhrase the mnemonic phrase used to generate the HdKeyPair + * @return the root HdKeyPair derived from the provided mnemonic phrase + * @throws CryptoException if the mnemonic phrase cannot be converted to entropy or if + * the key pair generation fails + */ + public HdKeyPair getRootKeyPairFromMnemonic(String mnemonicPhrase) { + var hdKeyGenerator = new HdKeyGenerator(); + try { + byte[] entropy = MnemonicCode.INSTANCE.toEntropy(mnemonicPhrase); + + return hdKeyGenerator.getRootKeyPairFromEntropy(entropy); + } catch (Exception ex) { + throw new CryptoException("Mnemonic to KeyPair generation failed", ex); + } + } + /** * Get HdKeyPair from entropy * @param entropy entropy @@ -42,8 +61,7 @@ public HdKeyPair getKeyPairFromEntropy(byte[] entropy, DerivationPath derivation HdKeyPair accountKey = hdKeyGenerator.getChildKeyPair(coinTypeKey, derivationPath.getAccount().getValue(), derivationPath.getAccount().isHarden()); HdKeyPair roleKey = hdKeyGenerator.getChildKeyPair(accountKey, derivationPath.getRole().getValue(), derivationPath.getRole().isHarden()); - HdKeyPair indexKey = hdKeyGenerator.getChildKeyPair(roleKey, derivationPath.getIndex().getValue(), derivationPath.getIndex().isHarden()); - return indexKey; + return hdKeyGenerator.getChildKeyPair(roleKey, derivationPath.getIndex().getValue(), derivationPath.getIndex().isHarden()); } /** @@ -58,8 +76,7 @@ public HdKeyPair getKeyPairFromAccountKey(byte[] accountKey, DerivationPath deri HdKeyPair accountKeyPair = hdKeyGenerator.getAccountKeyPairFromSecretKey(accountKey, derivationPath); HdKeyPair roleKey = hdKeyGenerator.getChildKeyPair(accountKeyPair, derivationPath.getRole().getValue(), derivationPath.getRole().isHarden()); - HdKeyPair indexKey = hdKeyGenerator.getChildKeyPair(roleKey, derivationPath.getIndex().getValue(), derivationPath.getIndex().isHarden()); - return indexKey; + return hdKeyGenerator.getChildKeyPair(roleKey, derivationPath.getIndex().getValue(), derivationPath.getIndex().isHarden()); } /** @@ -106,4 +123,34 @@ public static HdPublicKey getPublicKeyFromAccountPubKey(HdPublicKey accountHdPub return hdKeyGenerator.getChildPublicKey(roleHdPubKey, index); } + /** + * Generates an HdKeyPair derived from the given root key and a specified derivation path. + * + * @param rootKey the root key represented as a byte array + * @param derivationPath the hierarchical deterministic (HD) derivation path used to derive the key pair + * @return the HdKeyPair derived from the provided root key and derivation path + */ + public HdKeyPair getKeyPairFromRootKey(byte[] rootKey, DerivationPath derivationPath) { + HdKeyGenerator hdKeyGenerator = new HdKeyGenerator(); + HdKeyPair rootKeyPair = hdKeyGenerator.getKeyPairFromSecretKey(rootKey, HdKeyGenerator.MASTER_PATH); + + HdKeyPair purposeKey = hdKeyGenerator.getChildKeyPair(rootKeyPair, derivationPath.getPurpose().getValue(), derivationPath.getPurpose().isHarden()); + HdKeyPair coinTypeKey = hdKeyGenerator.getChildKeyPair(purposeKey, derivationPath.getCoinType().getValue(), derivationPath.getCoinType().isHarden()); + HdKeyPair accountKey = hdKeyGenerator.getChildKeyPair(coinTypeKey, derivationPath.getAccount().getValue(), derivationPath.getAccount().isHarden()); + HdKeyPair roleKey = hdKeyGenerator.getChildKeyPair(accountKey, derivationPath.getRole().getValue(), derivationPath.getRole().isHarden()); + + return hdKeyGenerator.getChildKeyPair(roleKey, derivationPath.getIndex().getValue(), derivationPath.getIndex().isHarden()); + } + + /** + * Get an HdKeyPair from the given root key using the master derivation path. + * + * @param rootKey the root key represented as a byte array + * @return the HdKeyPair derived from the provided root key and the master derivation path + */ + public HdKeyPair getRootKeyPairFromRootKey(byte[] rootKey) { + HdKeyGenerator hdKeyGenerator = new HdKeyGenerator(); + return hdKeyGenerator.getKeyPairFromSecretKey(rootKey, HdKeyGenerator.MASTER_PATH); + } + } diff --git a/crypto/src/test/java/com/bloxbean/cardano/client/crypto/cip1852/CIP1852Test.java b/crypto/src/test/java/com/bloxbean/cardano/client/crypto/cip1852/CIP1852Test.java index de3c629a..311395d4 100644 --- a/crypto/src/test/java/com/bloxbean/cardano/client/crypto/cip1852/CIP1852Test.java +++ b/crypto/src/test/java/com/bloxbean/cardano/client/crypto/cip1852/CIP1852Test.java @@ -90,4 +90,41 @@ void getPublicKeyFromAccountPubKey_xpub_2() { assertThat(HexUtil.encodeHexString(hdPublicKey.getBytes())).isEqualTo(expectedStakePubKey); } + + @Test + void getRootKeyPairFromMnemonic() { + String mnemonic = "top exact spice seed cloud birth orient bracket happy cat section girl such outside elder"; + + var rootKeyPair = new CIP1852().getRootKeyPairFromMnemonic(mnemonic); + String expectedRootKeyBech32 = "root_xsk1zza6z52v8gelnaqdhuny3ywlccud5dtm8rvvyem4utnfwzcaa9pspsmdm99qfpy2qz7sw9sts59mrkegmdqyjen5ykm4z3ccyrkn8g5mm0qw35arvwxclfh6tj3s4x7t2q85wenvppjpxckcxgnf8vd80ug0l6rw"; + String expectedRootPvtKeyHex = HexUtil.encodeHexString(Bech32.decode(expectedRootKeyBech32).data); + + var rootPvtKey = rootKeyPair.getPrivateKey().getBytes(); + + assertThat(HexUtil.encodeHexString(rootPvtKey)).isEqualTo(expectedRootPvtKeyHex); + } + + @Test + void getKeyPairFromRootKeyAtDerivationPath() { + String rootKey = "root_xsk1zza6z52v8gelnaqdhuny3ywlccud5dtm8rvvyem4utnfwzcaa9pspsmdm99qfpy2qz7sw9sts59mrkegmdqyjen5ykm4z3ccyrkn8g5mm0qw35arvwxclfh6tj3s4x7t2q85wenvppjpxckcxgnf8vd80ug0l6rw"; + byte[] rootKeyBytes = Bech32.decode(rootKey).data; + + var addr0KeyPair = new CIP1852().getKeyPairFromRootKey(rootKeyBytes, DerivationPath.createExternalAddressDerivationPath(0)); + + //1852H/1815H/0H/0/0 + String expectedAddr0PvtKey = "addr_xsk1artlf4j6xz246j4xqtn6d595l7sy4vk0zuvzawlg7lwvq2qaa9pkujarca9j5ju08m0dlgw2qagauw693lvmrghujzvxsdfj99pwdm7xqwpgj5asad6nl5rzact6hune2xsl5x5gv2tds75ksdptavxr6se0fk8e"; + String expectedAddr0PvtKeyHex = HexUtil.encodeHexString(Bech32.decode(expectedAddr0PvtKey).data); + + assertThat(HexUtil.encodeHexString(addr0KeyPair.getPrivateKey().getBytes())).isEqualTo(expectedAddr0PvtKeyHex); + } + + @Test + void getRootKeyPairFromRootKey() { + String rootKeyBech32 = "root_xsk1zza6z52v8gelnaqdhuny3ywlccud5dtm8rvvyem4utnfwzcaa9pspsmdm99qfpy2qz7sw9sts59mrkegmdqyjen5ykm4z3ccyrkn8g5mm0qw35arvwxclfh6tj3s4x7t2q85wenvppjpxckcxgnf8vd80ug0l6rw"; + byte[] expectedRootKeyBytes = Bech32.decode(rootKeyBech32).data; + + var rootKeyPair = new CIP1852().getRootKeyPairFromRootKey(expectedRootKeyBytes); + + assertThat(HexUtil.encodeHexString(rootKeyPair.getPrivateKey().getBytes())).isEqualTo(HexUtil.encodeHexString(expectedRootKeyBytes)); + } } 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 ccd0055d..14ca57e4 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 @@ -9,6 +9,7 @@ import com.bloxbean.cardano.client.backend.api.BackendService; import com.bloxbean.cardano.client.cip.cip20.MessageMetadata; import com.bloxbean.cardano.client.common.model.Networks; +import com.bloxbean.cardano.client.crypto.Bech32; import com.bloxbean.cardano.client.exception.CborSerializationException; import com.bloxbean.cardano.client.function.helper.SignerProviders; import com.bloxbean.cardano.client.metadata.Metadata; @@ -39,6 +40,7 @@ public class QuickTxBuilderIT extends QuickTxBaseIT { DefaultWalletUtxoSupplier walletUtxoSupplier; Wallet wallet1; Wallet wallet2; + Wallet wallet3; static Account topupAccount; @@ -58,9 +60,12 @@ void setup() { utxoSupplier = getUTXOSupplier(); String wallet1Mnemonic = "clog book honey force cricket stamp until seed minimum margin denial kind volume undo simple federal then jealous solid legal crucial crazy acoustic thank"; - wallet1 = new Wallet(Networks.testnet(), wallet1Mnemonic); + wallet1 = Wallet.createFromMnemonic(Networks.testnet(), wallet1Mnemonic); String wallet2Mnemonic = "theme orphan remind output arrive lobster decorate ten gap piece casual distance attend total blast dilemma damp punch pride file limit soldier plug canoe"; - wallet2 = new Wallet(Networks.testnet(), wallet2Mnemonic); + wallet2 = Wallet.createFromMnemonic(Networks.testnet(), wallet2Mnemonic); + + String acctSk = "acct_xsk1azc6gn5zkdprp4gkapmhdckykphjl62rm9224699ut5z6xcaa9p4hv5hmjfgcrzk72tnsqh6dw0njekdjpsv8nv5h5hk6lpd4ag62zenwhzqs205kfurd7kgs8fm5gx4l4j8htutwj060kyp5y5kgw55qc8lsltd"; + wallet3 = Wallet.createFromAccountKey(Networks.testnet(), Bech32.decode(acctSk).data); walletUtxoSupplier = new DefaultWalletUtxoSupplier(backendService.getUtxoService(), wallet1); } @@ -71,15 +76,15 @@ void simplePayment() { metadata.put(BigInteger.valueOf(100), "This is first metadata"); metadata.putNegative(200, -900); + wallet1.setSearchUtxoByAddrVkh(true); //topup wallet - splitPaymentBetweenAddress(topupAccount, wallet1, 20, Double.valueOf(50000)); - + splitPaymentBetweenAddress(topupAccount, wallet1, 20, Double.valueOf(3000), true); UtxoSupplier walletUtxoSupplier = new DefaultWalletUtxoSupplier(backendService.getUtxoService(), wallet1); QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService, walletUtxoSupplier); Tx tx = new Tx() - .payToAddress(wallet2.getBaseAddress(0).getAddress(), Amount.ada(50000)) + .payToAddress(wallet2.getBaseAddress(0).getAddress(), Amount.ada(2000)) .from(wallet1); Result result = quickTxBuilder.compose(tx) @@ -99,7 +104,7 @@ void simplePayment() { @Test void simplePayment_withIndexesToScan() { String mnemonic = "buzz sentence empty coffee manage grid claw street misery deputy direct seek tortoise wedding stay twist crew august omit taste expect obscure abandon iron"; - Wallet wallet = new Wallet(Networks.testnet(), mnemonic); + Wallet wallet = Wallet.createFromMnemonic(Networks.testnet(), mnemonic); wallet.setIndexesToScan(new int[]{5, 30, 45}); //topup index 5, 45 @@ -169,7 +174,7 @@ void utxoTest() { assertTrue(!utxos.isEmpty()); } - void splitPaymentBetweenAddress(Account topupAccount, Wallet receiverWallet, int totalAddresses, Double adaAmount) { + void splitPaymentBetweenAddress(Account topupAccount, Wallet receiverWallet, int totalAddresses, Double adaAmount, boolean enableEntAddrPayment) { // Create an amount array with no of totalAddresses with random distribution of split amounts Double[] amounts = new Double[totalAddresses]; Double remainingAmount = adaAmount; @@ -187,7 +192,15 @@ void splitPaymentBetweenAddress(Account topupAccount, Wallet receiverWallet, int int currentIndex = 0; for (int i = 0; i < totalAddresses; i++) { - addresses[i] = receiverWallet.getBaseAddressString(currentIndex); + if (enableEntAddrPayment) { + if (i % 2 == 0) + addresses[i] = receiverWallet.getBaseAddressString(currentIndex); + else + addresses[i] = receiverWallet.getEntAddress(currentIndex).toBech32(); + } else { + addresses[i] = receiverWallet.getBaseAddressString(currentIndex); + } + currentIndex += random.nextInt(20) + 1; } @@ -205,4 +218,34 @@ void splitPaymentBetweenAddress(Account topupAccount, Wallet receiverWallet, int System.out.println(result); } + + @Test + void simplePayment_fromAccountKey() { + Metadata metadata = MetadataBuilder.createMetadata(); + metadata.put(BigInteger.valueOf(100), "This is first metadata"); + metadata.putNegative(200, -900); + + //topup wallet + splitPaymentBetweenAddress(topupAccount, wallet3, 20, Double.valueOf(3000), false); + + UtxoSupplier walletUtxoSupplier = new DefaultWalletUtxoSupplier(backendService.getUtxoService(), wallet3); + QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService, walletUtxoSupplier); + + Tx tx = new Tx() + .payToAddress(wallet2.getBaseAddress(0).getAddress(), Amount.ada(2000)) + .from(wallet3); + + Result result = quickTxBuilder.compose(tx) + .withSigner(SignerProviders.signerFrom(wallet3)) + .withTxInspector(txn -> { + System.out.println(JsonUtil.getPrettyJson(txn)); + }) + .complete(); + + System.out.println(result); + assertTrue(result.isSuccessful()); + waitForTransaction(result); + + checkIfUtxoAvailable(result.getValue(), wallet2.getBaseAddress(0).getAddress()); + } } 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 174a5d9f..2df0d9dc 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 @@ -59,9 +59,9 @@ static void beforeAll() { quickTxBuilder = new QuickTxBuilder(backendService); String wallet1Mnemonic = "clog book honey force cricket stamp until seed minimum margin denial kind volume undo simple federal then jealous solid legal crucial crazy acoustic thank"; - wallet1 = new Wallet(Networks.testnet(), wallet1Mnemonic); + wallet1 = Wallet.createFromMnemonic(Networks.testnet(), wallet1Mnemonic); String wallet2Mnemonic = "theme orphan remind output arrive lobster decorate ten gap piece casual distance attend total blast dilemma damp punch pride file limit soldier plug canoe"; - wallet2 = new Wallet(Networks.testnet(), wallet2Mnemonic); + wallet2 = Wallet.createFromMnemonic(Networks.testnet(), wallet2Mnemonic); if (backendType.equals(DEVKIT)) { poolId = "pool1wvqhvyrgwch4jq9aa84hc8q4kzvyq2z3xr6mpafkqmx9wce39zy"; diff --git a/hd-wallet/src/it/resources/log4j.xml b/hd-wallet/src/it/resources/log4j.xml new file mode 100644 index 00000000..61a0a727 --- /dev/null +++ b/hd-wallet/src/it/resources/log4j.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 be02a7b9..6386f6b2 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 @@ -30,16 +30,29 @@ public class Wallet { @Getter - private int account = 0; + private int accountNo = 0; @Getter private final Network network; + @Getter - private final String mnemonic; + @JsonIgnore + private String mnemonic; + + @JsonIgnore + private byte[] rootKey; //Pvt key at root level m/ + + @JsonIgnore + private byte[] accountKey; //Pvt key at account level m/1852'/1815'/x + private String stakeAddress; private Map cache; - private HdKeyPair rootKeys; + private HdKeyPair rootKeyPair; private HdKeyPair stakeKeys; + @Getter + @Setter + private boolean searchUtxoByAddrVkh; + @Getter @Setter private int[] indexesToScan; //If set, only scan these indexes and avoid gap limit during address scanning @@ -63,58 +76,154 @@ public Wallet(Network network, Words noOfWords) { public Wallet(Network network, Words noOfWords, int account) { this.network = network; this.mnemonic = MnemonicUtil.generateNew(noOfWords); - this.account = account; + this.accountNo = account; cache = new HashMap<>(); } - public Wallet(String mnemonic) { - this(Networks.mainnet(), mnemonic); + /** + * Creates a Wallet instance from the given mnemonic for mainnet. + * The account is set to zero. + * + * @param mnemonic the mnemonic phrase + * @return a Wallet instance created from the provided mnemonic + */ + public static Wallet createFromMnemonic(String mnemonic) { + return createFromMnemonic(Networks.mainnet(), mnemonic, 0); + } + + /** + * Creates a Wallet instance using the specified network and mnemonic phrase. + * The account is set to zero. + * + * @param network the network to be used, e.g., Networks.mainnet(), Networks.testnet() + * @param mnemonic the mnemonic phrase + * @return a Wallet instance created from the provided mnemonic + */ + public static Wallet createFromMnemonic(Network network, String mnemonic) { + return createFromMnemonic(network, mnemonic, 0); + } + + /** + * Creates a Wallet instance using the specified network, mnemonic phrase, and account number. + * + * @param network the network to be used, e.g., Networks.mainnet(), Networks.testnet() + * @param mnemonic the mnemonic phrase + * @param account the account no to be used for wallet derivation + * @return a Wallet instance created from the provided mnemonic + */ + public static Wallet createFromMnemonic(Network network, String mnemonic, int account) { + return new Wallet(network, mnemonic, null, null, account); + } + + /** + * Creates a Wallet instance using the specified network and root key. + * The account is set to zero by default. + * + * @param network the network to be used, e.g., Networks.mainnet(), Networks.testnet() + * @param rootKey the root key used for wallet initialization + * @return a Wallet instance created from the provided root key + */ + public static Wallet createFromRootKey(Network network, byte[] rootKey) { + return createFromRootKey(network, rootKey, 0); } - public Wallet(Network network, String mnemonic) { - this(network,mnemonic, 0); + /** + * Creates a Wallet instance using the specified network, root key, and account number. + * + * @param network the network to be used, e.g., Networks.mainnet(), Networks.testnet() + * @param rootKey the root key used for wallet initialization + * @param account the account number to be used for wallet derivation + * @return a Wallet instance created from the provided root key + */ + public static Wallet createFromRootKey(Network network, byte[] rootKey, int account) { + return new Wallet(network, null, rootKey, null, account); } - public Wallet(Network network, String mnemonic, int account) { + /** + * Creates a Wallet instance using the specified network and account level key. + * + * @param network the network to be used, e.g., Networks.mainnet(), Networks.testnet() + * @param accountKey the account key used for wallet initialization + * @return a Wallet instance created from the provided account key + */ + public static Wallet createFromAccountKey(Network network, byte[] accountKey) { + return new Wallet(network, null, null, accountKey, 0); + } + + /** + * Create a Wallet object from given mnemonic or rootKey or accountKey + * Only one of these value should be set : mnemonic or rootKey or accountKey + * @param network network + * @param mnemonic mnemonic + * @param rootKey root key + * @param accountKey account level key + * @param account account number + */ + private Wallet(Network network, String mnemonic, byte[] rootKey, byte[] accountKey, int account) { + //check if more than one value set and throw exception + if ((mnemonic != null && !mnemonic.isEmpty() ? 1 : 0) + + (rootKey != null && rootKey.length > 0 ? 1 : 0) + + (accountKey != null && accountKey.length > 0 ? 1 : 0) > 1) { + throw new WalletException("Only one of mnemonic, rootKey, or accountKey should be set."); + } + this.network = network; - this.mnemonic = mnemonic; - this.account = account; - MnemonicUtil.validateMnemonic(this.mnemonic); - cache = new HashMap<>(); + this.cache = new HashMap<>(); + + if (mnemonic != null && !mnemonic.isEmpty()) { + this.mnemonic = mnemonic; + this.accountNo = account; + MnemonicUtil.validateMnemonic(this.mnemonic); + } else if (rootKey != null && rootKey.length > 0) { + this.accountNo = account; + + if (rootKey.length == 96) + this.rootKey = rootKey; + else + throw new WalletException("Invalid length (Root Key): " + rootKey.length); + } else if (accountKey != null && accountKey.length > 0) { + this.accountNo = account; + + if (accountKey.length == 96) + this.accountKey = accountKey; + else + throw new WalletException("Invalid length (Account Private Key): " + accountKey.length); + } + } /** * Get Enterprise address for current account. Account can be changed via the setter. - * @param index - * @return + * @param index address index + * @return Address object with enterprise address */ public Address getEntAddress(int index) { - return getEntAddress(this.account, index); + return getEntAddress(this.accountNo, index); } /** * Get Enterprise address for derivation path m/1852'/1815'/{account}'/0/{index} - * @param account - * @param index - * @return + * @param account account no + * @param index address index + * @return Address object with Enterprise address */ private Address getEntAddress(int account, int index) { - return getAccount(account, index).getEnterpriseAddress(); + return getAccountNo(account, index).getEnterpriseAddress(); } /** * Get Baseaddress for current account. Account can be changed via the setter. - * @param index - * @return + * @param index address index + * @return Address object for Base address */ public Address getBaseAddress(int index) { - return getBaseAddress(this.account, index); + return getBaseAddress(this.accountNo, index); } /** * Get Baseaddress for current account as String. Account can be changed via the setter. - * @param index - * @return + * @param index address index + * @return Base address as string */ public String getBaseAddressString(int index) { return getBaseAddress(index).getAddress(); @@ -122,85 +231,111 @@ public String getBaseAddressString(int index) { /** * Get Baseaddress for derivationpath m/1852'/1815'/{account}'/0/{index} - * @param account - * @param index - * @return + * @param account account number + * @param index address index + * @return Address object for Base address */ public Address getBaseAddress(int account, int index) { - return getAccount(account,index).getBaseAddress(); + return getAccountNo(account,index).getBaseAddress(); } /** * Returns the Account object for the index and current account. Account can be changed via the setter. - * @param index - * @return + * @param index address index + * @return Account object */ - public Account getAccount(int index) { - return getAccount(this.account, index); + public Account getAccountAtIndex(int index) { + return getAccountNo(this.accountNo, index); } /** * Returns the Account object for the index and account. - * @param account - * @param index - * @return + * @param account account number + * @param index address index + * @return Account object */ - public Account getAccount(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); + public Account getAccountNo(int account, int index) { + if(account != this.accountNo) { + return deriveAccount(account, index); } else { if(cache.containsKey(index)) { return cache.get(index); } else { - Account acc = new Account(this.network, this.mnemonic, index); - cache.put(index, acc); + Account acc = deriveAccount(account, index); + if (acc != null) + cache.put(index, acc); + return acc; } } } + private Account deriveAccount(int account, int index) { + DerivationPath derivationPath = DerivationPath.createExternalAddressDerivationPathForAccount(account); + derivationPath.getIndex().setValue(index); + + if (mnemonic != null && !mnemonic.isEmpty()) { + return Account.createFromMnemonic(this.network, this.mnemonic, derivationPath); + } else if (rootKey != null && rootKey.length > 0) { + return Account.createFromRootKey(this.network, this.rootKey, derivationPath); + } else if (accountKey != null && accountKey.length > 0) { + return Account.createFromAccountKey(this.network, this.accountKey, derivationPath); + }else { + throw new WalletException("Can't create Account. At least one of 'mnemonic', 'accountKey', or 'rootKey' must be set."); + } + } + /** * Setting the current account for derivation path. * Setting the account will reset the cache. - * @param account + * @param account account number which will be set in the wallet */ - public void setAccount(int account) { - this.account = account; + public void setAccountNo(int account) { + this.accountNo = account; // invalidating cache since it is only held for one account cache = new HashMap<>(); } /** * Returns the RootkeyPair - * @return + * @return Root key as HdKeyPair if non-empty else empty optional */ @JsonIgnore - public HdKeyPair getRootKeyPair() { - 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 WalletException("Unable to derive root key pair", e); + public Optional getRootKeyPair() { + if(rootKeyPair == null) { + if (mnemonic != null && !mnemonic.isEmpty()) { + HdKeyGenerator hdKeyGenerator = new HdKeyGenerator(); + try { + byte[] entropy = MnemonicCode.INSTANCE.toEntropy(this.mnemonic); + rootKeyPair = hdKeyGenerator.getRootKeyPairFromEntropy(entropy); + } catch (MnemonicException.MnemonicLengthException | MnemonicException.MnemonicWordException | + MnemonicException.MnemonicChecksumException e) { + throw new WalletException("Unable to derive root key pair", e); + } + } else if (rootKey != null && rootKey.length > 0) { + HdKeyGenerator hdKeyGenerator = new HdKeyGenerator(); + rootKeyPair = hdKeyGenerator.getKeyPairFromSecretKey(rootKey, HdKeyGenerator.MASTER_PATH); } } - return rootKeys; + + return Optional.ofNullable(rootKeyPair); + } + + public Optional getRootPvtKey() { + return getRootKeyPair() + .map(rkp -> rkp.getPrivateKey().getBytes()); } /** * Finds needed signers within wallet and signs the transaction with each one - * @param txToSign + * @param txToSign transaction * @return signed Transaction */ public Transaction sign(Transaction txToSign, Set utxos) { Map accountMap = utxos.stream() .map(WalletUtxo::getDerivationPath) .filter(Objects::nonNull) - .map(derivationPath -> getAccount( + .map(derivationPath -> getAccountNo( derivationPath.getAccount().getValue(), derivationPath.getIndex().getValue())) .collect(Collectors.toMap( @@ -267,7 +402,7 @@ public Transaction sign(Transaction txToSign, Set utxos) { /** * Returns the stake address of the wallet. - * @return + * @return Stake address as string */ public String getStakeAddress() { if (stakeAddress == null || stakeAddress.isEmpty()) { @@ -280,8 +415,8 @@ public String getStakeAddress() { /** * Signs the transaction with stake key from wallet. - * @param transaction - * @return + * @param transaction transaction object to sign + * @return Signed transaction object */ public Transaction signWithStakeKey(Transaction transaction) { return TransactionSigner.INSTANCE.sign(transaction, getStakeKeyPair()); @@ -289,7 +424,7 @@ public Transaction signWithStakeKey(Transaction transaction) { private HdKeyPair getStakeKeyPair() { if(stakeKeys == null) { - DerivationPath stakeDerivationPath = DerivationPath.createStakeAddressDerivationPathForAccount(this.account); + DerivationPath stakeDerivationPath = DerivationPath.createStakeAddressDerivationPathForAccount(this.accountNo); stakeKeys = new CIP1852().getKeyPairFromMnemonic(mnemonic, stakeDerivationPath); } return stakeKeys; 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 095573bc..ea65ff1a 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 @@ -1,5 +1,6 @@ package com.bloxbean.cardano.hdwallet.supplier; +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; @@ -13,6 +14,7 @@ import com.bloxbean.cardano.hdwallet.WalletException; import com.bloxbean.cardano.hdwallet.model.WalletUtxo; import lombok.Setter; +import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.Collections; @@ -20,6 +22,7 @@ import java.util.Optional; import java.util.stream.Collectors; +@Slf4j public class DefaultWalletUtxoSupplier implements WalletUtxoSupplier { private final UtxoService utxoService; @Setter @@ -32,7 +35,11 @@ public DefaultWalletUtxoSupplier(UtxoService utxoService, Wallet wallet) { @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 + //All utxos should be fetched at page=0 + if (page == 0) + return getAll(address); // todo get Page of utxo over multipe addresses - find a good way to aktually do something with page, nrOfItems and order + else + return Collections.emptyList(); } @Override @@ -62,7 +69,7 @@ public List getAll() { int noUtxoFound = 0; while (noUtxoFound < wallet.getGapLimit()) { - List utxoFromIndex = getUtxosForAccountAndIndex(wallet.getAccount(), index); + List utxoFromIndex = getUtxosForAccountAndIndex(wallet.getAccountNo(), index); utxos.addAll(utxoFromIndex); noUtxoFound = utxoFromIndex.isEmpty() ? noUtxoFound + 1 : 0; @@ -71,7 +78,7 @@ public List getAll() { } } else { for (int idx: wallet.getIndexesToScan()) { - List utxoFromIndex = getUtxosForAccountAndIndex(wallet.getAccount(), idx); + List utxoFromIndex = getUtxosForAccountAndIndex(wallet.getAccountNo(), idx); utxos.addAll(utxoFromIndex); } } @@ -81,13 +88,28 @@ public List getAll() { @Override public List getUtxosForAccountAndIndex(int account, int index) { checkIfWalletIsSet(); - String address = wallet.getBaseAddress(account, index).getAddress(); + + Address address = getBaseAddress(account, index); + + if (log.isDebugEnabled()) + log.debug("Scanning address for account: {}, index: {}", account, index); + + String addrStr = address.getAddress(); + String addrVkh = address.getBech32VerificationKeyHash().orElse(null); + List utxos = new ArrayList<>(); int page = 1; while(true) { - Result> result = null; + Result> result; + + String searchKey; + if (wallet.isSearchUtxoByAddrVkh()) + searchKey = addrVkh; + else + searchKey = addrStr; + try { - result = utxoService.getUtxos(address, UtxoSupplier.DEFAULT_NR_OF_ITEMS_TO_FETCH, page, OrderEnum.asc); + result = utxoService.getUtxos(searchKey, UtxoSupplier.DEFAULT_NR_OF_ITEMS_TO_FETCH, page, OrderEnum.asc); } catch (ApiException e) { throw new ApiRuntimeException(e); } @@ -103,13 +125,17 @@ public List getUtxosForAccountAndIndex(int account, int index) { }).collect(Collectors.toList()); utxos.addAll(utxoList); - if(utxoPage.size() < 100) + if(utxoPage.size() < DEFAULT_NR_OF_ITEMS_TO_FETCH) break; page++; } return utxos; } + private Address getBaseAddress(int account, int index) { + return wallet.getBaseAddress(account, index); + } + private void checkIfWalletIsSet() { if(this.wallet == null) 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 d123692e..ecb5bcf9 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,11 +3,15 @@ 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.Bech32; import com.bloxbean.cardano.client.crypto.bip39.Words; +import com.bloxbean.cardano.client.util.HexUtil; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; public class WalletTest { @@ -54,7 +58,7 @@ void WalletAddressToAccountAddressTest() { @Test void testGetBaseAddressFromMnemonicIndex_0() { - Wallet wallet = new Wallet(Networks.mainnet(), phrase24W); + Wallet wallet = Wallet.createFromMnemonic(Networks.mainnet(), phrase24W); Assertions.assertEquals(baseAddress0, wallet.getBaseAddressString(0)); Assertions.assertEquals(baseAddress1, wallet.getBaseAddressString(1)); Assertions.assertEquals(baseAddress2, wallet.getBaseAddressString(2)); @@ -63,7 +67,7 @@ void testGetBaseAddressFromMnemonicIndex_0() { @Test void testGetBaseAddressFromMnemonicByNetworkInfoTestnet() { - Wallet wallet = new Wallet(Networks.testnet(), phrase24W); + Wallet wallet = Wallet.createFromMnemonic(Networks.testnet(), phrase24W); Assertions.assertEquals(testnetBaseAddress0, wallet.getBaseAddressString(0)); Assertions.assertEquals(testnetBaseAddress1, wallet.getBaseAddressString(1)); Assertions.assertEquals(testnetBaseAddress2, wallet.getBaseAddressString(2)); @@ -71,7 +75,7 @@ void testGetBaseAddressFromMnemonicByNetworkInfoTestnet() { @Test void testGetEnterpriseAddressFromMnemonicIndex() { - Wallet wallet = new Wallet(Networks.mainnet(), phrase24W); + Wallet wallet = Wallet.createFromMnemonic(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 +83,7 @@ void testGetEnterpriseAddressFromMnemonicIndex() { @Test void testGetEnterpriseAddressFromMnemonicIndexByNetwork() { - Wallet wallet = new Wallet(Networks.testnet(), phrase24W); + Wallet wallet = Wallet.createFromMnemonic(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,15 +91,138 @@ void testGetEnterpriseAddressFromMnemonicIndexByNetwork() { @Test void testGetPublicKeyBytesFromMnemonic() { - byte[] pubKey = new Wallet(phrase24W).getRootKeyPair().getPublicKey().getKeyData(); + byte[] pubKey = Wallet.createFromMnemonic(phrase24W).getRootKeyPair().get().getPublicKey().getKeyData(); Assertions.assertEquals(32, pubKey.length); } @Test void testGetPrivateKeyBytesFromMnemonic() { - byte[] pvtKey = new Wallet(phrase24W).getRootKeyPair().getPrivateKey().getBytes(); + byte[] pvtKey = Wallet.createFromMnemonic(phrase24W).getRootKeyPair().get().getPrivateKey().getBytes(); Assertions.assertEquals(96, pvtKey.length); } + @Test + void testAccountFromAccountKey0() { + String accountPrivateKey = "a83aa0356397602d3da7648f139ca06be2465caef14ac4d795b17cdf13bd0f4fe9aac037f7e22335cd99495b963d54f21e8dae540112fe56243b287962da366fd4016f4cfb6d6baba1807621b4216d18581c38404c4768fe820204bef98ba706"; + String address0 = "addr_test1qzsaa6czesrzwp45rd5flg86n5hnwhz5setqfyt39natwvsl5mr3vkp82y2kcwxxtu4zjcxvm80ttmx2hyeyjka4v8psa5ns0z"; + + Wallet wallet = Wallet.createFromAccountKey(Networks.testnet(), HexUtil.decodeHexString(accountPrivateKey)); + assertThat(wallet.getBaseAddressString(0)).isEqualTo(address0); + } + + @Test + void testAccountFromRootKey() { + String rootKey = "xprv1frqqvtmax6a5lqv5h6e8vt2wxglasnweglnap8dclz69fd62zp2kqccn08nmjah5rct9zvuh3mx4dln9z984hf42474q6jp2frn3ahkxxaau9y2yfvrr7ex4nw24g37flvarqfhy87g99kp20yknqn7kgs04h87k"; + byte[] rootKeyBytes = Bech32.decode(rootKey).data; + + Wallet wallet = Wallet.createFromRootKey(Networks.testnet(), rootKeyBytes); + assertThat(wallet.getBaseAddressString(0)).isEqualTo("addr_test1qzm0439fe55aynh58qcn4jnh4mwuqwr5n5fez7j0hck9ds8j3nmg5pkqfur4gyupppuu82r83s5eheewzmf6fwlzfz7qzsp6rc"); + } + + @Test + void testAccountFromAccountKey_0() { + //original phrase + //fresh apple bus punch dynamic what arctic elevator logic hole survey hunt better adapt helmet fat refuse season enter category tomato mule capable faith + + //at account=0 + String accountPrvKey = "acct_xsk14zau3uj79pxh2wplfnezeetj3ms5wfgvyltg3jap0ch5cuswq3qe39l4aty2wjgtyzagzc8squ0hz6hrej6ypqdrj4yhxynapsf462ypgv3clpf74q56k6r32847a4cp9dlx6n8ew8hyqdv6ydv5q8yt9vhn8ktv"; + byte[] accountPrvKeyBytes = Bech32.decode(accountPrvKey).data; + + Wallet wallet = Wallet.createFromAccountKey(Networks.testnet(), accountPrvKeyBytes); + Account account = wallet.getAccountAtIndex(0); + + assertThat(wallet.getBaseAddressString(0)).isEqualTo("addr_test1qq7x3pklemucwtw6qcym6trkcfmenslhnsq7e8cag7h82507mkje2hyjgc8zr00z835as0kw5hwa48h3vnrctjpu4a8qy8lf5r"); + assertThat(account.changeAddress()).isEqualTo("addr_test1qr430yp4r7mgsj5t34pgp2zcv37w8arj62gyqtdayheejz87mkje2hyjgc8zr00z835as0kw5hwa48h3vnrctjpu4a8qy27g5x"); + assertThat(account.stakeAddress()).isEqualTo("stake_test1urldmfv4tjfyvr3phh3rc6wc8m82thw6nmckf3u9eq727ns3kwf9u"); + assertThat(account.drepId()).isEqualTo("drep1y2zjw9adazlmychc6wlz4k8qa8g5jcjqwujg0jws9r6v9egvasg8r"); + assertThat(account.committeeColdKey().id()).isEqualTo("cc_cold1ztslrgxse5awd9yx9csqrcmystzw6rd88tva4tw7lqkjrtc6d4ezh"); + assertThat(account.committeeHotKey().id()).isEqualTo("cc_hot1qgmfvk4g7vfquys52cdrx59ez948q337jhp5ycde3umlprqulcyff"); + + assertThat(wallet.getRootKeyPair()).isEmpty(); + } + + @Test + void testGetRootKeyWhenFromMnemonic() { + String mnemonic = "fresh apple bus punch dynamic what arctic elevator logic hole survey hunt better adapt helmet fat refuse season enter category tomato mule capable faith"; + + String expectedRootKey = "root_xsk1xrvg8kfpdlaluwstt0twcajgavqcgkczmav6lffvgfmrxeqwq3qer3rasrjdj9f663xa98xcu4a28zuv5cks5lytdvfezn49ycrndz2mptat9v0t5eafsdj9rpe4lcxndvys0v6qahq8v0flv9ycpav8ks46k8xh"; + byte[] expectedRootKeyBytes = Bech32.decode(expectedRootKey).data; + + Wallet wallet = Wallet.createFromMnemonic(Networks.testnet(), mnemonic); + + assertThat(HexUtil.encodeHexString(wallet.getRootPvtKey().get())).isEqualTo(HexUtil.encodeHexString(expectedRootKeyBytes)); + } + + @Test + void testGetRootKeyWhenFromRootKey() { + String expectedRootKey = "root_xsk1xrvg8kfpdlaluwstt0twcajgavqcgkczmav6lffvgfmrxeqwq3qer3rasrjdj9f663xa98xcu4a28zuv5cks5lytdvfezn49ycrndz2mptat9v0t5eafsdj9rpe4lcxndvys0v6qahq8v0flv9ycpav8ks46k8xh"; + byte[] expectedRootKeyBytes = Bech32.decode(expectedRootKey).data; + Wallet wallet = Wallet.createFromRootKey(Networks.testnet(), expectedRootKeyBytes); + + assertThat(HexUtil.encodeHexString(wallet.getRootPvtKey().get())).isEqualTo(HexUtil.encodeHexString(expectedRootKeyBytes)); + } + + @Test + void testAccountWhenFromRootKey_account_2() { + //original phrase + //fresh apple bus punch dynamic what arctic elevator logic hole survey hunt better adapt helmet fat refuse season enter category tomato mule capable faith + + String rootKey = "root_xsk1xrvg8kfpdlaluwstt0twcajgavqcgkczmav6lffvgfmrxeqwq3qer3rasrjdj9f663xa98xcu4a28zuv5cks5lytdvfezn49ycrndz2mptat9v0t5eafsdj9rpe4lcxndvys0v6qahq8v0flv9ycpav8ks46k8xh"; + byte[] rootKeyBytes = Bech32.decode(rootKey).data; + + Wallet wallet = Wallet.createFromRootKey(Networks.testnet(), rootKeyBytes, 2); + + assertThat(wallet.getBaseAddressString(0)).isEqualTo("addr_test1qpyf7633hxe5t5lwr20dre9xy54nuhl4qf53rvpcs3geq5mgypzw9uc2ug5dsdet3gtvkd7f32f4q6a5x3afwk5m6y7sfpmuak"); + assertThat(wallet.getAccountAtIndex(0).changeAddress()).isEqualTo("addr_test1qqphrf9wtwwhghevuy0gd95cldmdzn3gyrganlh7g8d8hvrgypzw9uc2ug5dsdet3gtvkd7f32f4q6a5x3afwk5m6y7s6xvl48"); + assertThat(wallet.getAccountAtIndex(0).stakeAddress()).isEqualTo("stake_test1up5zq38z7v9wy2xcxu4c59ktxlyc4y6sdw6rg75ht2daz0guncpsl"); + assertThat(wallet.getAccountAtIndex(0).drepId()).isEqualTo("drep1ygmmj4nqjzvaa39qxj6w7pxpx4u62f8lvnu4xzjwjk8segg56qful"); + assertThat(wallet.getAccountAtIndex(0).committeeColdKey().id()).isEqualTo("cc_cold1zftrxuz0u938d8mu5ndawz8yv6rkurfq8flrus6mcr32fhspk4ju3"); + assertThat(wallet.getAccountAtIndex(0).committeeHotKey().id()).isEqualTo("cc_hot1qtx827hr45paddz5h6w7k0rg40vpc79262dff8r42rlwxgq4jj6dg"); + + assertThat(wallet.getAccountAtIndex(8).baseAddress()).isEqualTo("addr_test1qqacfd33e3qa20vqmv00qx6xftxhcqwhlr905l4rchf5j0rgypzw9uc2ug5dsdet3gtvkd7f32f4q6a5x3afwk5m6y7sa0sjfq"); + + assertThat(HexUtil.encodeHexString(wallet.getRootPvtKey().get())).isEqualTo(HexUtil.encodeHexString(rootKeyBytes)); + } + + @Test + void testAccountWhenFromAccountKey_15words_acc4_index3() { + //Original mnemonic + //top exact spice seed cloud birth orient bracket happy cat section girl such outside elder + + String accountKey = "acct_xsk1azc6gn5zkdprp4gkapmhdckykphjl62rm9224699ut5z6xcaa9p4hv5hmjfgcrzk72tnsqh6dw0njekdjpsv8nv5h5hk6lpd4ag62zenwhzqs205kfurd7kgs8fm5gx4l4j8htutwj060kyp5y5kgw55qc8lsltd"; + + Wallet wallet = Wallet.createFromAccountKey(Networks.testnet(), Bech32.decode(accountKey).data); + + assertThat(wallet.getAccountAtIndex(3).baseAddress()).isEqualTo("addr_test1qz3cw2uuwjwhdjwyf32pre79kca5mf722nm09a6welje4edx0xezf9ug8educ8pv3gp2ujflk6jcatz9muf4jckak3fshq2mdz"); + assertThat(wallet.getAccountAtIndex(3).changeAddress()).isEqualTo("addr_test1qq8x3y83vfsx6zrcnuyseact0z85fyev6vjhrk5nfxzm57dx0xezf9ug8educ8pv3gp2ujflk6jcatz9muf4jckak3fsf894c3"); + assertThat(wallet.getAccountAtIndex(3).stakeAddress()).isEqualTo("stake_test1uzn8nv3yj7yruk7vrskg5q4wfylmdfvw43za7y6evtwmg5c47c0hf"); + assertThat(wallet.getAccountAtIndex(3).drepId()).isEqualTo("drep1yt07pz022vfqzwr40jrch8pwcd09g2s2nqqsffdjag9d33g2czftn"); + assertThat(wallet.getAccountAtIndex(3).committeeColdKey().id()).isEqualTo("cc_cold1ztw27g74fpg4n5wl556c4spzx8n5gz6njtf8lqcrehgvqaswaasts"); + assertThat(wallet.getAccountAtIndex(3).committeeHotKey().id()).isEqualTo("cc_hot1qgjg5c946hgkjh5mvpw8x4q7v9m0zykjvw0wrq5pfm04tvqqh35qa"); + } + + @Test + void testAccountFromAccountKey_128bytes_throwsException() { + //Added random 32 bytes at the end to test with a 128 bytes key + String accountKey = "e8b1a44e82b34230d516e87776e2c4b06f2fe943d954aae8a5e2e82d1b1de9435bb297dc928c0c56f2973802fa6b9f3966cd9060c3cd94bd2f6d7c2daf51a50b3375c40829f4b27836fac881d3ba20d5fd647baf8b749fa7d881a129643a9406c333ef7429361bdb1414e15e054f6654bce419d26057d0e38d76993f9c3ab71f"; + + var network = Networks.testnet(); + var acctKeyBytes = HexUtil.decodeHexString(accountKey); + assertThrows(WalletException.class, () -> { + Wallet.createFromAccountKey(network, acctKeyBytes); + }); + } + + @Test + void testAccountFromRootKey_128Bytes_throwsException() { + //Last 32 bytes in 128 bytes array are arbitary bytes + String rootKey128Bytes = "48c0062f7d36bb4f8194beb2762d4e323fd84dd947e7d09db8f8b454b74a105560631379e7b976f41e165133978ecd56fe65114f5ba6aaafaa0d482a48e71edec6377bc291444b063f64d59b955447c9fb3a3026e43f9052d82a792d304fd644c333ef7429361bdb1414e15e054f6654bce419d26057d0e38d76993f9c3ab71f"; + byte[] rootKey = HexUtil.decodeHexString(rootKey128Bytes); + + var network = Networks.testnet(); + assertThrows(WalletException.class, () -> { + Wallet.createFromRootKey(network, rootKey); + }); + } } 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 index 7aafeb80..108fdaaf 100644 --- 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 @@ -40,10 +40,10 @@ public void setup() { @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(); + var addr1 = wallet.getAccountAtIndex(3).baseAddress(); + var addr2 = wallet.getAccountAtIndex(7).baseAddress(); + var addr3 = wallet.getAccountAtIndex(25).baseAddress(); + var addr4 = wallet.getAccountAtIndex(50).baseAddress(); DefaultWalletUtxoSupplier utxoSupplier = new DefaultWalletUtxoSupplier(utxoService, wallet); @@ -106,10 +106,10 @@ void getAll() throws ApiException { 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(); + var addr1 = wallet.getAccountAtIndex(3).baseAddress(); + var addr2 = wallet.getAccountAtIndex(7).baseAddress(); + var addr3 = wallet.getAccountAtIndex(25).baseAddress(); + var addr4 = wallet.getAccountAtIndex(50).baseAddress(); DefaultWalletUtxoSupplier utxoSupplier = new DefaultWalletUtxoSupplier(utxoService, wallet);