diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a44a07e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +name: CI + +on: + push: + branches: [ master, main ] + pull_request: + branches: [ master, main ] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + java: [ '8', '11', '17' ] + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK ${{ matrix.java }} + uses: actions/setup-java@v4 + with: + java-version: ${{ matrix.java }} + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build + run: ./gradlew build + + - name: Run tests + run: ./gradlew test + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-java-${{ matrix.java }} + path: build/reports/tests/test/ + retention-days: 7 diff --git a/.gitignore b/.gitignore index 0c6bd14..f3ed390 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,30 @@ +# Gradle .gradle/ build/ +/build +/*/build/ + +# IDE .settings/ .project .classpath -*.class -.idea +.idea/ *.iml -/local.properties -.DS_Store -/build +*.ipr +*.iws +.vscode/ + +# Compiled +*.class /*/out/ -out +out/ + +# OS +.DS_Store +Thumbs.db + +# Properties +/local.properties + +# Test output +bin/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 8f5a439..0000000 --- a/.travis.yml +++ /dev/null @@ -1,9 +0,0 @@ -matrix: - include: - - name: build - script: - - "./gradlew build" - -skip_build: - - README.md: - - LICENSE diff --git a/README.md b/README.md index 4337ac5..7dce965 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@

Tokencore

- - Build Status + + Build Status Issues @@ -34,6 +34,11 @@ Tokencore is a central component for blockchain wallet backends. It currently su - TRX, TRC20, BCH, BSV - DOGE, DASH, LTC, FILECOIN +## Requirements + +- Java 8+ +- Gradle 8.5+ (included via wrapper) + ## Integration ### Gradle @@ -43,7 +48,7 @@ repositories { maven { url 'https://jitpack.io' } } dependencies { - compile 'com.github.galaxyscitech:tokencore:1.2.7' + implementation 'com.github.galaxyscitech:tokencore:1.3.0' } ``` @@ -51,10 +56,6 @@ dependencies { ```xml - - tronj - https://dl.bintray.com/tronj/tronj - jitpack.io https://jitpack.io @@ -62,17 +63,13 @@ dependencies { - com.github.galaxyzxcv + com.github.galaxyscitech tokencore - 1.2.7 + 1.3.0 ``` -## Sample Test - -View a sample test at [Tokencore Test Sample](https://github.com/galaxyscitech/tokencore/blob/master/src/test/java/org/consenlabs/tokencore/Test.java). - -## Usage Guide +## Quick Start ### Initialize Identity @@ -81,12 +78,17 @@ try { Files.createDirectories(Paths.get("${keyStoreProperties.dir}/wallets")); } catch(Throwable ignored) {} -WalletManager.storage = new KeystoreStorage(); +WalletManager.storage = new KeystoreStorage() { + @Override + public File getKeystoreDir() { + return new File("/path/to/keystore"); + } +}; WalletManager.scanWallets(); -String password = "123456"; +String password = "your_password"; Identity identity = Identity.getCurrentIdentity(); -if(identity == null) { +if (identity == null) { Identity.createIdentity("token", password, "", Network.MAINNET, Metadata.P2WPKH); } ``` @@ -95,23 +97,22 @@ if(identity == null) { ```java Identity identity = Identity.getCurrentIdentity(); -String password = "123456"; -Wallet wallet = identity.deriveWalletByMnemonics(ChainType.BITCOIN, password, MnemonicUtil.randomMnemonicCodes()); +String password = "your_password"; +Wallet wallet = identity.deriveWalletByMnemonics( + ChainType.BITCOIN, password, MnemonicUtil.randomMnemonicCodes()); System.out.println(wallet.getAddress()); ``` ## Offline Signature -Offline signing refers to the process of creating a digital signature for a transaction without connecting to the internet. This method enhances security by ensuring private keys never come in contact with an online environment. Here's how you can create an offline signature with Tokencore for Bitcoin and TRON: +Offline signing refers to the process of creating a digital signature for a transaction without connecting to the internet. This method enhances security by ensuring private keys never come in contact with an online environment. ### Bitcoin 1. **Set Up Transaction Details** - Define the details of your Bitcoin transaction, including recipient's address, change index, amount to be transferred, and the fee. - ```java - String password = "123456"; + String password = "your_password"; String toAddress = "33sXfhCBPyHqeVsVthmyYonCBshw5XJZn9"; int changeIdx = 0; long amount = 1000L; @@ -128,39 +129,67 @@ Offline signing refers to the process of creating a digital signature for a tran 3. **Initialize Transaction & Sign** - With all the details in place, initialize the Bitcoin transaction and sign it offline. - ```java - BitcoinTransaction bitcoinTransaction = new BitcoinTransaction(toAddress, changeIdx, amount, fee, utxos); - Wallet wallet = WalletManager.findWalletByAddress(ChainType.BITCOIN, "33sXfhCBPyHqeVsVthmyYonCBshw5XJZn9"); - TxSignResult txSignResult = bitcoinTransaction.signTransaction(String.valueOf(ChainId.BITCOIN_MAINNET), password, wallet); - System.out.println(txSignResult); + BitcoinTransaction bitcoinTransaction = new BitcoinTransaction( + toAddress, changeIdx, amount, fee, utxos); + Wallet wallet = WalletManager.findWalletByAddress( + ChainType.BITCOIN, "33sXfhCBPyHqeVsVthmyYonCBshw5XJZn9"); + TxSignResult txSignResult = bitcoinTransaction.signTransaction( + String.valueOf(ChainId.BITCOIN_MAINNET), password, wallet); + System.out.println(txSignResult.getSignedTx()); ``` ### TRON 1. **Set Up Transaction Details** - Define your TRON transaction details, including the sender's address, recipient's address, and amount. - ```java String from = "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8"; String to = "TF17BgPaZYbz8oxbjhriubPDsA7ArKoLX3"; long amount = 1; - String password = "123456"; + String password = "your_password"; ``` 2. **Initialize Transaction & Sign** - Once you have the transaction details, initialize the TRON transaction and sign it offline. - ```java TronTransaction transaction = new TronTransaction(from, to, amount); - Wallet wallet = WalletManager.findWalletByAddress(ChainType.BITCOIN, "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8"); - TxSignResult txSignResult = transaction.signTransaction(String.valueOf(ChainId.BITCOIN_MAINNET), password, wallet); - System.out.println(txSignResult); + Wallet wallet = WalletManager.findWalletByAddress( + ChainType.TRON, "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8"); + TxSignResult txSignResult = transaction.signTransaction( + "mainnet", password, wallet); + System.out.println(txSignResult.getSignedTx()); ``` -Remember, offline signing enhances security but requires a thorough understanding of transaction construction to avoid errors. +### Ethereum + +```java +EthereumTransaction tx = new EthereumTransaction( + BigInteger.ZERO, // nonce + BigInteger.valueOf(20_000_000_000L), // gasPrice + BigInteger.valueOf(21000), // gasLimit + "0xRecipientAddress", // to + BigInteger.valueOf(1_000_000_000_000_000_000L), // value (1 ETH) + "" // data +); + +Wallet wallet = WalletManager.findWalletByAddress( + ChainType.ETHEREUM, "0xYourAddress"); +TxSignResult result = tx.signTransaction( + String.valueOf(ChainId.ETHEREUM_MAINNET), password, wallet); +System.out.println(result.getSignedTx()); +``` + +## Running Tests + +```bash +./gradlew test +``` + +## Building + +```bash +./gradlew build +``` > **Note**: Tokencore is a functional component for digital currency. It's primarily for learning purposes and doesn't offer a complete blockchain business suite. diff --git a/build.gradle b/build.gradle index 6dd0547..32e3111 100644 --- a/build.gradle +++ b/build.gradle @@ -1,56 +1,75 @@ -apply plugin: 'java' -apply plugin: 'com.github.dcendents.android-maven' +plugins { + id 'java-library' + id 'maven-publish' +} +group = 'com.github.paipaipaipai' +version = '1.3.0' -group 'com.github.paipaipaipai' -version '1.2.1' +sourceCompatibility = JavaVersion.VERSION_1_8 +targetCompatibility = JavaVersion.VERSION_1_8 -sourceCompatibility = 1.8 -//noinspection GroovyAssignabilityCheck allprojects { - repositories { - jcenter() - maven { url "https://jitpack.io" } - maven { url 'https://maven.aliyun.com/nexus/content/groups/public/' } mavenCentral() + maven { url 'https://jitpack.io' } } - } +java { + withSourcesJar() + withJavadocJar() +} -buildscript { - repositories { - jcenter() - } - dependencies { - //添加jitpack依赖 - classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' - } +javadoc { + options.encoding = 'UTF-8' + options.addStringOption('Xdoclint:none', '-quiet') + failOnError = false } dependencies { - compile 'io.github.qyvlik:io.eblock.eos-eos4j:1.0.1' - compile 'com.fasterxml.jackson.core:jackson-databind:2.15.2' - compile 'org.bitcoinj:bitcoinj-core:0.16.2' -// compile group: 'com.google.protobuf', name: 'protobuf-lite', version: '3.0.1' -// compile 'com.google.protobuf:protobuf-java:3.5.1' - compile 'org.json:json:20230618' + implementation 'io.github.qyvlik:io.eblock.eos-eos4j:1.0.1' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2' + implementation 'org.bitcoinj:bitcoinj-core:0.14.7' + implementation 'org.json:json:20230618' - compile 'com.github.paipaipaipai:FilecoinJ:0.1.1' - compile 'com.google.protobuf:protobuf-java:3.24.3' + implementation 'com.github.paipaipaipai:FilecoinJ:0.1.1' + implementation 'com.google.protobuf:protobuf-java:3.24.3' + implementation 'com.github.lailaibtc:trident:0.0.3' + implementation 'com.google.guava:guava:32.1.2-jre' + implementation 'io.grpc:grpc-stub:1.58.0' - compile 'com.github.lailaibtc:trident:0.0.3' + implementation 'cn.hutool:hutool-core:5.8.22' + implementation 'com.alibaba:fastjson:1.2.83' + implementation 'org.apache.commons:commons-lang3:3.13.0' - compile 'com.google.guava:guava:32.1.2-jre' - -// compile 'com.madgag.spongycastle:core:1.58.0.0' + testImplementation platform('org.junit:junit-bom:5.10.1') + testImplementation 'org.junit.jupiter:junit-jupiter' + testImplementation 'org.junit.jupiter:junit-jupiter-params' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} - compile 'io.grpc:grpc-stub:1.58.0' +test { + useJUnitPlatform() + testLogging { + events 'passed', 'skipped', 'failed' + showExceptions true + showCauses true + showStackTraces true + } } -processResources { - from('src/main/java/resources') { - include '**' +publishing { + publications { + maven(MavenPublication) { + from components.java + groupId = project.group + artifactId = rootProject.name + version = project.version + } } -} \ No newline at end of file +} + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 85be2c2..1af9e09 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ -#Tue Jan 22 19:05:01 CST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.1-all.zip diff --git a/src/main/java/org/consenlabs/tokencore/wallet/Identity.java b/src/main/java/org/consenlabs/tokencore/wallet/Identity.java index 4fab14c..091207e 100755 --- a/src/main/java/org/consenlabs/tokencore/wallet/Identity.java +++ b/src/main/java/org/consenlabs/tokencore/wallet/Identity.java @@ -1,6 +1,5 @@ package org.consenlabs.tokencore.wallet; -import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; @@ -212,8 +211,8 @@ private static Identity tryLoadFromFile() { try { File file = new File(WalletManager.getDefaultKeyDirectory(), FILE_NAME); ObjectMapper mapper = new ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - mapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true); + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true); IdentityKeystore keystore = mapper.readValue(file, IdentityKeystore.class); return new Identity(keystore); } catch (IOException ignored) { @@ -224,9 +223,7 @@ private static Identity tryLoadFromFile() { private void flush() { try { File file = new File(WalletManager.getDefaultKeyDirectory(), FILE_NAME); - ObjectMapper objectMapper = new ObjectMapper(); - objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); - objectMapper.writeValue(file, this.keystore); + WalletManager.getWriteMapper().writeValue(file, this.keystore); } catch (IOException ex) { throw new TokenException(Messages.WALLET_STORE_FAIL, ex); } diff --git a/src/main/java/org/consenlabs/tokencore/wallet/Wallet.java b/src/main/java/org/consenlabs/tokencore/wallet/Wallet.java index d19ba42..3a5c759 100755 --- a/src/main/java/org/consenlabs/tokencore/wallet/Wallet.java +++ b/src/main/java/org/consenlabs/tokencore/wallet/Wallet.java @@ -1,7 +1,6 @@ package org.consenlabs.tokencore.wallet; import cn.hutool.core.codec.Base64; -import cn.hutool.core.util.HexUtil; import com.alibaba.fastjson.JSON; import com.fasterxml.jackson.databind.ObjectMapper; import org.bitcoinj.crypto.ChildNumber; @@ -13,7 +12,6 @@ import org.consenlabs.tokencore.wallet.model.*; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -63,6 +61,12 @@ MnemonicAndPath exportMnemonic(String password) { return null; } + private static final ObjectMapper EXPORT_MAPPER = new ObjectMapper(); + + static { + EXPORT_MAPPER.addMixIn(IMTKeystore.class, V3Ignore.class); + } + String exportKeystore(String password) { if (keystore instanceof ExportableKeystore) { if (!keystore.verifyPassword(password)) { @@ -70,9 +74,7 @@ String exportKeystore(String password) { } try { - ObjectMapper mapper = new ObjectMapper(); - mapper.addMixIn(IMTKeystore.class, V3Ignore.class); - return mapper.writeValueAsString(keystore); + return EXPORT_MAPPER.writeValueAsString(keystore); } catch (Exception ex) { throw new TokenException(Messages.WALLET_INVALID, ex); } diff --git a/src/main/java/org/consenlabs/tokencore/wallet/WalletManager.java b/src/main/java/org/consenlabs/tokencore/wallet/WalletManager.java index f1464d3..f588ac4 100755 --- a/src/main/java/org/consenlabs/tokencore/wallet/WalletManager.java +++ b/src/main/java/org/consenlabs/tokencore/wallet/WalletManager.java @@ -24,21 +24,27 @@ import java.io.IOException; import java.nio.charset.Charset; import java.util.Arrays; -import java.util.Hashtable; import java.util.List; +import java.util.concurrent.ConcurrentHashMap; public class WalletManager { - private static Hashtable keystoreMap = new Hashtable<>(); - private static final String LOG_TAG = WalletManager.class.getSimpleName(); + private static final ConcurrentHashMap keystoreMap = new ConcurrentHashMap<>(); + + private static final ObjectMapper WRITE_MAPPER = new ObjectMapper(); + private static final ObjectMapper READ_MAPPER = new ObjectMapper(); + + static { + WRITE_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL); + READ_MAPPER.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true); + READ_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + READ_MAPPER.configure(DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES, true); + } public static KeystoreStorage storage; -// -// static { -// try {ø -// scanWallets(); -// } catch (IOException ignored) { -// } -// } + + static ObjectMapper getWriteMapper() { + return WRITE_MAPPER; + } static Wallet createWallet(IMTKeystore keystore) { File file = generateWalletFile(keystore.getId()); @@ -47,7 +53,7 @@ static Wallet createWallet(IMTKeystore keystore) { return new Wallet(keystore); } - public static Hashtable getKeyMap(){ + public static ConcurrentHashMap getKeyMap(){ return keystoreMap; } @@ -161,16 +167,23 @@ public static Wallet importWalletFromMnemonic(Metadata metadata, @Nullable Strin MnemonicUtil.validateMnemonics(mnemonicCodes); switch (metadata.getChainType()) { case ChainType.ETHEREUM: + case ChainType.TRON: + case ChainType.FILECOIN: keystore = V3MnemonicKeystore.create(metadata, password, mnemonicCodes, path); break; case ChainType.BITCOIN: - keystore = HDMnemonicKeystore.create(metadata, password, mnemonicCodes, path); - break; case ChainType.LITECOIN: + case ChainType.DASH: + case ChainType.DOGECOIN: + case ChainType.BITCOINCASH: + case ChainType.BITCOINSV: keystore = HDMnemonicKeystore.create(metadata, password, mnemonicCodes, path); break; case ChainType.EOS: keystore = EOSKeystore.create(metadata, password, accountName, mnemonicCodes, path, permissions); + break; + default: + throw new TokenException(String.format("Mnemonic import not supported for chain: %s", metadata.getChainType())); } return persistWallet(keystore, overwrite); @@ -334,9 +347,7 @@ private static Wallet flushWallet(IMTKeystore keystore, boolean overwrite) { private static void writeToFile(Keystore keyStore, File destination) { try { - ObjectMapper objectMapper = new ObjectMapper(); - objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); - objectMapper.writeValue(destination, keyStore); + WRITE_MAPPER.writeValue(destination, keyStore); } catch (IOException ex) { throw new TokenException(Messages.WALLET_STORE_FAIL, ex); } @@ -345,10 +356,11 @@ private static void writeToFile(Keystore keyStore, File destination) { private static boolean deleteDir(File dir) { if (dir.isDirectory()) { String[] children = dir.list(); - for (String child : children) { - boolean success = deleteDir(new File(dir, child)); - if (!success) { - return false; + if (children != null) { + for (String child : children) { + if (!deleteDir(new File(dir, child))) { + return false; + } } } } @@ -384,24 +396,22 @@ private static IMTKeystore mustFindKeystoreById(String id) { } private static T unmarshalKeystore(String keystoreContent, Class clazz) { - T importedKeystore; try { - ObjectMapper mapper = new ObjectMapper(); - mapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true); - mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - mapper.configure(DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES, true); - importedKeystore = mapper.readValue(keystoreContent, clazz); + return READ_MAPPER.readValue(keystoreContent, clazz); } catch (IOException ex) { throw new TokenException(Messages.WALLET_INVALID_KEYSTORE, ex); } - return importedKeystore; } public static void scanWallets() { File directory = getDefaultKeyDirectory(); keystoreMap.clear(); - for (File file : directory.listFiles()) { + File[] files = directory.listFiles(); + if (files == null) { + return; + } + for (File file : files) { if (!file.getName().startsWith("identity")) { try { IMTKeystore keystore = null; @@ -428,8 +438,7 @@ public static void scanWallets() { if (keystore != null) { keystoreMap.put(keystore.getId(), keystore); } - } catch (Exception ex) { -// log.info(LOG_TAG, "Can't loaded " + file.getName() + " file", ex); + } catch (Exception ignored) { } } } diff --git a/src/main/java/org/consenlabs/tokencore/wallet/keystore/HDMnemonicKeystore.java b/src/main/java/org/consenlabs/tokencore/wallet/keystore/HDMnemonicKeystore.java index 12879bb..cc95b44 100755 --- a/src/main/java/org/consenlabs/tokencore/wallet/keystore/HDMnemonicKeystore.java +++ b/src/main/java/org/consenlabs/tokencore/wallet/keystore/HDMnemonicKeystore.java @@ -156,11 +156,10 @@ public String newReceiveAddress(int nextIdx) { public static class Info { - private String curve = "spec256k1"; - private String purpuse = "sign"; + private String curve = "secp256k1"; + private String purpose = "sign"; public Info() { - } public String getCurve() { @@ -171,12 +170,22 @@ public void setCurve(String curve) { this.curve = curve; } + public String getPurpose() { + return purpose; + } + + public void setPurpose(String purpose) { + this.purpose = purpose; + } + + @Deprecated public String getPurpuse() { - return purpuse; + return purpose; } + @Deprecated public void setPurpuse(String purpuse) { - this.purpuse = purpuse; + this.purpose = purpuse; } } } diff --git a/src/main/java/org/consenlabs/tokencore/wallet/keystore/V3Keystore.java b/src/main/java/org/consenlabs/tokencore/wallet/keystore/V3Keystore.java index 7f757bc..4c36958 100755 --- a/src/main/java/org/consenlabs/tokencore/wallet/keystore/V3Keystore.java +++ b/src/main/java/org/consenlabs/tokencore/wallet/keystore/V3Keystore.java @@ -5,10 +5,7 @@ import org.consenlabs.tokencore.foundation.crypto.Crypto; import org.consenlabs.tokencore.foundation.utils.MetaUtil; import org.consenlabs.tokencore.foundation.utils.NumericUtil; -import org.consenlabs.tokencore.wallet.address.BitcoinAddressCreator; -import org.consenlabs.tokencore.wallet.address.EthereumAddressCreator; -import org.consenlabs.tokencore.wallet.address.SegWitBitcoinAddressCreator; -import org.consenlabs.tokencore.wallet.address.TronAddressCreator; +import org.consenlabs.tokencore.wallet.address.*; import org.consenlabs.tokencore.wallet.model.ChainType; import org.consenlabs.tokencore.wallet.model.Metadata; import org.consenlabs.tokencore.wallet.model.TokenException; @@ -55,8 +52,12 @@ public V3Keystore(Metadata metadata, String password, String prvKeyHex, String i prvKeyBytes = NumericUtil.hexToBytes(prvKeyHex); new PrivateKeyValidator(prvKeyHex).validate(); this.address = new TronAddressCreator().fromPrivateKey(prvKeyBytes); - }else { - throw new TokenException("Can't init eos keystore in this constructor"); + } else if (metadata.getChainType().equals(ChainType.FILECOIN)) { + prvKeyBytes = NumericUtil.hexToBytes(prvKeyHex); + new PrivateKeyValidator(prvKeyHex).validate(); + this.address = new FilecoinAddressCreator().fromPrivateKey(prvKeyBytes); + } else { + throw new TokenException(String.format("Unsupported chain type for V3Keystore: %s", metadata.getChainType())); } this.crypto = Crypto.createPBKDF2Crypto(password, prvKeyBytes); metadata.setWalletType(Metadata.V3); diff --git a/src/main/java/org/consenlabs/tokencore/wallet/transaction/BitcoinTransaction.java b/src/main/java/org/consenlabs/tokencore/wallet/transaction/BitcoinTransaction.java index 0d5f8aa..d67184e 100755 --- a/src/main/java/org/consenlabs/tokencore/wallet/transaction/BitcoinTransaction.java +++ b/src/main/java/org/consenlabs/tokencore/wallet/transaction/BitcoinTransaction.java @@ -1,6 +1,5 @@ package org.consenlabs.tokencore.wallet.transaction; -import org.apache.commons.lang3.StringUtils; import org.bitcoinj.core.*; import org.bitcoinj.crypto.ChildNumber; import org.bitcoinj.crypto.DeterministicKey; diff --git a/src/main/java/org/consenlabs/tokencore/wallet/transaction/FileTransaction.java b/src/main/java/org/consenlabs/tokencore/wallet/transaction/FileTransaction.java index 1dc587a..22fb1cd 100644 --- a/src/main/java/org/consenlabs/tokencore/wallet/transaction/FileTransaction.java +++ b/src/main/java/org/consenlabs/tokencore/wallet/transaction/FileTransaction.java @@ -55,11 +55,10 @@ public TxSignResult signTransaction(String chainId, String password, Wallet wall String cid = HexUtil.encodeHexStr(cidHash); ECKey ecKey = ECKey.fromPrivate(HexUtil.decodeHex(hexPrivateKey)); - String sing = Base64.encode(ecKey.sign(cidHash).toByteArray()); - System.out.println("cidHash: " + HexUtil.encodeHexStr(cidHash)); - return new TxSignResult(sing, cid); + String sign = Base64.encode(ecKey.sign(cidHash).toByteArray()); + return new TxSignResult(sign, cid); } catch (Exception e) { - throw new TokenException("签名失败 原因", e); + throw new TokenException("Filecoin sign failed", e); } } } diff --git a/src/main/java/org/consenlabs/tokencore/wallet/transaction/TronTransaction.java b/src/main/java/org/consenlabs/tokencore/wallet/transaction/TronTransaction.java index 22467d8..bac5c1f 100644 --- a/src/main/java/org/consenlabs/tokencore/wallet/transaction/TronTransaction.java +++ b/src/main/java/org/consenlabs/tokencore/wallet/transaction/TronTransaction.java @@ -67,44 +67,57 @@ public TronTransaction(String from, String to, String contractAddress, long amou private String contractAddress; - ApiWrapper client = ApiWrapper.ofMainnet(""); + private volatile ApiWrapper client; + + public TronTransaction setClient(ApiWrapper client) { + this.client = client; + return this; + } + + private ApiWrapper getClient() { + if (client == null) { + synchronized (this) { + if (client == null) { + client = ApiWrapper.ofMainnet(""); + } + } + } + return client; + } @Override public TxSignResult signTransaction(String chainId, String password, Wallet wallet) { - String hexPrivateKey = wallet.exportPrivateKey(password); SECP256K1.KeyPair keyPair = SECP256K1.KeyPair.create(SECP256K1.PrivateKey.create(Bytes32.fromHexString(hexPrivateKey))); - Response.TransactionExtention txnExt; try { - - txnExt = client.transfer(from, to, amount); - Chain.Transaction signedTransaction = client.signTransaction(txnExt, keyPair); + ApiWrapper api = getClient(); + Response.TransactionExtention txnExt = api.transfer(from, to, amount); + Chain.Transaction signedTransaction = api.signTransaction(txnExt, keyPair); String txid = Hex.toHexString(txnExt.getTxid().toByteArray()); return new TxSignResult(signedTransaction.toString(), txid); } catch (IllegalException e) { - throw new TokenException("签名失败 原因", e); + throw new TokenException("Tron sign failed", e); } } public TxSignResult signTrc10Transaction(String chainId, String password, Wallet wallet) { String hexPrivateKey = wallet.exportPrivateKey(password); SECP256K1.KeyPair keyPair = SECP256K1.KeyPair.create(SECP256K1.PrivateKey.create(Bytes32.fromHexString(hexPrivateKey))); - Response.TransactionExtention txnExt; try { - txnExt = client.transferTrc10(from, to, tokenId, amount); - Chain.Transaction signedTransaction = client.signTransaction(txnExt, keyPair); + ApiWrapper api = getClient(); + Response.TransactionExtention txnExt = api.transferTrc10(from, to, tokenId, amount); + Chain.Transaction signedTransaction = api.signTransaction(txnExt, keyPair); String txid = Hex.toHexString(txnExt.getTxid().toByteArray()); return new TxSignResult(signedTransaction.toString(), txid); } catch (IllegalException e) { - throw new TokenException("签名失败 原因", e); + throw new TokenException("Tron TRC10 sign failed", e); } } - public TxSignResult signTrc20Transaction(String chainId, String password, Wallet wallet) { String hexPrivateKey = wallet.exportPrivateKey(password); SECP256K1.KeyPair keyPair = SECP256K1.KeyPair.create(SECP256K1.PrivateKey.create(Bytes32.fromHexString(hexPrivateKey))); - // transfer(address,uint256) returns (bool) + Function trc20Transfer = new Function("transfer", Arrays.asList(new Address(to), new Uint256(BigInteger.valueOf(amount))), @@ -120,10 +133,10 @@ public TxSignResult signTrc20Transaction(String chainId, String password, Wallet .setData(ApiWrapper.parseHex(encodedHex)) .build(); - Response.TransactionExtention txnExt = client.blockingStub.triggerContract(trigger); + ApiWrapper api = getClient(); + Response.TransactionExtention txnExt = api.blockingStub.triggerContract(trigger); String txid = Hex.toHexString(txnExt.getTxid().toByteArray()); - - Chain.Transaction signedTxn = client.signTransaction(txnExt, keyPair); + Chain.Transaction signedTxn = api.signTransaction(txnExt, keyPair); return new TxSignResult(signedTxn.toString(), txid); } diff --git a/src/test/java/org/consenlabs/tokencore/foundation/utils/MnemonicUtilTest.java b/src/test/java/org/consenlabs/tokencore/foundation/utils/MnemonicUtilTest.java new file mode 100644 index 0000000..32c234c --- /dev/null +++ b/src/test/java/org/consenlabs/tokencore/foundation/utils/MnemonicUtilTest.java @@ -0,0 +1,67 @@ +package org.consenlabs.tokencore.foundation.utils; + +import org.consenlabs.tokencore.wallet.model.TokenException; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class MnemonicUtilTest { + + @Test + void randomMnemonicCodes_shouldGenerate12Words() { + List codes = MnemonicUtil.randomMnemonicCodes(); + assertEquals(12, codes.size()); + for (String word : codes) { + assertNotNull(word); + assertFalse(word.isEmpty()); + } + } + + @Test + void randomMnemonicCodes_shouldBeDifferentEachTime() { + List a = MnemonicUtil.randomMnemonicCodes(); + List b = MnemonicUtil.randomMnemonicCodes(); + assertNotEquals(a, b); + } + + @Test + void randomMnemonicStr_shouldReturnSpaceSeparatedWords() { + String mnemonic = MnemonicUtil.randomMnemonicStr(); + assertNotNull(mnemonic); + String[] words = mnemonic.split(" "); + assertEquals(12, words.length); + } + + @Test + void validateMnemonics_shouldAcceptValid() { + List codes = MnemonicUtil.randomMnemonicCodes(); + assertDoesNotThrow(() -> MnemonicUtil.validateMnemonics(codes)); + } + + @Test + void validateMnemonics_shouldRejectInvalidLength() { + List tooShort = Arrays.asList("abandon", "abandon", "abandon"); + assertThrows(TokenException.class, () -> MnemonicUtil.validateMnemonics(tooShort)); + } + + @Test + void validateMnemonics_shouldRejectInvalidWords() { + List invalid = Arrays.asList( + "notaword", "notaword", "notaword", "notaword", + "notaword", "notaword", "notaword", "notaword", + "notaword", "notaword", "notaword", "notaword" + ); + assertThrows(TokenException.class, () -> MnemonicUtil.validateMnemonics(invalid)); + } + + @Test + void toMnemonicCodes_shouldConvertEntropy() { + byte[] entropy = new byte[16]; + List codes = MnemonicUtil.toMnemonicCodes(entropy); + assertEquals(12, codes.size()); + MnemonicUtil.validateMnemonics(codes); + } +} diff --git a/src/test/java/org/consenlabs/tokencore/foundation/utils/NumericUtilTest.java b/src/test/java/org/consenlabs/tokencore/foundation/utils/NumericUtilTest.java new file mode 100644 index 0000000..0a04be2 --- /dev/null +++ b/src/test/java/org/consenlabs/tokencore/foundation/utils/NumericUtilTest.java @@ -0,0 +1,136 @@ +package org.consenlabs.tokencore.foundation.utils; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.math.BigInteger; + +import static org.junit.jupiter.api.Assertions.*; + +class NumericUtilTest { + + @Test + void generateRandomBytes_shouldReturnCorrectLength() { + byte[] bytes16 = NumericUtil.generateRandomBytes(16); + assertEquals(16, bytes16.length); + + byte[] bytes32 = NumericUtil.generateRandomBytes(32); + assertEquals(32, bytes32.length); + } + + @Test + void generateRandomBytes_shouldReturnDifferentValues() { + byte[] a = NumericUtil.generateRandomBytes(32); + byte[] b = NumericUtil.generateRandomBytes(32); + assertNotEquals(NumericUtil.bytesToHex(a), NumericUtil.bytesToHex(b)); + } + + @ParameterizedTest + @ValueSource(strings = {"0xabcdef", "0XABCDEF", "abcdef", "1234", "0x00ff"}) + void isValidHex_shouldReturnTrueForValidHex(String hex) { + assertTrue(NumericUtil.isValidHex(hex)); + } + + @ParameterizedTest + @NullSource + void isValidHex_shouldReturnFalseForNull(String hex) { + assertFalse(NumericUtil.isValidHex(hex)); + } + + @ParameterizedTest + @ValueSource(strings = {"", "0x", "xyz", "0xgg"}) + void isValidHex_shouldReturnFalseForInvalidHex(String hex) { + assertFalse(NumericUtil.isValidHex(hex)); + } + + @Test + void cleanHexPrefix_shouldRemovePrefix() { + assertEquals("abcdef", NumericUtil.cleanHexPrefix("0xabcdef")); + assertEquals("abcdef", NumericUtil.cleanHexPrefix("abcdef")); + } + + @Test + void prependHexPrefix_shouldAddPrefix() { + assertEquals("0xabcdef", NumericUtil.prependHexPrefix("abcdef")); + assertEquals("0xabcdef", NumericUtil.prependHexPrefix("0xabcdef")); + } + + @Test + void hexToBytes_shouldConvertCorrectly() { + byte[] bytes = NumericUtil.hexToBytes("0x0102ff"); + assertEquals(3, bytes.length); + assertEquals(1, bytes[0]); + assertEquals(2, bytes[1]); + assertEquals((byte) 0xff, bytes[2]); + } + + @Test + void hexToBytes_emptyInput() { + byte[] bytes = NumericUtil.hexToBytes(""); + assertEquals(0, bytes.length); + } + + @Test + void bytesToHex_shouldConvertCorrectly() { + byte[] bytes = {0x01, 0x02, (byte) 0xff}; + assertEquals("0102ff", NumericUtil.bytesToHex(bytes)); + } + + @Test + void bytesToHex_emptyArray() { + assertEquals("", NumericUtil.bytesToHex(new byte[0])); + } + + @Test + void roundTrip_hexConversion() { + String original = "deadbeef01020304"; + String result = NumericUtil.bytesToHex(NumericUtil.hexToBytes(original)); + assertEquals(original, result); + } + + @Test + void hexToBigInteger_shouldConvert() { + BigInteger result = NumericUtil.hexToBigInteger("0xff"); + assertEquals(BigInteger.valueOf(255), result); + } + + @Test + void bigIntegerToHex_shouldConvert() { + String hex = NumericUtil.bigIntegerToHex(BigInteger.valueOf(255)); + assertEquals("ff", hex); + } + + @Test + void bigIntegerToBytesWithZeroPadded_correctPadding() { + byte[] result = NumericUtil.bigIntegerToBytesWithZeroPadded(BigInteger.ONE, 32); + assertEquals(32, result.length); + assertEquals(1, result[31]); + for (int i = 0; i < 31; i++) { + assertEquals(0, result[i]); + } + } + + @Test + void bigIntegerToBytesWithZeroPadded_throwsOnTooLargeInput() { + BigInteger tooLarge = BigInteger.ONE.shiftLeft(256); + assertThrows(RuntimeException.class, + () -> NumericUtil.bigIntegerToBytesWithZeroPadded(tooLarge, 32)); + } + + @Test + void intToBytes_andBack() { + byte[] bytes = NumericUtil.intToBytes(256); + int result = NumericUtil.bytesToInt(new byte[]{0, 0, bytes[0], bytes[1]}); + assertEquals(256, result); + } + + @Test + void reverseBytes_shouldReverse() { + byte[] input = {1, 2, 3, 4}; + byte[] reversed = NumericUtil.reverseBytes(input); + assertArrayEquals(new byte[]{4, 3, 2, 1}, reversed); + } +} diff --git a/src/test/java/org/consenlabs/tokencore/wallet/WalletManagerTest.java b/src/test/java/org/consenlabs/tokencore/wallet/WalletManagerTest.java new file mode 100644 index 0000000..b044bdb --- /dev/null +++ b/src/test/java/org/consenlabs/tokencore/wallet/WalletManagerTest.java @@ -0,0 +1,180 @@ +package org.consenlabs.tokencore.wallet; + +import org.consenlabs.tokencore.foundation.utils.MnemonicUtil; +import org.consenlabs.tokencore.wallet.model.*; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.*; + +class WalletManagerTest { + + @TempDir + Path tempDir; + + @BeforeEach + void setUp() { + WalletManager.storage = () -> tempDir.toFile(); + WalletManager.clearKeystoreMap(); + Identity.currentIdentity = null; + } + + @Test + void scanWallets_emptyDirectory_shouldNotThrow() { + assertDoesNotThrow(() -> WalletManager.scanWallets()); + assertTrue(WalletManager.getKeyMap().isEmpty()); + } + + @Test + void createIdentity_shouldCreateWallets() { + Identity identity = Identity.createIdentity( + "test", "password123", "", Network.MAINNET, Metadata.P2WPKH); + + assertNotNull(identity); + assertNotNull(identity.getIdentifier()); + assertFalse(identity.getWallets().isEmpty()); + } + + @Test + void deriveEthereumWallet_shouldReturnValidAddress() { + Identity identity = Identity.createIdentity( + "test", "password123", "", Network.MAINNET, Metadata.P2WPKH); + + Wallet wallet = identity.deriveWalletByMnemonics( + ChainType.ETHEREUM, "password123", MnemonicUtil.randomMnemonicCodes()); + + assertNotNull(wallet); + assertNotNull(wallet.getAddress()); + assertEquals(40, wallet.getAddress().length()); + } + + @Test + void deriveBitcoinWallet_shouldReturnValidAddress() { + Identity identity = Identity.createIdentity( + "test", "password123", "", Network.MAINNET, Metadata.P2WPKH); + + Wallet wallet = identity.deriveWalletByMnemonics( + ChainType.BITCOIN, "password123", MnemonicUtil.randomMnemonicCodes()); + + assertNotNull(wallet); + assertNotNull(wallet.getAddress()); + } + + @Test + void findWalletByAddress_shouldReturnWallet() { + Identity identity = Identity.createIdentity( + "test", "password123", "", Network.MAINNET, Metadata.P2WPKH); + + Wallet ethWallet = identity.deriveWalletByMnemonics( + ChainType.ETHEREUM, "password123", MnemonicUtil.randomMnemonicCodes()); + + Wallet found = WalletManager.findWalletByAddress( + ChainType.ETHEREUM, ethWallet.getAddress()); + assertNotNull(found); + assertEquals(ethWallet.getAddress(), found.getAddress()); + } + + @Test + void findWalletByAddress_notFound_shouldReturnNull() { + Identity.createIdentity( + "test", "password123", "", Network.MAINNET, Metadata.P2WPKH); + + Wallet found = WalletManager.findWalletByAddress( + ChainType.ETHEREUM, "nonexistentaddress"); + assertNull(found); + } + + @Test + void exportPrivateKey_shouldReturnHexKey() { + Identity identity = Identity.createIdentity( + "test", "password123", "", Network.MAINNET, Metadata.P2WPKH); + + Wallet ethWallet = identity.deriveWalletByMnemonics( + ChainType.ETHEREUM, "password123", MnemonicUtil.randomMnemonicCodes()); + + String privateKey = WalletManager.exportPrivateKey(ethWallet.getId(), "password123"); + assertNotNull(privateKey); + assertTrue(privateKey.length() > 0); + } + + @Test + void exportMnemonic_shouldReturnMnemonicAndPath() { + Identity identity = Identity.createIdentity( + "test", "password123", "", Network.MAINNET, Metadata.P2WPKH); + + Wallet ethWallet = identity.deriveWalletByMnemonics( + ChainType.ETHEREUM, "password123", MnemonicUtil.randomMnemonicCodes()); + + MnemonicAndPath result = WalletManager.exportMnemonic(ethWallet.getId(), "password123"); + assertNotNull(result); + assertNotNull(result.getMnemonic()); + String[] words = result.getMnemonic().split(" "); + assertEquals(12, words.length); + } + + @Test + void changePassword_shouldWork() { + Identity identity = Identity.createIdentity( + "test", "password123", "", Network.MAINNET, Metadata.P2WPKH); + + Wallet ethWallet = identity.deriveWalletByMnemonics( + ChainType.ETHEREUM, "password123", MnemonicUtil.randomMnemonicCodes()); + + assertDoesNotThrow(() -> + WalletManager.changePassword(ethWallet.getId(), "password123", "newpass456")); + } + + @Test + void removeWallet_shouldRemoveFromMap() { + Identity identity = Identity.createIdentity( + "test", "password123", "", Network.MAINNET, Metadata.P2WPKH); + + Wallet ethWallet = identity.deriveWalletByMnemonics( + ChainType.ETHEREUM, "password123", MnemonicUtil.randomMnemonicCodes()); + + String walletId = ethWallet.getId(); + assertNotNull(WalletManager.findWalletById(walletId)); + + WalletManager.removeWallet(walletId, "password123"); + assertNull(WalletManager.findWalletById(walletId)); + } + + @Test + void removeWallet_wrongPassword_shouldThrow() { + Identity identity = Identity.createIdentity( + "test", "password123", "", Network.MAINNET, Metadata.P2WPKH); + + Wallet ethWallet = identity.deriveWalletByMnemonics( + ChainType.ETHEREUM, "password123", MnemonicUtil.randomMnemonicCodes()); + + assertThrows(TokenException.class, () -> + WalletManager.removeWallet(ethWallet.getId(), "wrongpassword")); + } + + @Test + void mustFindWalletById_nonExistent_shouldThrow() { + assertThrows(TokenException.class, () -> + WalletManager.mustFindWalletById("nonexistent-id")); + } + + @Test + void importWalletFromPrivateKey_ethereum() { + Identity.createIdentity( + "test", "password123", "", Network.MAINNET, Metadata.P2WPKH); + + Metadata metadata = new Metadata(); + metadata.setChainType(ChainType.ETHEREUM); + metadata.setSource(Metadata.FROM_PRIVATE); + + String privKey = "4c0883a69102937d6231471b5dbb6204fe512961708279f14a15c89a7e5a5c3c"; + Wallet wallet = WalletManager.importWalletFromPrivateKey( + metadata, privKey, "password123", true); + + assertNotNull(wallet); + assertNotNull(wallet.getAddress()); + assertEquals(40, wallet.getAddress().length()); + } +} diff --git a/src/test/java/org/consenlabs/tokencore/wallet/address/AddressCreatorManagerTest.java b/src/test/java/org/consenlabs/tokencore/wallet/address/AddressCreatorManagerTest.java new file mode 100644 index 0000000..00c6d8e --- /dev/null +++ b/src/test/java/org/consenlabs/tokencore/wallet/address/AddressCreatorManagerTest.java @@ -0,0 +1,59 @@ +package org.consenlabs.tokencore.wallet.address; + +import org.consenlabs.tokencore.wallet.model.ChainType; +import org.consenlabs.tokencore.wallet.model.Metadata; +import org.consenlabs.tokencore.wallet.model.TokenException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class AddressCreatorManagerTest { + + @Test + void getInstance_ethereum_shouldReturnEthereumCreator() { + AddressCreator creator = AddressCreatorManager.getInstance( + ChainType.ETHEREUM, true, null); + assertInstanceOf(EthereumAddressCreator.class, creator); + } + + @Test + void getInstance_tron_shouldReturnTronCreator() { + AddressCreator creator = AddressCreatorManager.getInstance( + ChainType.TRON, true, null); + assertInstanceOf(TronAddressCreator.class, creator); + } + + @Test + void getInstance_filecoin_shouldReturnFilecoinCreator() { + AddressCreator creator = AddressCreatorManager.getInstance( + ChainType.FILECOIN, true, null); + assertInstanceOf(FilecoinAddressCreator.class, creator); + } + + @Test + void getInstance_bitcoin_segwit_shouldReturnSegWitCreator() { + AddressCreator creator = AddressCreatorManager.getInstance( + ChainType.BITCOIN, true, Metadata.P2WPKH); + assertInstanceOf(SegWitBitcoinAddressCreator.class, creator); + } + + @Test + void getInstance_bitcoin_legacy_shouldReturnBitcoinCreator() { + AddressCreator creator = AddressCreatorManager.getInstance( + ChainType.BITCOIN, true, Metadata.NONE); + assertInstanceOf(BitcoinAddressCreator.class, creator); + } + + @Test + void getInstance_litecoin_shouldReturnBitcoinCreator() { + AddressCreator creator = AddressCreatorManager.getInstance( + ChainType.LITECOIN, true, null); + assertInstanceOf(BitcoinAddressCreator.class, creator); + } + + @Test + void getInstance_unsupportedType_shouldThrow() { + assertThrows(TokenException.class, () -> + AddressCreatorManager.getInstance("UNSUPPORTED", true, null)); + } +} diff --git a/src/test/java/org/consenlabs/tokencore/wallet/address/BitcoinAddressCreatorTest.java b/src/test/java/org/consenlabs/tokencore/wallet/address/BitcoinAddressCreatorTest.java new file mode 100644 index 0000000..39aa19f --- /dev/null +++ b/src/test/java/org/consenlabs/tokencore/wallet/address/BitcoinAddressCreatorTest.java @@ -0,0 +1,38 @@ +package org.consenlabs.tokencore.wallet.address; + +import org.bitcoinj.params.MainNetParams; +import org.bitcoinj.params.TestNet3Params; +import org.consenlabs.tokencore.foundation.utils.NumericUtil; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class BitcoinAddressCreatorTest { + + @Test + void fromPrivateKey_mainnet_shouldDeriveAddress() { + BitcoinAddressCreator creator = new BitcoinAddressCreator(MainNetParams.get()); + String hexKey = "e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35"; + String address = creator.fromPrivateKey(hexKey); + assertNotNull(address); + assertTrue(address.startsWith("1")); + } + + @Test + void fromPrivateKey_testnet_shouldDeriveAddress() { + BitcoinAddressCreator creator = new BitcoinAddressCreator(TestNet3Params.get()); + String hexKey = "e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35"; + String address = creator.fromPrivateKey(hexKey); + assertNotNull(address); + assertTrue(address.startsWith("m") || address.startsWith("n")); + } + + @Test + void fromPrivateKey_bytes_shouldWork() { + BitcoinAddressCreator creator = new BitcoinAddressCreator(MainNetParams.get()); + byte[] key = NumericUtil.hexToBytes("e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35"); + String address = creator.fromPrivateKey(key); + assertNotNull(address); + assertTrue(address.length() > 0); + } +} diff --git a/src/test/java/org/consenlabs/tokencore/wallet/address/EthereumAddressCreatorTest.java b/src/test/java/org/consenlabs/tokencore/wallet/address/EthereumAddressCreatorTest.java new file mode 100644 index 0000000..414ee45 --- /dev/null +++ b/src/test/java/org/consenlabs/tokencore/wallet/address/EthereumAddressCreatorTest.java @@ -0,0 +1,37 @@ +package org.consenlabs.tokencore.wallet.address; + +import org.consenlabs.tokencore.foundation.utils.NumericUtil; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class EthereumAddressCreatorTest { + + private final EthereumAddressCreator creator = new EthereumAddressCreator(); + + @Test + void fromPrivateKey_shouldDeriveValidAddress() { + String address = creator.fromPrivateKey( + "4c0883a69102937d6231471b5dbb6204fe512961708279f14a15c89a7e5a5c3c"); + assertNotNull(address); + assertEquals(40, address.length()); + assertTrue(NumericUtil.isValidHex(address)); + } + + @Test + void fromPrivateKey_bytes_shouldWork() { + byte[] privKey = NumericUtil.hexToBytes( + "4c0883a69102937d6231471b5dbb6204fe512961708279f14a15c89a7e5a5c3c"); + String address = creator.fromPrivateKey(privKey); + assertNotNull(address); + assertEquals(40, address.length()); + } + + @Test + void samePrivateKey_shouldProduceSameAddress() { + String key = "4c0883a69102937d6231471b5dbb6204fe512961708279f14a15c89a7e5a5c3c"; + String addr1 = creator.fromPrivateKey(key); + String addr2 = creator.fromPrivateKey(NumericUtil.hexToBytes(key)); + assertEquals(addr1, addr2); + } +} diff --git a/src/test/java/org/consenlabs/tokencore/wallet/model/BIP44UtilTest.java b/src/test/java/org/consenlabs/tokencore/wallet/model/BIP44UtilTest.java new file mode 100644 index 0000000..8e7b8b3 --- /dev/null +++ b/src/test/java/org/consenlabs/tokencore/wallet/model/BIP44UtilTest.java @@ -0,0 +1,71 @@ +package org.consenlabs.tokencore.wallet.model; + +import com.google.common.collect.ImmutableList; +import org.bitcoinj.crypto.ChildNumber; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class BIP44UtilTest { + + @Test + void generatePath_bitcoinMainnet() { + ImmutableList path = BIP44Util.generatePath("m/44'/0'/0'"); + assertEquals(3, path.size()); + assertEquals(new ChildNumber(44, true), path.get(0)); + assertEquals(new ChildNumber(0, true), path.get(1)); + assertEquals(new ChildNumber(0, true), path.get(2)); + } + + @Test + void generatePath_ethereumFull() { + ImmutableList path = BIP44Util.generatePath("m/44'/60'/0'/0/0"); + assertEquals(5, path.size()); + assertEquals(new ChildNumber(44, true), path.get(0)); + assertEquals(new ChildNumber(60, true), path.get(1)); + assertEquals(new ChildNumber(0, true), path.get(2)); + assertEquals(new ChildNumber(0, false), path.get(3)); + assertEquals(new ChildNumber(0, false), path.get(4)); + } + + @Test + void generatePath_tronPath() { + ImmutableList path = BIP44Util.generatePath(BIP44Util.TRON_PATH); + assertEquals(5, path.size()); + assertEquals(new ChildNumber(195, true), path.get(1)); + } + + @Test + void getBTCMnemonicPath_segwitMainnet() { + String path = BIP44Util.getBTCMnemonicPath(Metadata.P2WPKH, true); + assertEquals(BIP44Util.BITCOIN_SEGWIT_MAIN_PATH, path); + } + + @Test + void getBTCMnemonicPath_segwitTestnet() { + String path = BIP44Util.getBTCMnemonicPath(Metadata.P2WPKH, false); + assertEquals(BIP44Util.BITCOIN_SEGWIT_TESTNET_PATH, path); + } + + @Test + void getBTCMnemonicPath_legacyMainnet() { + String path = BIP44Util.getBTCMnemonicPath(Metadata.NONE, true); + assertEquals(BIP44Util.BITCOIN_MAINNET_PATH, path); + } + + @Test + void getBTCMnemonicPath_legacyTestnet() { + String path = BIP44Util.getBTCMnemonicPath(Metadata.NONE, false); + assertEquals(BIP44Util.BITCOIN_TESTNET_PATH, path); + } + + @Test + void pathConstants_areCorrect() { + assertTrue(BIP44Util.ETHEREUM_PATH.startsWith("m/44'/60'")); + assertTrue(BIP44Util.TRON_PATH.startsWith("m/44'/195'")); + assertTrue(BIP44Util.FILECOIN_PATH.startsWith("m/44'/461'")); + assertTrue(BIP44Util.LITECOIN_MAINNET_PATH.startsWith("m/44'/2'")); + assertTrue(BIP44Util.DOGECOIN_MAINNET_PATH.startsWith("m/44'/3'")); + assertTrue(BIP44Util.DASH_MAINNET_PATH.startsWith("m/44'/5'")); + } +} diff --git a/src/test/java/org/consenlabs/tokencore/wallet/model/ChainTypeTest.java b/src/test/java/org/consenlabs/tokencore/wallet/model/ChainTypeTest.java new file mode 100644 index 0000000..d34b84f --- /dev/null +++ b/src/test/java/org/consenlabs/tokencore/wallet/model/ChainTypeTest.java @@ -0,0 +1,41 @@ +package org.consenlabs.tokencore.wallet.model; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.*; + +class ChainTypeTest { + + @ParameterizedTest + @ValueSource(strings = {"ETHEREUM", "BITCOIN", "EOS", "LITECOIN", "DASH", + "BITCOINSV", "BITCOINCASH", "DOGECOIN", "TRON", "FILECOIN"}) + void validate_shouldAcceptSupportedChains(String chainType) { + assertDoesNotThrow(() -> ChainType.validate(chainType)); + } + + @Test + void validate_shouldRejectUnknownChain() { + assertThrows(TokenException.class, () -> ChainType.validate("UNKNOWN")); + } + + @Test + void validate_shouldRejectNull() { + assertThrows(TokenException.class, () -> ChainType.validate(null)); + } + + @Test + void constants_shouldHaveCorrectValues() { + assertEquals("ETHEREUM", ChainType.ETHEREUM); + assertEquals("BITCOIN", ChainType.BITCOIN); + assertEquals("EOS", ChainType.EOS); + assertEquals("LITECOIN", ChainType.LITECOIN); + assertEquals("DASH", ChainType.DASH); + assertEquals("BITCOINCASH", ChainType.BITCOINCASH); + assertEquals("BITCOINSV", ChainType.BITCOINSV); + assertEquals("DOGECOIN", ChainType.DOGECOIN); + assertEquals("TRON", ChainType.TRON); + assertEquals("FILECOIN", ChainType.FILECOIN); + } +} diff --git a/src/test/java/org/consenlabs/tokencore/wallet/model/MetadataTest.java b/src/test/java/org/consenlabs/tokencore/wallet/model/MetadataTest.java new file mode 100644 index 0000000..4872b47 --- /dev/null +++ b/src/test/java/org/consenlabs/tokencore/wallet/model/MetadataTest.java @@ -0,0 +1,68 @@ +package org.consenlabs.tokencore.wallet.model; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class MetadataTest { + + @Test + void defaultConstructor_shouldCreateEmptyMetadata() { + Metadata metadata = new Metadata(); + assertNull(metadata.getName()); + assertNull(metadata.getChainType()); + assertNull(metadata.getNetwork()); + assertEquals("NORMAL", metadata.getMode()); + } + + @Test + void paramConstructor_shouldSetFields() { + Metadata metadata = new Metadata(ChainType.ETHEREUM, Network.MAINNET, "Test", "hint"); + assertEquals(ChainType.ETHEREUM, metadata.getChainType()); + assertEquals(Network.MAINNET, metadata.getNetwork()); + assertEquals("Test", metadata.getName()); + assertEquals("hint", metadata.getPasswordHint()); + assertTrue(metadata.getTimestamp() > 0); + } + + @Test + void isMainNet_shouldReturnTrue() { + Metadata metadata = new Metadata(); + metadata.setNetwork(Network.MAINNET); + assertTrue(metadata.isMainNet()); + } + + @Test + void isMainNet_shouldReturnFalse() { + Metadata metadata = new Metadata(); + metadata.setNetwork(Network.TESTNET); + assertFalse(metadata.isMainNet()); + } + + @Test + void clone_shouldCreateIndependentCopy() { + Metadata original = new Metadata(); + original.setChainType(ChainType.BITCOIN); + original.setName("Original"); + original.setSegWit(Metadata.P2WPKH); + + Metadata cloned = original.clone(); + assertEquals(original.getChainType(), cloned.getChainType()); + assertEquals(original.getName(), cloned.getName()); + + cloned.setName("Cloned"); + assertNotEquals(original.getName(), cloned.getName()); + } + + @Test + void segWitConstants() { + assertEquals("P2WPKH", Metadata.P2WPKH); + assertEquals("NONE", Metadata.NONE); + } + + @Test + void walletTypeConstants() { + assertEquals("HD", Metadata.HD); + assertEquals("V3", Metadata.V3); + } +} diff --git a/src/test/java/org/consenlabs/tokencore/wallet/model/NetworkTest.java b/src/test/java/org/consenlabs/tokencore/wallet/model/NetworkTest.java new file mode 100644 index 0000000..d344615 --- /dev/null +++ b/src/test/java/org/consenlabs/tokencore/wallet/model/NetworkTest.java @@ -0,0 +1,34 @@ +package org.consenlabs.tokencore.wallet.model; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class NetworkTest { + + @Test + void isMainnet_shouldReturnTrue() { + Network network = new Network(Network.MAINNET); + assertTrue(network.isMainnet()); + } + + @Test + void isMainnet_shouldReturnFalse() { + Network network = new Network(Network.TESTNET); + assertFalse(network.isMainnet()); + } + + @Test + void isMainnet_shouldBeCaseInsensitive() { + Network network = new Network("mainnet"); + assertTrue(network.isMainnet()); + } + + @Test + void constants_shouldBeCorrect() { + assertEquals("MAINNET", Network.MAINNET); + assertEquals("TESTNET", Network.TESTNET); + assertEquals("KOVAN", Network.KOVAN); + assertEquals("ROPSTEN", Network.ROPSTEN); + } +} diff --git a/src/test/java/org/consenlabs/tokencore/wallet/model/TokenExceptionTest.java b/src/test/java/org/consenlabs/tokencore/wallet/model/TokenExceptionTest.java new file mode 100644 index 0000000..54f11c4 --- /dev/null +++ b/src/test/java/org/consenlabs/tokencore/wallet/model/TokenExceptionTest.java @@ -0,0 +1,23 @@ +package org.consenlabs.tokencore.wallet.model; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class TokenExceptionTest { + + @Test + void shouldCarryMessage() { + TokenException ex = new TokenException("test_error"); + assertEquals("test_error", ex.getMessage()); + assertTrue(ex instanceof RuntimeException); + } + + @Test + void shouldCarryCause() { + Exception cause = new IllegalStateException("root cause"); + TokenException ex = new TokenException("wrapper", cause); + assertEquals("wrapper", ex.getMessage()); + assertSame(cause, ex.getCause()); + } +} diff --git a/src/test/java/org/consenlabs/tokencore/wallet/transaction/EthereumTransactionTest.java b/src/test/java/org/consenlabs/tokencore/wallet/transaction/EthereumTransactionTest.java new file mode 100644 index 0000000..e42c406 --- /dev/null +++ b/src/test/java/org/consenlabs/tokencore/wallet/transaction/EthereumTransactionTest.java @@ -0,0 +1,77 @@ +package org.consenlabs.tokencore.wallet.transaction; + +import org.consenlabs.tokencore.foundation.utils.NumericUtil; +import org.junit.jupiter.api.Test; + +import java.math.BigInteger; + +import static org.junit.jupiter.api.Assertions.*; + +class EthereumTransactionTest { + + @Test + void encodeToRLP_shouldProduceNonEmptyBytes() { + EthereumTransaction tx = new EthereumTransaction( + BigInteger.ZERO, + BigInteger.valueOf(20_000_000_000L), + BigInteger.valueOf(21000), + "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf", + BigInteger.valueOf(1_000_000_000_000_000_000L), + "" + ); + + SignatureData emptySignature = new SignatureData(1, new byte[]{}, new byte[]{}); + byte[] encoded = tx.encodeToRLP(emptySignature); + assertNotNull(encoded); + assertTrue(encoded.length > 0); + } + + @Test + void signTransaction_shouldProduceValidSignedTx() { + EthereumTransaction tx = new EthereumTransaction( + BigInteger.valueOf(9), + BigInteger.valueOf(20_000_000_000L), + BigInteger.valueOf(21000), + "0x3535353535353535353535353535353535353535", + BigInteger.valueOf(1_000_000_000_000_000_000L), + "" + ); + + byte[] privateKey = NumericUtil.hexToBytes( + "4646464646464646464646464646464646464646464646464646464646464646"); + + String signedTx = tx.signTransaction(1, privateKey); + assertNotNull(signedTx); + assertTrue(signedTx.length() > 0); + assertTrue(NumericUtil.isValidHex(signedTx)); + } + + @Test + void calcTxHash_shouldReturn66CharHex() { + EthereumTransaction tx = new EthereumTransaction( + BigInteger.ZERO, BigInteger.ONE, BigInteger.ONE, "", BigInteger.ZERO, ""); + + String signedTx = "f86c09850" + "4a817c800" + "82520894" + + "3535353535353535353535353535353535353535" + + "880de0b6b3a7640000" + "80" + + "25a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276" + + "a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83"; + String hash = tx.calcTxHash(signedTx); + assertNotNull(hash); + assertTrue(hash.startsWith("0x")); + } + + @Test + void getters_shouldReturnConstructorValues() { + EthereumTransaction tx = new EthereumTransaction( + BigInteger.ONE, BigInteger.TEN, BigInteger.valueOf(21000), + "0xabc", BigInteger.ZERO, "0xdeadbeef"); + + assertEquals(BigInteger.ONE, tx.getNonce()); + assertEquals(BigInteger.TEN, tx.getGasPrice()); + assertEquals(BigInteger.valueOf(21000), tx.getGasLimit()); + assertEquals("0xabc", tx.getTo()); + assertEquals(BigInteger.ZERO, tx.getValue()); + assertEquals("deadbeef", tx.getData()); + } +}