diff --git a/build.xml b/build.xml index bdab4c03..47b21dbd 100644 --- a/build.xml +++ b/build.xml @@ -60,7 +60,7 @@ addressToAccount = new HashMap<>(); + + private final Map addressToKeystoreContent = Collections.synchronizedMap(new HashMap<>()); + + private final KeystoreFormat keystoreFormat = new KeystoreFormat(); + + private final Function balanceProvider; + + private final Supplier currencySupplier; + + private MasterKey root; + + private boolean isWalletLocked = false; + + public AccountManager(final Function balanceProvider, final Supplier currencySupplier) { + this.balanceProvider = balanceProvider; + this.currencySupplier = currencySupplier; + for (String address : Keystore.list()) { + addressToAccount.put(address, getNewAccount(address)); + } + } + + public String createMasterAccount(final String password, final String name) throws ValidationException { + final StringBuilder mnemonicBuilder = new StringBuilder(); + final byte[] entropy = new byte[Words.TWELVE.byteLength()]; + new SecureRandom().nextBytes(entropy); + new MnemonicGenerator(English.INSTANCE).createMnemonic(entropy, mnemonicBuilder::append); + final String mnemonic = mnemonicBuilder.toString(); + + final AccountDTO account = processMasterAccount(mnemonic, password); + if (account == null) { + return null; + } + account.setName(name); + storeAccountName(account.getPublicAddress(), name); + return mnemonic; + } + + public void importMasterAccount(final String mnemonic, final String password) throws ValidationException { + try { + processMasterAccount(mnemonic, password); + } catch (final Exception e) { + throw new ValidationException(e); + } + } + + private AccountDTO processMasterAccount(final String mnemonic, final String password) throws ValidationException { + final ECKey rootEcKey = CryptoUtils.getBip39ECKey(mnemonic); + + root = new MasterKey(rootEcKey); + walletStorage.setMasterAccountMnemonic(mnemonic, password); + final AccountDTO accountDTO = addInternalAccount(); + EventPublisher.fireAccountAdded(accountDTO); + return accountDTO; + } + + public void unlockMasterAccount(final String password) throws ValidationException { + if (!walletStorage.hasMasterAccount()) { + return; + } + isWalletLocked = false; + + final ECKey rootEcKey = CryptoUtils.getBip39ECKey(walletStorage.getMasterAccountMnemonic(password)); + root = new MasterKey(rootEcKey); + + final int accountDerivations = walletStorage.getMasterAccountDerivations(); + Set recoveredAddresses = new LinkedHashSet<>(accountDerivations); + for (int i = 0; i < accountDerivations; i++) { + final String address = unlockInternalAccount(i); + if (address != null) { + recoveredAddresses.add(address); + } + } + EventPublisher.fireAccountsRecovered(recoveredAddresses); + } + + public boolean isMasterAccountUnlocked() { + return root != null; + } + + public void createAccount() throws ValidationException { + EventPublisher.fireAccountAdded(addInternalAccount()); + } + + public AccountDTO importKeystore(final byte[] file, final String password, final boolean shouldKeep) throws ValidationException { + try { + ECKey key = KeystoreFormat.fromKeystore(file, password); + if (key == null) { + throw new ValidationException("Could Not extract ECKey from keystore file"); + } + return addExternalAccount(key, file, password, shouldKeep); + } catch (final Exception e) { + throw new ValidationException(e); + } + } + + public AccountDTO importPrivateKey(final byte[] raw, final String password, final boolean shouldKeep) throws ValidationException { + try { + ECKey key = ECKeyFac.inst().fromPrivate(raw); + final byte[] keystoreContent = keystoreFormat.toKeystore(key, password); + return addExternalAccount(key, keystoreContent, password, shouldKeep); + } catch (final Exception e) { + throw new ValidationException(e); + } + } + + private String unlockInternalAccount(final int derivationIndex) throws ValidationException { + if (root == null) { + return null; + } + final ECKey derivedKey = getEcKeyFromRoot(derivationIndex); + final String address = TypeConverter.toJsonHex(derivedKey.computeAddress(derivedKey.getPubKey())); + AccountDTO recoveredAccount = addressToAccount.get(address); + if (recoveredAccount != null) { + recoveredAccount.setPrivateKey(derivedKey.getPrivKeyBytes()); + } else { + recoveredAccount = createAccountWithPrivateKey(address, derivedKey.getPrivKeyBytes(), false, derivationIndex); + } + return recoveredAccount == null ? null : address; + } + + private AccountDTO addInternalAccount(final int derivationIndex) throws ValidationException { + if (root == null) { + return null; + } + final ECKey derivedKey = getEcKeyFromRoot(derivationIndex); + final String address = TypeConverter.toJsonHex(derivedKey.computeAddress(derivedKey.getPubKey())); + return createAccountWithPrivateKey(address, derivedKey.getPrivKeyBytes(), false, derivationIndex); + } + + private ECKey getEcKeyFromRoot(final int derivationIndex) throws ValidationException { + return root.deriveHardened(new int[]{44, 425, 0, 0, derivationIndex}); + } + + private AccountDTO addInternalAccount() throws ValidationException { + AccountDTO dto = addInternalAccount(walletStorage.getMasterAccountDerivations()); + walletStorage.incrementMasterAccountDerivations(); + return dto; + } + + private AccountDTO addExternalAccount(final ECKey key, final byte[] fileContent, final String password, final boolean shouldKeep) throws UnsupportedEncodingException, ValidationException { + String address = TypeConverter.toJsonHex(KeystoreItem.parse(fileContent).getAddress()); + final AccountDTO accountDTO; + if (shouldKeep) { + if (!Keystore.exist(address)) { + address = Keystore.create(password, key); + if (AddressUtils.isValid(address)) { + accountDTO = createImportedAccountFromPrivateKey(address, key.getPrivKeyBytes()); + } else { + throw new ValidationException("Failed to save keystore file"); + } + } else { + throw new ValidationException("Account already exists!"); + } + } else { + if (!addressToAccount.keySet().contains(address)) { + accountDTO = createImportedAccountFromPrivateKey(address, key.getPrivKeyBytes()); + } else { + throw new ValidationException("Account already exists!"); + } + } + if (accountDTO == null) { + throw new ValidationException("Failed to create account"); + } + processAccountAdded(accountDTO, fileContent); + return accountDTO; + } + + public void exportAccount(final AccountDTO account, final String password, final String destinationDir) throws ValidationException { + final ECKey ecKey = CryptoUtils.getECKey(account.getPrivateKey()); + final boolean remembered = account.isImported() && Keystore.exist(account.getPublicAddress()); + if (!remembered) { + Keystore.create(password, ecKey); + } + if (Files.isDirectory(WalletStorage.KEYSTORE_PATH)) { + final String fileNameRegex = getExportedFileNameRegex(account.getPublicAddress()); + try (DirectoryStream stream = Files.newDirectoryStream(WalletStorage.KEYSTORE_PATH, fileNameRegex)) { + for (Path keystoreFile : stream) { + final String fileName = keystoreFile.getFileName().toString(); + if (remembered) { + Files.copy(keystoreFile, Paths.get(destinationDir + File.separator + fileName), StandardCopyOption.REPLACE_EXISTING); + } else { + try { + Files.move(keystoreFile, Paths.get(destinationDir + File.separator + fileName), StandardCopyOption.ATOMIC_MOVE); + } finally { + if (Files.exists(keystoreFile)) { + Files.delete(keystoreFile); + } + } + } + } + } catch (IOException e) { + throw new ValidationException(e); + } + } else { + log.error("Could not find Keystore directory: " + WalletStorage.KEYSTORE_PATH); + } + + } + + private String getExportedFileNameRegex(final String publicAddress) { + return "UTC--*--" + publicAddress.substring(2); + } + + public Set getTransactions(final String address) { + return addressToAccount.get(address).getTransactionsSnapshot(); + } + + public void removeTransactions(final String address, final Collection transactions) { + addressToAccount.get(address).removeTransactions(transactions); + } + + public void addTransactions(final String address, final Collection transactions) { + addressToAccount.get(address).addTransactions(transactions); + } + + public BlockDTO getLastSafeBlock(final String address) { + return addressToAccount.get(address).getLastSafeBlock(); + } + + public void updateLastSafeBlock(final String address, final BlockDTO lastCheckedBlock) { + addressToAccount.get(address).setLastSafeBlock(lastCheckedBlock); + } + + public List getAccounts() { + final Collection filteredAccounts = addressToAccount.values().stream().filter(account -> account.isImported() || account.isUnlocked()).collect(Collectors.toList()); + for (AccountDTO account : filteredAccounts) { + account.setBalance(BalanceUtils.formatBalance(balanceProvider.apply(account.getPublicAddress()))); + } + List accounts = new ArrayList<>(filteredAccounts); + accounts.sort((AccountDTO o1, AccountDTO o2) -> { + if (!o1.isImported() && !o2.isImported()) { + return o1.getDerivationIndex() - o2.getDerivationIndex(); + } + return o1.isImported() ? 1 : -1; + }); + return accounts; + } + + public Set getAddresses() { + return new HashSet<>(addressToAccount.keySet()); + } + + public AccountDTO getAccount(final String address) { + return Optional.ofNullable(addressToAccount.get(address)).orElse(getNewAccount(address)); + } + + public void updateAccount(final AccountDTO account) { + storeAccountName(account.getPublicAddress(), account.getName()); + } + + public void unlockAccount(final AccountDTO account, final String password) throws ValidationException { + isWalletLocked = false; + final Optional fileContent = Optional.ofNullable(addressToKeystoreContent.get(account.getPublicAddress())); + final ECKey storedKey; + if (fileContent.isPresent()) { + storedKey = KeystoreFormat.fromKeystore(fileContent.get(), password); + } else { + storedKey = Keystore.getKey(account.getPublicAddress(), password); + } + + if (storedKey != null) { + account.setActive(true); + account.setPrivateKey(storedKey.getPrivKeyBytes()); + EventPublisher.fireAccountChanged(account); + } else { + throw new ValidationException("The password is incorrect!"); + } + + } + + public List getTimedOutTransactions(final String accountAddress) { + return addressToAccount.get(accountAddress).getTimedOutTransactions(); + } + + public void addTimedOutTransaction(final SendTransactionDTO transaction) { + addressToAccount.get(transaction.getFrom()).addTimedOutTransaction(transaction); + } + + public void removeTimedOutTransaction(final SendTransactionDTO transaction) { + addressToAccount.get(transaction.getFrom()).removeTimedOutTransaction(transaction); + } + + public void lockAll() { + if (isWalletLocked) { + return; + } + isWalletLocked = true; + ConsoleManager.addLog("Wallet has been locked due to inactivity", ConsoleManager.LogType.ACCOUNT); + root = null; + for (AccountDTO account : addressToAccount.values()) { + account.setPrivateKey(null); + account.setActive(false); + EventPublisher.fireAccountLocked(account); + } + } + + private AccountDTO createImportedAccountFromPrivateKey(final String address, final byte[] privateKeyBytes) { + return createAccountWithPrivateKey(address, privateKeyBytes, true, -1); + } + + private AccountDTO createAccountWithPrivateKey(final String address, final byte[] privateKeyBytes, boolean isImported, int derivation) { + if (address == null) { + log.error("Can't create account with null address"); + return null; + } + if (privateKeyBytes == null || privateKeyBytes.length == 0) { + log.error("Can't create account without private key"); + return null; + } + AccountDTO account = getNewAccount(address, isImported, derivation); + account.setPrivateKey(privateKeyBytes); + account.setActive(true); + addressToAccount.put(account.getPublicAddress(), account); + return account; + } + + private void processAccountAdded(final AccountDTO account, final byte[] keystoreContent) { + if (account == null || keystoreContent == null) { + throw new IllegalArgumentException(String.format("account %s ; keystoreContent: %s", account, Arrays.toString(keystoreContent))); + } + final String address = account.getPublicAddress(); + addressToKeystoreContent.put(address, keystoreContent); + EventPublisher.fireAccountAdded(account); + } + + private String getStoredAccountName(final String publicAddress) { + return walletStorage.getAccountName(publicAddress); + } + + private AccountDTO getNewAccount(final String publicAddress, boolean isImported, int derivation) { + return new AccountDTO(getStoredAccountName(publicAddress), + publicAddress, + getFormattedBalance(publicAddress), + currencySupplier.get(), + isImported, + derivation); + } + + private AccountDTO getNewAccount(final String publicAddress) { + return getNewAccount(publicAddress, true, -1); + } + + private String getFormattedBalance(String address) { + return BalanceUtils.formatBalance(balanceProvider.apply(address)); + } + + private void storeAccountName(final String address, final String name) { + if (name.equalsIgnoreCase(getStoredAccountName(address))) { + return; + } + walletStorage.setAccountName(address, name); + } +} diff --git a/src/main/java/org/aion/wallet/connector/BlockchainConnector.java b/src/main/java/org/aion/wallet/connector/BlockchainConnector.java index 43ef1b0d..03a7627d 100644 --- a/src/main/java/org/aion/wallet/connector/BlockchainConnector.java +++ b/src/main/java/org/aion/wallet/connector/BlockchainConnector.java @@ -1,20 +1,25 @@ package org.aion.wallet.connector; +import org.aion.wallet.account.AccountManager; import org.aion.wallet.connector.api.ApiBlockchainConnector; -import org.aion.wallet.connector.dto.SendRequestDTO; +import org.aion.wallet.connector.dto.SendTransactionDTO; import org.aion.wallet.connector.dto.SyncInfoDTO; import org.aion.wallet.connector.dto.TransactionDTO; +import org.aion.wallet.connector.dto.TransactionResponseDTO; import org.aion.wallet.dto.AccountDTO; +import org.aion.wallet.dto.LightAppSettings; import org.aion.wallet.exception.NotFoundException; import org.aion.wallet.exception.ValidationException; import org.aion.wallet.storage.ApiType; -import org.aion.wallet.dto.LightAppSettings; import org.aion.wallet.storage.WalletStorage; import org.aion.wallet.util.ConfigUtils; +import java.io.File; import java.lang.reflect.InvocationTargetException; import java.math.BigInteger; +import java.nio.file.Path; import java.util.List; +import java.util.Set; import java.util.concurrent.locks.ReentrantLock; public abstract class BlockchainConnector { @@ -27,6 +32,12 @@ public abstract class BlockchainConnector { private final ReentrantLock lock = new ReentrantLock(); + private final AccountManager accountManager; + + protected BlockchainConnector() { + this.accountManager = new AccountManager(this::getBalance, this::getCurrency); + } + public static BlockchainConnector getInstance() { if (INST != null) { return INST; @@ -43,19 +54,57 @@ public static BlockchainConnector getInstance() { return INST; } - public abstract void createAccount(final String password, final String name); + public final boolean hasMasterAccount() { + return walletStorage.hasMasterAccount(); + } + + public final boolean isMasterAccountUnlocked() { + return accountManager.isMasterAccountUnlocked(); + } + + public final String createMasterAccount(final String password, final String name) throws ValidationException { + return accountManager.createMasterAccount(password, name); + } + + public final void importMasterAccount(final String mnemonic, final String password) throws ValidationException { + accountManager.importMasterAccount(mnemonic, password); + } + + public final void unlockMasterAccount(final String password) throws ValidationException { + accountManager.unlockMasterAccount(password); + } + + public final void createAccount() throws ValidationException { + accountManager.createAccount(); + } + + public final AccountDTO importKeystoreFile(final byte[] file, final String password, final boolean shouldKeep) throws ValidationException { + return accountManager.importKeystore(file, password, shouldKeep); + } + + public final AccountDTO importPrivateKey(final byte[] raw, final String password, final boolean shouldKeep) throws ValidationException { + return accountManager.importPrivateKey(raw, password, shouldKeep); + } - public abstract AccountDTO addKeystoreUTCFile(final byte[] file, final String password, final boolean shouldKeep) throws ValidationException; + public final void exportAccount(final AccountDTO account, final String password, final String destinationDir) throws ValidationException { + accountManager.exportAccount(account, password, destinationDir); + } - public abstract AccountDTO addPrivateKey(final byte[] raw, final String password, final boolean shouldKeep) throws ValidationException; + public final void unlockAccount(final AccountDTO account, final String password) throws ValidationException { + accountManager.unlockAccount(account, password); + } - public abstract AccountDTO getAccount(final String address); + public final AccountDTO getAccount(final String publicAddress) { + return accountManager.getAccount(publicAddress); + } - public abstract List getAccounts(); + public final List getAccounts() { + return accountManager.getAccounts(); + } public abstract BigInteger getBalance(final String address); - public final String sendTransaction(final SendRequestDTO dto) throws ValidationException { + public final TransactionResponseDTO sendTransaction(final SendTransactionDTO dto) throws ValidationException { if (dto == null || !dto.validate()) { throw new ValidationException("Invalid transaction request data"); } @@ -65,52 +114,51 @@ public final String sendTransaction(final SendRequestDTO dto) throws ValidationE return sendTransactionInternal(dto); } - protected abstract String sendTransactionInternal(final SendRequestDTO dto) throws ValidationException; - public abstract TransactionDTO getTransaction(final String txHash) throws NotFoundException; - public abstract List getLatestTransactions(final String address); + public abstract Set getLatestTransactions(final String address); - public abstract boolean getConnectionStatusByConnectedPeers(); + public abstract boolean getConnectionStatus(); public abstract SyncInfoDTO getSyncInfo(); public abstract int getPeerCount(); - // todo: Add balances with different currencies in AccountDTO - public abstract String getCurrency(); + public abstract LightAppSettings getSettings(); + + protected abstract TransactionResponseDTO sendTransactionInternal(final SendTransactionDTO dto); + + protected abstract String getCurrency(); public void close() { walletStorage.save(); } - public void reloadSettings(final LightAppSettings settings){ + public void reloadSettings(final LightAppSettings settings) { walletStorage.saveLightAppSettings(settings); } - public abstract LightAppSettings getSettings(); - - protected final void lock(){ - lock.lock(); + public void lockAll() { + accountManager.lockAll(); } - protected final void unLock() { - lock.unlock(); + public final AccountManager getAccountManager() { + return accountManager; } - protected final String getStoredAccountName(final String publicAddress) { - return walletStorage.getAccountName(publicAddress); + protected final void lock() { + lock.lock(); } - protected final void storeAccountName(final String address, final String name) { - walletStorage.setAccountName(address, name); + protected final void unLock() { + lock.unlock(); } - protected final LightAppSettings getLightweightWalletSettings(final ApiType type){ + protected final LightAppSettings getLightweightWalletSettings(final ApiType type) { return walletStorage.getLightAppSettings(type); } - protected final void storeLightweightWalletSettings(final LightAppSettings lightAppSettings){ + protected final void storeLightweightWalletSettings(final LightAppSettings lightAppSettings) { walletStorage.saveLightAppSettings(lightAppSettings); } } diff --git a/src/main/java/org/aion/wallet/connector/api/ApiBlockchainConnector.java b/src/main/java/org/aion/wallet/connector/api/ApiBlockchainConnector.java index af307507..fc0a3a55 100644 --- a/src/main/java/org/aion/wallet/connector/api/ApiBlockchainConnector.java +++ b/src/main/java/org/aion/wallet/connector/api/ApiBlockchainConnector.java @@ -3,38 +3,26 @@ import com.google.common.eventbus.Subscribe; import org.aion.api.IAionAPI; import org.aion.api.impl.AionAPIImpl; +import org.aion.api.impl.internal.Message; import org.aion.api.log.LogEnum; import org.aion.api.type.*; import org.aion.base.type.Address; import org.aion.base.type.Hash256; import org.aion.base.util.ByteArrayWrapper; import org.aion.base.util.TypeConverter; -import org.aion.crypto.ECKey; -import org.aion.crypto.ECKeyFac; -import org.aion.mcf.account.Keystore; -import org.aion.mcf.account.KeystoreFormat; -import org.aion.mcf.account.KeystoreItem; import org.aion.wallet.connector.BlockchainConnector; -import org.aion.wallet.connector.dto.SendRequestDTO; -import org.aion.wallet.connector.dto.SyncInfoDTO; -import org.aion.wallet.connector.dto.TransactionDTO; +import org.aion.wallet.connector.dto.*; +import org.aion.wallet.console.ConsoleManager; import org.aion.wallet.dto.AccountDTO; import org.aion.wallet.dto.LightAppSettings; +import org.aion.wallet.events.*; import org.aion.wallet.exception.NotFoundException; -import org.aion.wallet.exception.ValidationException; import org.aion.wallet.log.WalletLoggerFactory; import org.aion.wallet.storage.ApiType; -import org.aion.wallet.storage.WalletStorage; -import org.aion.wallet.ui.events.EventBusFactory; -import org.aion.wallet.ui.events.EventPublisher; import org.aion.wallet.util.AionConstants; -import org.aion.wallet.util.BalanceUtils; import org.slf4j.Logger; -import java.io.IOException; import java.math.BigInteger; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.*; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -53,36 +41,45 @@ public class ApiBlockchainConnector extends BlockchainConnector { private static final int DISCONNECT_TIMER = 3000; - private final Map addressToAccount = new HashMap<>(); + private static final List ACCEPTED_TRANSACTION_RESPONSE_STATUSES = Arrays.asList(Message.Retcode.r_tx_Init_VALUE, Message.Retcode.r_tx_Recved_VALUE, Message.Retcode.r_tx_NewPending_VALUE, Message.Retcode.r_tx_Pending_VALUE, Message.Retcode.r_tx_Included_VALUE); - private final Map> addressToTransactions = Collections.synchronizedMap(new HashMap<>()); - - private final Map addressToLastTxInfo = Collections.synchronizedMap(new HashMap<>()); - - private final ExecutorService backgroundExecutor = Executors.newSingleThreadExecutor(); - - private final Comparator transactionComparator = new TransactionComparator(); + private final ExecutorService backgroundExecutor; private LightAppSettings lightAppSettings = getLightweightWalletSettings(ApiType.JAVA); private Future connectionFuture; + private String connectionString; public ApiBlockchainConnector() { - connect(); - loadLocallySavedAccounts(); - backgroundExecutor.submit(() -> processNewTransactions(0, addressToAccount.keySet())); - EventBusFactory.getBus(EventPublisher.ACCOUNT_CHANGE_EVENT_ID).register(this); - EventBusFactory.getBus(EventPublisher.SETTINGS_CHANGED_ID).register(this); + backgroundExecutor = Executors.newFixedThreadPool(getCores()); + connect(getConnectionString()); + EventPublisher.fireApplicationSettingsChanged(lightAppSettings); + registerEventBusConsumer(); + } + + private int getCores() { + int cores = Runtime.getRuntime().availableProcessors(); + if (cores > 1) { + cores = cores / 2; + } + return cores; + } + + private void registerEventBusConsumer() { + EventBusFactory.getBus(AbstractAccountEvent.ID).register(this); + EventBusFactory.getBus(SettingsEvent.ID).register(this); } - private void connect() { + private void connect(final String newConnectionString) { + connectionString = newConnectionString; if (connectionFuture != null) { connectionFuture.cancel(true); } connectionFuture = backgroundExecutor.submit(() -> { - API.connect(getConnectionString(), true); - EventPublisher.fireOperationFinished(); + API.connect(newConnectionString, true); + EventPublisher.fireConnectionEstablished(); + processTransactionsOnReconnect(); }); } @@ -91,133 +88,14 @@ private void disconnect() { lock(); try { API.destroyApi().getObject(); + EventPublisher.fireConnectionBroken(); } finally { unLock(); } } - private void loadLocallySavedAccounts() { - for (String address : Keystore.list()) { - addressToAccount.put(address, getAccount(address)); - addressToTransactions.put(address, new TreeSet<>(transactionComparator)); - addressToLastTxInfo.put(address, new TxInfo(0, -1)); - } - } - - @Override - public void createAccount(final String password, final String name) { - final String address = Keystore.create(password); - final ECKey ecKey = Keystore.getKey(address, password); - if (ecKey != null) { - final AccountDTO account = createAccountWithPrivateKey(address, ecKey.getPrivKeyBytes()); - account.setName(name); - processAccountAdded(address, true); - storeAccountName(address, name); - } else { - log.error("An exception occurred while creating the new account: "); - } - } - - @Override - public AccountDTO addKeystoreUTCFile(final byte[] file, final String password, final boolean shouldKeep) throws ValidationException { - try { - ECKey key = KeystoreFormat.fromKeystore(file, password); - if (key == null) { - throw new ValidationException("Could Not extract ECKey from keystore file"); - } - KeystoreItem keystoreItem = KeystoreItem.parse(file); - String address = keystoreItem.getAddress(); - final AccountDTO accountDTO; - if (!Keystore.exist(address) && shouldKeep) { - address = Keystore.create(password, key); - accountDTO = createAccountWithPrivateKey(address, key.getPrivKeyBytes()); - } else { - accountDTO = addressToAccount.getOrDefault(address, createAccountWithPrivateKey(address, key.getPrivKeyBytes())); - } - processAccountAdded(accountDTO.getPublicAddress(), false); - return accountDTO; - } catch (final Exception e) { - throw new ValidationException("Could not open Keystore File", e); - } - } - - @Override - public AccountDTO addPrivateKey(final byte[] raw, final String password, final boolean shouldKeep) throws ValidationException { - try { - ECKey key = ECKeyFac.inst().fromPrivate(raw); - String address = Keystore.create(password, key); - if (!address.equals("0x")) { - if (!shouldKeep) { - removeKeystoreFile(address); - } - log.info("The private key was imported, the address is: " + address); - final AccountDTO account = createAccountWithPrivateKey(address, raw); - processAccountAdded(account.getPublicAddress(), false); - return account; - } else { - log.info("Failed to import the private key. Already exists?"); - return null; - } - } catch (Exception e) { - throw new ValidationException("Unsupported key type", e); - } - } - - private void processAccountAdded(final String address, final boolean isCreated) { - addressToLastTxInfo.put(address, new TxInfo(isCreated ? -1 : 0, -1)); - addressToTransactions.put(address, new TreeSet<>(transactionComparator)); - backgroundExecutor.submit(() -> processNewTransactions(0, Collections.singleton(address))); - } - - private AccountDTO createAccountWithPrivateKey(final String address, final byte[] privKeyBytes) { - final String name = getStoredAccountName(address); - final String balance = BalanceUtils.formatBalance(getBalance(address)); - AccountDTO account = new AccountDTO(name, address, balance, getCurrency()); - account.setPrivateKey(privKeyBytes); - addressToAccount.put(account.getPublicAddress(), account); - return account; - } - - private void removeKeystoreFile(final String address) { - if (Keystore.exist(address)) { - final String unwrappedAddress = address.substring(2); - Arrays.stream(Keystore.list()) - .filter(s -> s.contains(unwrappedAddress)) - .forEach(account -> removeAssociatedKeyStoreFile(unwrappedAddress)); - } - } - - private void removeAssociatedKeyStoreFile(final String unwrappedAddress) { - try { - for (Path keystoreFile : Files.newDirectoryStream(WalletStorage.KEYSTORE_PATH)) { - if (keystoreFile.toString().contains(unwrappedAddress)) { - Files.deleteIfExists(keystoreFile); - } - } - } catch (IOException e) { - log.error(e.getMessage(), e); - } - } - - @Override - public AccountDTO getAccount(final String publicAddress) { - final String name = getStoredAccountName(publicAddress); - final String balance = BalanceUtils.formatBalance(getBalance(publicAddress)); - return new AccountDTO(name, publicAddress, balance, getCurrency()); - } - - @Override - public List getAccounts() { - for (Map.Entry entry : addressToAccount.entrySet()) { - AccountDTO account = entry.getValue(); - account.setBalance(BalanceUtils.formatBalance(getBalance(account.getPublicAddress()))); - entry.setValue(account); - } - return new ArrayList<>(addressToAccount.values()); - } - @Override - public BigInteger getBalance(final String address) { + public final BigInteger getBalance(final String address) { lock(); final BigInteger balance; try { @@ -233,7 +111,7 @@ public BigInteger getBalance(final String address) { } @Override - protected String sendTransactionInternal(final SendRequestDTO dto) { + protected TransactionResponseDTO sendTransactionInternal(final SendTransactionDTO dto) { final BigInteger latestTransactionNonce = getLatestTransactionNonce(dto.getFrom()); TxArgs txArgs = new TxArgs.TxArgsBuilder() .from(new Address(TypeConverter.toJsonHex(dto.getFrom()))) @@ -247,16 +125,25 @@ protected String sendTransactionInternal(final SendRequestDTO dto) { final MsgRsp response; lock(); try { + ConsoleManager.addLog("Sending transaction", ConsoleManager.LogType.TRANSACTION, ConsoleManager.LogLevel.INFO); response = API.getTx().sendSignedTransaction( txArgs, - new ByteArrayWrapper((addressToAccount.get(dto.getFrom())).getPrivateKey()), - dto.getPassword() + new ByteArrayWrapper((getAccountManager().getAccount(dto.getFrom())).getPrivateKey()) ).getObject(); } finally { unLock(); } - return String.valueOf(response.getTxHash()); + final TransactionResponseDTO transactionResponseDTO = mapTransactionResponse(response); + final int responseStatus = transactionResponseDTO.getStatus(); + if (!ACCEPTED_TRANSACTION_RESPONSE_STATUSES.contains(responseStatus)) { + getAccountManager().addTimedOutTransaction(dto); + } + return transactionResponseDTO; + } + + private TransactionResponseDTO mapTransactionResponse(final MsgRsp response) { + return new TransactionResponseDTO(response.getStatus(), response.getTxHash(), response.getError()); } @Override @@ -276,14 +163,13 @@ public TransactionDTO getTransaction(final String txHash) throws NotFoundExcepti } @Override - public List getLatestTransactions(final String address) { - long lastBlockToCheck = addressToLastTxInfo.get(address).getLastCheckedBlock(); - processNewTransactions(lastBlockToCheck, Collections.singleton(address)); - return new ArrayList<>(addressToTransactions.getOrDefault(address, Collections.emptySortedSet())); + public Set getLatestTransactions(final String address) { + backgroundExecutor.submit(this::processTransactionsFromOldestRegisteredSafeBlock); + return getAccountManager().getTransactions(address); } @Override - public boolean getConnectionStatusByConnectedPeers() { + public boolean getConnectionStatus() { final boolean connected; lock(); try { @@ -309,7 +195,8 @@ public SyncInfoDTO getSyncInfo() { chainBest = syncInfo.getChainBestBlock(); netBest = syncInfo.getNetworkBestBlock(); } catch (Exception e) { - chainBest = getLatest(); + log.error("Could not get SyncInfo - sync displays latest block!"); + chainBest = getLatestBlock().getNumber(); netBest = chainBest; } SyncInfoDTO syncInfoDTO = new SyncInfoDTO(); @@ -335,7 +222,7 @@ public int getPeerCount() { } @Override - public String getCurrency() { + public final String getCurrency() { return AionConstants.CCY; } @@ -347,15 +234,21 @@ public void close() { @Override public void reloadSettings(final LightAppSettings settings) { - super.reloadSettings(settings); - lightAppSettings = getLightweightWalletSettings(ApiType.JAVA); - disconnect(); - try { - Thread.sleep(DISCONNECT_TIMER); - } catch (InterruptedException e) { - log.error(e.getMessage(), e); + if (!lightAppSettings.equals(settings)) { + super.reloadSettings(settings); + lightAppSettings = getLightweightWalletSettings(ApiType.JAVA); + final String newConnectionString = getConnectionString(); + if (!newConnectionString.equalsIgnoreCase(this.connectionString)) { + disconnect(); + try { + Thread.sleep(DISCONNECT_TIMER); + } catch (InterruptedException e) { + log.error(e.getMessage(), e); + } + connect(newConnectionString); + } + EventPublisher.fireApplicationSettingsApplied(settings); } - connect(); } @Override @@ -364,73 +257,178 @@ public LightAppSettings getSettings() { } @Subscribe - private void handleAccountChanged(final AccountDTO account) { - if (!account.getName().equalsIgnoreCase(getStoredAccountName(account.getPublicAddress()))) { - storeAccountName(account.getPublicAddress(), account.getName()); + private void handleAccountEvent(final AccountEvent event) { + final AccountDTO account = event.getPayload(); + if (AbstractAccountEvent.Type.CHANGED.equals(event.getType())) { + getAccountManager().updateAccount(account); + } else if (AbstractAccountEvent.Type.ADDED.equals(event.getType())) { + backgroundExecutor.submit(() -> processTransactionsFromBlock(null, Collections.singleton(account.getPublicAddress()))); } } @Subscribe - private void handleSettingsChanged(final LightAppSettings settings) { - if (settings != null) { - reloadSettings(settings); + private void handleAccountListEvent(final AccountListEvent event) { + if (AbstractAccountEvent.Type.RECOVERED.equals(event.getType())) { + final Set addresses = event.getPayload(); + final BlockDTO oldestSafeBlock = getOldestSafeBlock(addresses, i -> {}); + backgroundExecutor.submit(() -> processTransactionsFromBlock(oldestSafeBlock, addresses)); + final Iterator addressesIterator = addresses.iterator(); + AccountDTO account = getAccount(addressesIterator.next()); + account.setActive(true); + EventPublisher.fireAccountChanged(account); } } - private BigInteger getLatestTransactionNonce(final String address) { - final TxInfo transactionInfo = addressToLastTxInfo.get(address); - final long lastCheckedBlock = transactionInfo.getLastCheckedBlock(); - long lastKnownTxCount = transactionInfo.getTxCount(); - if (lastCheckedBlock >= 0) { - processNewTransactions(lastCheckedBlock, Collections.singleton(address)); + @Subscribe + private void handleSettingsChanged(final SettingsEvent event) { + if (SettingsEvent.Type.CHANGED.equals(event.getType())) { + final LightAppSettings settings = event.getSettings(); + if (settings != null) { + reloadSettings(settings); + } } - return BigInteger.valueOf(lastKnownTxCount + 1); } - private void processNewTransactions(final long lastBlockToCheck, final Set addresses) { - if (API.isConnected() && !addresses.isEmpty()) { - final long latest = getLatest(); - for (long i = latest; i > lastBlockToCheck; i -= BLOCK_BATCH_SIZE) { - List blockBatch = LongStream.iterate(i, j -> j - 1).limit(BLOCK_BATCH_SIZE).boxed().collect(Collectors.toList()); - List blk = getBlockDetailsByNumbers(blockBatch); - blk.forEach(getBlockDetailsConsumer(latest, addresses)); + private void processTransactionsOnReconnect() { + final Set addresses = getAccountManager().getAddresses(); + final BlockDTO oldestSafeBlock = getOldestSafeBlock(addresses, i -> { + }); + processTransactionsFromBlock(oldestSafeBlock, addresses); + } + + private void processTransactionsFromOldestRegisteredSafeBlock() { + final Set addresses = getAccountManager().getAddresses(); + final Consumer> nullSafeBlockFilter = Iterator::remove; + final BlockDTO oldestSafeBlock = getOldestSafeBlock(addresses, nullSafeBlockFilter); + if (oldestSafeBlock != null) { + processTransactionsFromBlock(oldestSafeBlock, addresses); + } + } + + private BlockDTO getOldestSafeBlock(final Set addresses, final Consumer> nullSafeBlockFilter) { + BlockDTO oldestSafeBlock = null; + final Iterator addressIterator = addresses.iterator(); + while (addressIterator.hasNext()) { + final String address = addressIterator.next(); + final BlockDTO lastSafeBlock = getAccountManager().getLastSafeBlock(address); + if (lastSafeBlock != null) { + if (oldestSafeBlock == null || oldestSafeBlock.getNumber() > lastSafeBlock.getNumber()) { + oldestSafeBlock = lastSafeBlock; + } + } else { + nullSafeBlockFilter.accept(addressIterator); } - for (String address : addresses) { - final long txCount = addressToLastTxInfo.get(address).getTxCount(); - addressToLastTxInfo.put(address, new TxInfo(latest, txCount)); + } + return oldestSafeBlock; + } + + private void processTransactionsFromBlock(final BlockDTO lastSafeBlock, final Set addresses) { + if (API.isConnected()) { + if (!addresses.isEmpty()) { + final long latest = getLatestBlock().getNumber(); + final long previousSafe = lastSafeBlock != null ? lastSafeBlock.getNumber() : 0; + log.debug("Processing transactions from block: {} to block: {}, for addresses: {}", previousSafe, latest, addresses); + if (previousSafe > 0) { + final Block lastSupposedSafe = getBlock(previousSafe); + if (!Arrays.equals(lastSafeBlock.getHash(), (lastSupposedSafe.getHash().toBytes()))) { + EventPublisher.fireFatalErrorEncountered("A re-organization happened too far back. Please restart Wallet!"); + } + removeTransactionsFromBlock(addresses, previousSafe); + } + for (long i = latest; i > previousSafe; i -= BLOCK_BATCH_SIZE) { + List blockBatch = LongStream.iterate(i, j -> j - 1).limit(BLOCK_BATCH_SIZE).boxed().collect(Collectors.toList()); + List blk = getBlockDetailsByNumbers(blockBatch); + blk.forEach(getBlockDetailsConsumer(addresses)); + } + final long newSafeBlockNumber = latest - BLOCK_BATCH_SIZE; + final Block newSafe; + if (newSafeBlockNumber > 0) { + newSafe = getBlock(newSafeBlockNumber); + for (String address : addresses) { + getAccountManager().updateLastSafeBlock(address, new BlockDTO(newSafe.getNumber(), newSafe.getHash().toBytes())); + } + } + log.debug("finished processing for addresses: {}", addresses); } + } else { + log.warn("WIll not process transactions from block: {} for addresses: {} because API is disconnected or no addresses", lastSafeBlock, addresses); } } - private Consumer getBlockDetailsConsumer(final long latest, final Set addresses) { + private void removeTransactionsFromBlock(final Set addresses, final long previousSafe) { + for (String address : addresses) { + final List txs = new ArrayList<>(getAccountManager().getTransactions(address)); + final Iterator iterator = txs.iterator(); + final List oldTxs = new ArrayList<>(); + while (iterator.hasNext()) { + final TransactionDTO t = iterator.next(); + if (t.getBlockNumber() > previousSafe) { + oldTxs.add(t); + } else { + break; + } + } + getAccountManager().removeTransactions(address, oldTxs); + } + } + + private BigInteger getLatestTransactionNonce(final String address) { + final BigInteger txCount; + lock(); + try { + if (API.isConnected()) { + txCount = API.getChain().getNonce(Address.wrap(address)).getObject(); + } else { + txCount = BigInteger.ZERO; + } + } finally { + unLock(); + } + return txCount; + } + + private Consumer getBlockDetailsConsumer(final Set addresses) { return blockDetails -> { if (blockDetails != null) { final long timestamp = blockDetails.getTimestamp(); + final long blockNumber = blockDetails.getNumber(); for (final String address : addresses) { - Set txs = addressToTransactions.get(address); - txs.addAll(blockDetails.getTxDetails().stream() + final List newTxs = blockDetails.getTxDetails().stream() .filter(t -> TypeConverter.toJsonHex(t.getFrom().toString()).equals(address) || TypeConverter.toJsonHex(t.getTo().toString()).equals(address)) - .map(t -> recordTransaction(address, t, timestamp, latest)) - .collect(Collectors.toList())); + .map(t -> mapTransaction(t, timestamp, blockNumber)) + .collect(Collectors.toList()); + getAccountManager().addTransactions(address, newTxs); } } }; } - private Long getLatest() { - final Long latest; + private Block getLatestBlock() { + final Block block; lock(); try { if (API.isConnected()) { - latest = API.getChain().blockNumber().getObject(); + final long latest = API.getChain().blockNumber().getObject(); + block = API.getChain().getBlockByNumber(latest).getObject(); } else { - latest = 0L; + block = null; } } finally { unLock(); } - return latest; + return block; + } + + private Block getBlock(final long blockNumber) { + final Block lastSupposedSafe; + lock(); + try { + lastSupposedSafe = API.getChain().getBlockByNumber(blockNumber).getObject(); + } finally { + unLock(); + } + return lastSupposedSafe; } private List getBlockDetailsByNumbers(final List numbers) { @@ -456,11 +454,12 @@ private TransactionDTO mapTransaction(final Transaction transaction) { transaction.getNrgConsumed(), transaction.getNrgPrice(), transaction.getTimeStamp(), - TxState.FINISHED - ); + transaction.getBlockNumber(), + transaction.getNonce(), + transaction.getTransactionIndex()); } - private TransactionDTO mapTransaction(final TxDetails transaction, final long timeStamp) { + private TransactionDTO mapTransaction(final TxDetails transaction, final long timeStamp, final long blockNumber) { if (transaction == null) { return null; } @@ -472,20 +471,9 @@ private TransactionDTO mapTransaction(final TxDetails transaction, final long ti transaction.getNrgConsumed(), transaction.getNrgPrice(), timeStamp, - TxState.FINISHED - ); - } - - private TransactionDTO recordTransaction(final String address, final TxDetails transaction, final long timeStamp, final long lastCheckedBlock) { - final TransactionDTO transactionDTO = mapTransaction(transaction, timeStamp); - final long txCount = addressToLastTxInfo.get(address).getTxCount(); - if (transactionDTO.getFrom().equals(address)) { - final long txNonce = transaction.getNonce().longValue(); - if (txCount < txNonce) { - addressToLastTxInfo.put(address, new TxInfo(lastCheckedBlock, txNonce)); - } - } - return transactionDTO; + blockNumber, + transaction.getNonce(), + transaction.getTxIndex()); } private String getConnectionString() { @@ -494,13 +482,4 @@ private String getConnectionString() { final String port = lightAppSettings.getPort(); return protocol + "://" + ip + ":" + port; } - - private class TransactionComparator implements Comparator { - @Override - public int compare(final TransactionDTO tx1, final TransactionDTO tx2) { - return tx1 == null ? - (tx2 == null ? 0 : -1) : - (tx2 == null ? 1 : Long.compare(tx2.getTimeStamp(), tx1.getTimeStamp())); - } - } } diff --git a/src/main/java/org/aion/wallet/connector/api/TxInfo.java b/src/main/java/org/aion/wallet/connector/api/TxInfo.java deleted file mode 100644 index 03507def..00000000 --- a/src/main/java/org/aion/wallet/connector/api/TxInfo.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.aion.wallet.connector.api; - -public class TxInfo { - private final long lastCheckedBlock; - private final long txCount; - - public TxInfo(final long lastCheckedBlock, final long txCount) { - this.lastCheckedBlock = lastCheckedBlock; - this.txCount = txCount; - } - - public long getLastCheckedBlock() { - return lastCheckedBlock; - } - - public long getTxCount() { - return txCount; - } -} diff --git a/src/main/java/org/aion/wallet/connector/api/TxState.java b/src/main/java/org/aion/wallet/connector/api/TxState.java deleted file mode 100644 index 5cf0ba1d..00000000 --- a/src/main/java/org/aion/wallet/connector/api/TxState.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.aion.wallet.connector.api; - -public enum TxState { - FINISHED, PENDING -} diff --git a/src/main/java/org/aion/wallet/connector/core/CoreBlockchainConnector.java b/src/main/java/org/aion/wallet/connector/core/CoreBlockchainConnector.java index 0b179221..0fc1ee51 100644 --- a/src/main/java/org/aion/wallet/connector/core/CoreBlockchainConnector.java +++ b/src/main/java/org/aion/wallet/connector/core/CoreBlockchainConnector.java @@ -2,80 +2,41 @@ import com.google.common.eventbus.Subscribe; import org.aion.api.log.LogEnum; -import org.aion.api.server.types.ArgTxCall; import org.aion.api.server.types.SyncInfo; -import org.aion.base.type.Address; import org.aion.base.util.ByteUtil; import org.aion.base.util.TypeConverter; -import org.aion.mcf.account.Keystore; import org.aion.wallet.connector.BlockchainConnector; -import org.aion.wallet.connector.api.TxState; -import org.aion.wallet.connector.dto.SendRequestDTO; -import org.aion.wallet.connector.dto.SyncInfoDTO; -import org.aion.wallet.connector.dto.TransactionDTO; -import org.aion.wallet.connector.dto.UnlockableAccount; -import org.aion.wallet.dto.AccountDTO; +import org.aion.wallet.connector.dto.*; import org.aion.wallet.dto.LightAppSettings; +import org.aion.wallet.events.AccountEvent; +import org.aion.wallet.events.EventBusFactory; +import org.aion.wallet.events.EventPublisher; import org.aion.wallet.exception.NotFoundException; -import org.aion.wallet.exception.ValidationException; import org.aion.wallet.log.WalletLoggerFactory; -import org.aion.wallet.ui.events.EventBusFactory; -import org.aion.wallet.ui.events.EventPublisher; +import org.aion.wallet.storage.ApiType; import org.aion.wallet.util.AionConstants; -import org.aion.wallet.util.BalanceUtils; import org.aion.zero.impl.types.AionBlock; import org.aion.zero.types.AionTransaction; import org.slf4j.Logger; import java.math.BigInteger; -import java.util.ArrayList; -import java.util.List; +import java.util.Collections; +import java.util.Set; import java.util.stream.Collectors; public class CoreBlockchainConnector extends BlockchainConnector { private static final Logger log = WalletLoggerFactory.getLogger(LogEnum.WLT.name()); - private final static WalletApi API = new WalletApi(); + private static final WalletApi API = new WalletApi(); public CoreBlockchainConnector() { - EventBusFactory.getBus(EventPublisher.ACCOUNT_CHANGE_EVENT_ID).register(this); - } - - public void createAccount(final String password, final String name) { - final String address = Keystore.create(password); - AccountDTO account = getAccount(address); - account.setName(name); - storeAccountName(address, name); - } - - @Override - public AccountDTO addKeystoreUTCFile(byte[] file, String password, final boolean shouldKeep) throws ValidationException { - throw new ValidationException("Unsupported operation"); - } - - @Override - public AccountDTO addPrivateKey(byte[] raw, String password, final boolean shouldKeep) { - throw new UnsupportedOperationException(); - } - - @Override - public AccountDTO getAccount(final String publicAddress) { - final String name = getStoredAccountName(publicAddress); - return new AccountDTO(name, publicAddress, BalanceUtils.formatBalance(getBalance(publicAddress)), getCurrency()); - } - - @Override - public List getAccounts() { - final List accounts = new ArrayList<>(); - for (final String publicAddress : (List) API.getAccounts()) { - accounts.add(getAccount(publicAddress)); - } - return accounts; + EventBusFactory.getBus(AccountEvent.ID).register(this); + EventPublisher.fireApplicationSettingsChanged(getLightweightWalletSettings(ApiType.CORE)); } @Override - public BigInteger getBalance(String address){ + public BigInteger getBalance(final String address) { try { return API.getBalance(address); } catch (Exception e) { @@ -85,19 +46,13 @@ public BigInteger getBalance(String address){ } @Override - protected String sendTransactionInternal(SendRequestDTO dto) throws ValidationException { - if (!unlock(dto)) { - throw new ValidationException("Failed to unlock wallet"); - } - ArgTxCall transactionParams = new ArgTxCall(Address.wrap(ByteUtil.hexStringToBytes(dto.getFrom())) - , Address.wrap(ByteUtil.hexStringToBytes(dto.getTo())), dto.getData(), - dto.getNonce(), dto.getValue(), dto.getNrg(), dto.getNrgPrice()); - - return TypeConverter.toJsonHex(API.sendTransaction(transactionParams)); + protected TransactionResponseDTO sendTransactionInternal(final SendTransactionDTO dto) { + throw new UnsupportedOperationException(); + //TODO } @Override - public TransactionDTO getTransaction(String txHash) throws NotFoundException { + public TransactionDTO getTransaction(final String txHash) throws NotFoundException { TransactionDTO transaction = mapTransaction(API.getTransactionByHash(TypeConverter.StringHexToByteArray(txHash))); if (transaction == null) { throw new NotFoundException(); @@ -106,12 +61,38 @@ public TransactionDTO getTransaction(String txHash) throws NotFoundException { } @Override - public List getLatestTransactions(String address) { - return getTransactions(address, AionConstants.MAX_BLOCKS_FOR_LATEST_TRANSACTIONS_QUERY); + public Set getLatestTransactions(final String address) { + final BlockDTO lastSafeBlock = getAccountManager().getLastSafeBlock(address); + processNewTransactions(lastSafeBlock, Collections.singleton(address)); + return getAccountManager().getTransactions(address); + } + + private void processNewTransactions(final BlockDTO lastCheckedBlock, final Set addresses) { + if (!addresses.isEmpty()) { + final AionBlock latest = API.getBestBlock(); + long lastBlockToCheck = lastCheckedBlock != null ? lastCheckedBlock.getNumber() : 0; + for (long i = latest.getNumber(); i > lastBlockToCheck; i -= 1) { + AionBlock blk = API.getBlock(i); + if (blk == null || blk.getTransactionsList().size() == 0) { + continue; + } + for (final String address : addresses) { + getAccountManager().addTransactions(address, + blk.getTransactionsList().stream() + .filter(t -> TypeConverter.toJsonHex(t.getFrom().toString()).equals(address) + || TypeConverter.toJsonHex(t.getTo().toString()).equals(address)) + .map(this::mapTransaction) + .collect(Collectors.toList())); + } + } + for (String address : addresses) { + getAccountManager().updateLastSafeBlock(address, new BlockDTO(latest.getNumber(), latest.getHash())); + } + } } @Override - public boolean getConnectionStatusByConnectedPeers() { + public boolean getConnectionStatus() { return API.peerCount() > 0; } @@ -130,50 +111,26 @@ public LightAppSettings getSettings() { throw new UnsupportedOperationException(); } - private boolean unlock(UnlockableAccount account) { - return API.unlockAccount(account.getAddress(), account.getPassword(), AionConstants.DEFAULT_WALLET_UNLOCK_DURATION); - } - @Override public int getPeerCount() { return API.peerCount(); } @Subscribe - private void handleAccountChanged(final AccountDTO account) { - storeAccountName(account.getPublicAddress(), account.getName()); + private void handleAccountEvent(final AccountEvent event) { + if (AccountEvent.Type.CHANGED.equals(event.getType())) { + getAccountManager().updateAccount(event.getPayload()); + } } - private SyncInfoDTO mapSyncInfo(SyncInfo sync) { + private SyncInfoDTO mapSyncInfo(final SyncInfo sync) { SyncInfoDTO syncInfoDTO = new SyncInfoDTO(); syncInfoDTO.setChainBestBlkNumber(sync.chainBestBlkNumber); syncInfoDTO.setNetworkBestBlkNumber(sync.networkBestBlkNumber); return syncInfoDTO; } - private List getTransactions(final String addr, long nrOfBlocksToCheck) { - AionBlock latest = API.getBestBlock(); - long blockOffset = latest.getNumber() - nrOfBlocksToCheck; - if (blockOffset < 0) { - blockOffset = 0; - } - final String parsedAddr = TypeConverter.toJsonHex(addr); - List txs = new ArrayList<>(); - for (long i = latest.getNumber(); i > blockOffset; i--) { - AionBlock blk = API.getBlock(i); - if (blk == null || blk.getTransactionsList().size() == 0) { - continue; - } - txs.addAll(blk.getTransactionsList().stream() - .filter(t -> TypeConverter.toJsonHex(t.getFrom().toString()).equals(parsedAddr) - || TypeConverter.toJsonHex(t.getTo().toString()).equals(parsedAddr)) - .map(this::mapTransaction) - .collect(Collectors.toList())); - } - return txs; - } - - private TransactionDTO mapTransaction(AionTransaction transaction) { + private TransactionDTO mapTransaction(final AionTransaction transaction) { if (transaction == null) { return null; } @@ -185,6 +142,8 @@ private TransactionDTO mapTransaction(AionTransaction transaction) { transaction.getNrg(), transaction.getNrgPrice(), transaction.getTimeStampBI().longValue(), - TxState.FINISHED); + 0L, + transaction.getNonceBI(), + (int) transaction.getTxIndexInBlock()); } } diff --git a/src/main/java/org/aion/wallet/connector/dto/BlockDTO.java b/src/main/java/org/aion/wallet/connector/dto/BlockDTO.java new file mode 100644 index 00000000..75190c67 --- /dev/null +++ b/src/main/java/org/aion/wallet/connector/dto/BlockDTO.java @@ -0,0 +1,19 @@ +package org.aion.wallet.connector.dto; + +public class BlockDTO { + private final long number; + private final byte[] hash; + + public BlockDTO(final long number, final byte[] hash) { + this.number = number; + this.hash = hash; + } + + public long getNumber() { + return number; + } + + public byte[] getHash() { + return hash; + } +} diff --git a/src/main/java/org/aion/wallet/connector/dto/SendRequestDTO.java b/src/main/java/org/aion/wallet/connector/dto/SendTransactionDTO.java similarity index 86% rename from src/main/java/org/aion/wallet/connector/dto/SendRequestDTO.java rename to src/main/java/org/aion/wallet/connector/dto/SendTransactionDTO.java index d5c7c593..da67ec27 100644 --- a/src/main/java/org/aion/wallet/connector/dto/SendRequestDTO.java +++ b/src/main/java/org/aion/wallet/connector/dto/SendTransactionDTO.java @@ -7,18 +7,15 @@ import java.math.BigInteger; -public class SendRequestDTO implements UnlockableAccount { +public class SendTransactionDTO { private String from; - private String password; + private String password = ""; private String to; private Long nrg; private BigInteger nrgPrice; private BigInteger value; - - @Override - public String getAddress() { - return this.from; - } + private byte[] data = ByteArrayWrapper.NULL_BYTE; + private BigInteger nonce = BigInteger.ZERO; public String getPassword() { return password; @@ -73,11 +70,19 @@ public BigInteger estimateValue() { } public byte[] getData() { - return ByteArrayWrapper.NULL_BYTE; + return data; + } + + public void setData(byte[] data) { + this.data = data; } public BigInteger getNonce() { - return BigInteger.ZERO; + return nonce; + } + + private void setNonce(final BigInteger nonce) { + this.nonce = nonce; } public boolean validate() throws ValidationException { diff --git a/src/main/java/org/aion/wallet/connector/dto/TransactionDTO.java b/src/main/java/org/aion/wallet/connector/dto/TransactionDTO.java index 588a5376..ccaa2ecd 100644 --- a/src/main/java/org/aion/wallet/connector/dto/TransactionDTO.java +++ b/src/main/java/org/aion/wallet/connector/dto/TransactionDTO.java @@ -1,11 +1,11 @@ package org.aion.wallet.connector.dto; import org.aion.base.util.TypeConverter; -import org.aion.wallet.connector.api.TxState; import java.math.BigInteger; +import java.util.Objects; -public class TransactionDTO { +public class TransactionDTO implements Comparable { private final String from; private final String to; private final String hash; @@ -13,9 +13,11 @@ public class TransactionDTO { private final long nrg; private final long nrgPrice; private final long timeStamp; - private final TxState state; + private final Long blockNumber; + private final BigInteger nonce; + private final int txIndex; - public TransactionDTO(final String from, final String to, final String hash, final BigInteger value, final long nrg, final long nrgPrice, final long timeStamp, final TxState state) { + public TransactionDTO(final String from, final String to, final String hash, final BigInteger value, final long nrg, final long nrgPrice, final long timeStamp, final long blockNumber, BigInteger nonce, final int txIndex) { this.from = TypeConverter.toJsonHex(from); this.to = TypeConverter.toJsonHex(to); this.hash = hash; @@ -23,7 +25,9 @@ public TransactionDTO(final String from, final String to, final String hash, fin this.nrg = nrg; this.nrgPrice = nrgPrice; this.timeStamp = timeStamp; - this.state = state; + this.blockNumber = blockNumber; + this.nonce = nonce; + this.txIndex = txIndex; } public String getFrom() { @@ -54,7 +58,49 @@ public long getTimeStamp() { return timeStamp; } - public TxState getState() { - return state; + public Long getBlockNumber() { + return blockNumber; + } + + public BigInteger getNonce() { + return nonce; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final TransactionDTO that = (TransactionDTO) o; + return Objects.equals(from, that.from) && + Objects.equals(to, that.to) && + Objects.equals(hash, that.hash) && + Objects.equals(value, that.value) && + Objects.equals(blockNumber, that.blockNumber) && + Objects.equals(nonce, that.nonce); + } + + @Override + public int hashCode() { + return Objects.hash(from, to, hash, value, blockNumber, nonce); + } + + @Override + public int compareTo(final TransactionDTO that) { + final int comparison; + if (that == null) { + comparison = 1; + } else { + final int blockCompare = that.blockNumber.compareTo(this.blockNumber); + if (blockCompare == 0) { + if (this.from.equals(that.from)) { + comparison = that.nonce.compareTo(this.nonce); + } else { + comparison = Integer.compare(that.txIndex, this.txIndex); + } + } else { + comparison = blockCompare; + } + } + return comparison; } } diff --git a/src/main/java/org/aion/wallet/connector/dto/TransactionResponseDTO.java b/src/main/java/org/aion/wallet/connector/dto/TransactionResponseDTO.java new file mode 100644 index 00000000..8449d224 --- /dev/null +++ b/src/main/java/org/aion/wallet/connector/dto/TransactionResponseDTO.java @@ -0,0 +1,42 @@ +package org.aion.wallet.connector.dto; + +import org.aion.base.type.Hash256; + +public class TransactionResponseDTO { + private final byte status; + private final Hash256 txHash; + private final String error; + + public TransactionResponseDTO() { + status = 0; + txHash = null; + error = null; + } + + public TransactionResponseDTO(final byte status, final Hash256 txHash, final String error){ + this.status = status; + this.txHash = txHash; + this.error = error; + } + + public byte getStatus() { + return status; + } + + public Hash256 getTxHash() { + return txHash; + } + + public String getError() { + return error; + } + + @Override + public String toString() { + return "TransactionResponseDTO{" + + "status=" + status + + ", txHash=" + txHash + + ", error='" + error + '\'' + + '}'; + } +} diff --git a/src/main/java/org/aion/wallet/console/ConsoleManager.java b/src/main/java/org/aion/wallet/console/ConsoleManager.java new file mode 100644 index 00000000..1594e975 --- /dev/null +++ b/src/main/java/org/aion/wallet/console/ConsoleManager.java @@ -0,0 +1,60 @@ +package org.aion.wallet.console; + +import javafx.scene.Scene; +import javafx.scene.control.TextArea; +import javafx.scene.layout.StackPane; +import javafx.stage.Stage; + +import java.text.SimpleDateFormat; +import java.util.Date; + +public class ConsoleManager { + private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("dd-MM-YY - HH.mm.ss"); + + private static final TextArea LOGS_TEXT_FIELD = new TextArea(); + private static Stage CONSOLE_LOG_WINDOW; + private static StringBuilder logs = new StringBuilder(); + + static { + LOGS_TEXT_FIELD.setEditable(false); + StackPane root = new StackPane(); + root.getChildren().add(ConsoleManager.LOGS_TEXT_FIELD); + Scene scene = new Scene(root, 600, 350); + + CONSOLE_LOG_WINDOW = new Stage(); + CONSOLE_LOG_WINDOW.setTitle("Console"); + CONSOLE_LOG_WINDOW.setScene(scene); + } + + public static void addLog(String log, LogType type, LogLevel level) { + logs.append(SIMPLE_DATE_FORMAT.format(new Date())); + logs.append(" "); + logs.append(level.toString()); + logs.append(" ["); + logs.append(type.toString()); + logs.append("]: "); + logs.append(log); + logs.append("\n"); + LOGS_TEXT_FIELD.setText(logs.toString()); + LOGS_TEXT_FIELD.setScrollTop(Double.MAX_VALUE); + } + + public static void addLog(String log, LogType type) { + addLog(log, type, LogLevel.INFO); + } + + public static void show() { + CONSOLE_LOG_WINDOW.show(); + } + + public enum LogType { + ACCOUNT, + TRANSACTION, + SETTINGS + } + + public enum LogLevel { + INFO, + WARNING + } +} diff --git a/src/main/java/org/aion/wallet/crypto/MasterKey.java b/src/main/java/org/aion/wallet/crypto/MasterKey.java new file mode 100644 index 00000000..ddf50534 --- /dev/null +++ b/src/main/java/org/aion/wallet/crypto/MasterKey.java @@ -0,0 +1,46 @@ +package org.aion.wallet.crypto; + +import org.aion.crypto.ECKey; +import org.aion.wallet.exception.ValidationException; +import org.aion.wallet.util.CryptoUtils; + +import java.util.Arrays; + +public class MasterKey { + + private final ECKey ecKey; + + public MasterKey(ECKey ecKey) { + this.ecKey = ecKey; + } + + public ECKey getEcKey() { + return ecKey; + } + + public ECKey deriveHardened(int[] derivationPath) throws ValidationException { + if (derivationPath.length == 0) { + throw new ValidationException("Derivation path is incorrect"); + } + byte[] key = ecKey.getPrivKeyBytes(); + for (final int pathElement : derivationPath) { + key = getChild(pathElement, key); + } + final byte[] seed = Arrays.copyOfRange(key, 0, 32); + return new SeededECKeyEd25519(seed); + + } + + private byte[] getChild(final int pathElement, final byte[] keyHash) throws ValidationException { + + byte[] parentPrivateKey = Arrays.copyOfRange(keyHash, 0, 32); + byte[] parentChainCode = Arrays.copyOfRange(keyHash, 32, 64); + + // ed25519 supports ONLY hardened keys + final byte[] offset = CryptoUtils.getHardenedNumber(pathElement); + + byte[] parentPaddedKey = org.spongycastle.util.Arrays.concatenate(new byte[]{0}, parentPrivateKey, offset); + + return CryptoUtils.getSha512(parentChainCode, parentPaddedKey); + } +} diff --git a/src/main/java/org/aion/wallet/crypto/SeededECKeyEd25519.java b/src/main/java/org/aion/wallet/crypto/SeededECKeyEd25519.java new file mode 100644 index 00000000..92bed2e6 --- /dev/null +++ b/src/main/java/org/aion/wallet/crypto/SeededECKeyEd25519.java @@ -0,0 +1,55 @@ +package org.aion.wallet.crypto; + +import org.aion.crypto.ECKey; +import org.aion.crypto.ed25519.ECKeyEd25519; +import org.libsodium.jni.Sodium; + +import java.math.BigInteger; + +public class SeededECKeyEd25519 extends ECKeyEd25519 { + + private static int SIG_BYTES = Sodium.crypto_sign_ed25519_seedbytes(); + + private final byte[] publicKey; + private final byte[] secretKey; + private final byte[] address; + + public SeededECKeyEd25519(final byte[] seed) { + checkSeed(seed); + publicKey = new byte[PUBKEY_BYTES]; + secretKey = new byte[SECKEY_BYTES]; + Sodium.crypto_sign_ed25519_seed_keypair(publicKey, secretKey, seed); + address = computeAddress(publicKey); + } + + private void checkSeed(final byte[] seed) { + if (SIG_BYTES != seed.length) { + throw new IllegalArgumentException(String.format("Seed has to be exactly %s bytes long, but is %s", SIG_BYTES, seed.length)); + } + } + + @Override + public byte[] getAddress() { + return address; + } + + @Override + public byte[] getPubKey() { + return publicKey; + } + + @Override + public byte[] getPrivKeyBytes() { + return secretKey; + } + + @Override + public ECKey fromPrivate(final BigInteger privateKey) { + throw new UnsupportedOperationException(); + } + + @Override + public ECKey fromPrivate(final byte[] bs) { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/org/aion/wallet/dto/AccountDTO.java b/src/main/java/org/aion/wallet/dto/AccountDTO.java index b70f9b34..5ab1674b 100644 --- a/src/main/java/org/aion/wallet/dto/AccountDTO.java +++ b/src/main/java/org/aion/wallet/dto/AccountDTO.java @@ -1,22 +1,37 @@ package org.aion.wallet.dto; import org.aion.base.util.TypeConverter; +import org.aion.wallet.connector.dto.BlockDTO; +import org.aion.wallet.connector.dto.SendTransactionDTO; +import org.aion.wallet.connector.dto.TransactionDTO; +import org.aion.wallet.util.QRCodeUtils; -import java.util.Objects; +import java.awt.image.BufferedImage; +import java.util.*; public class AccountDTO { + private final String currency; private final String publicAddress; + private final boolean isImported; + private final int derivationIndex; + private final BufferedImage qrCode; + private final SortedSet transactions = new TreeSet<>(); + private final List timedOutTransactions = new ArrayList<>(); private byte[] privateKey; private String balance; //TODO this has to be BigInteger private String name; private boolean active; + private BlockDTO lastSafeBlock = null; - public AccountDTO(final String name, final String publicAddress, final String balance, final String currency) { + public AccountDTO(final String name, final String publicAddress, final String balance, final String currency, boolean isImported, int derivationIndex) { this.name = name; this.publicAddress = TypeConverter.toJsonHex(publicAddress); this.balance = balance; this.currency = currency; + this.qrCode = QRCodeUtils.writeQRCode(publicAddress); + this.isImported = isImported; + this.derivationIndex = derivationIndex; } public String getName() { @@ -59,6 +74,56 @@ public void setActive(boolean active) { this.active = active; } + public boolean isImported() { + return isImported; + } + + public int getDerivationIndex() { + return derivationIndex; + } + + public BufferedImage getQrCode() { + return qrCode; + } + + public SortedSet getTransactionsSnapshot() { + return Collections.unmodifiableSortedSet(new TreeSet<>(transactions)); + } + + public void addTransactions(final Collection transactions) { + this.transactions.addAll(transactions); + } + + public void removeTransactions(final Collection transactions) { + this.transactions.removeAll(transactions); + } + + public BlockDTO getLastSafeBlock() { + return lastSafeBlock; + } + + public void setLastSafeBlock(final BlockDTO lastSafeBlock) { + this.lastSafeBlock = lastSafeBlock; + } + + public List getTimedOutTransactions() { + return timedOutTransactions; + } + + public void addTimedOutTransaction(SendTransactionDTO transaction) { + if (transaction == null) { + return; + } + this.timedOutTransactions.add(transaction); + } + + public void removeTimedOutTransaction(SendTransactionDTO transaction) { + if (transaction == null) { + return; + } + this.timedOutTransactions.remove(transaction); + } + @Override public boolean equals(final Object o) { if (this == o) return true; @@ -76,4 +141,5 @@ public int hashCode() { public boolean isUnlocked() { return privateKey != null; } + } diff --git a/src/main/java/org/aion/wallet/dto/LightAppSettings.java b/src/main/java/org/aion/wallet/dto/LightAppSettings.java index 4b65f48d..8200bfb2 100644 --- a/src/main/java/org/aion/wallet/dto/LightAppSettings.java +++ b/src/main/java/org/aion/wallet/dto/LightAppSettings.java @@ -1,36 +1,50 @@ package org.aion.wallet.dto; +import org.aion.wallet.exception.ValidationException; import org.aion.wallet.storage.ApiType; +import java.util.Objects; import java.util.Optional; import java.util.Properties; public class LightAppSettings { + private static final Integer DEFAULT_LOCK_TIMEOUT = 3; + private static final String DEFAULT_LOCK_TIMEOUT_MEASUREMENT_UNIT = "minutes"; private static final String ADDRESS = ".address"; private static final String PORT = ".port"; private static final String PROTOCOL = ".protocol"; + private static final String ACCOUNTS = "accounts"; + private static final String DEFAULT_IP = "127.0.0.1"; private static final String DEFAULT_PORT = "8547"; private static final String DEFAULT_PROTOCOL = "tcp"; + private static final String LOCK_TIMEOUT = ".lock_timeout"; + private static final String LOCK_TIMEOUT_MEASUREMENT_UNIT = ".lock_timeout_measurement_unit"; private final ApiType type; private final String address; private final String port; private final String protocol; + private final Integer lockTimeout; + private final String lockTimeoutMeasurementUnit; public LightAppSettings(final Properties lightSettingsProps, final ApiType type) { this.type = type; address = Optional.ofNullable(lightSettingsProps.getProperty(type + ADDRESS)).orElse(DEFAULT_IP); port = Optional.ofNullable(lightSettingsProps.getProperty(type + PORT)).orElse(DEFAULT_PORT); protocol = Optional.ofNullable(lightSettingsProps.getProperty(type + PROTOCOL)).orElse(DEFAULT_PROTOCOL); + lockTimeout = Integer.parseInt(Optional.ofNullable(lightSettingsProps.getProperty(ACCOUNTS + LOCK_TIMEOUT)).orElse(DEFAULT_LOCK_TIMEOUT.toString())); + lockTimeoutMeasurementUnit = Optional.ofNullable(lightSettingsProps.getProperty(ACCOUNTS + LOCK_TIMEOUT_MEASUREMENT_UNIT)).orElse(DEFAULT_LOCK_TIMEOUT_MEASUREMENT_UNIT); } - public LightAppSettings(final String address, final String port, final String protocol, final ApiType type) { + public LightAppSettings(final String address, final String port, final String protocol, final ApiType type, final Integer timeout, final String lockTimeoutMeasurementUnit) throws ValidationException { this.type = type; this.address = address; this.port = port; this.protocol = protocol; + this.lockTimeout = timeout; + this.lockTimeoutMeasurementUnit = lockTimeoutMeasurementUnit; } public final String getAddress() { @@ -49,11 +63,40 @@ public ApiType getType() { return type; } + public Integer getUnlockTimeout() { + return lockTimeout; + } + + public String getLockTimeoutMeasurementUnit() { + return lockTimeoutMeasurementUnit; + } + public final Properties getSettingsProperties() { final Properties properties = new Properties(); properties.setProperty(type + ADDRESS, address); properties.setProperty(type + PORT, port); properties.setProperty(type + PROTOCOL, protocol); + properties.setProperty(ACCOUNTS + LOCK_TIMEOUT, lockTimeout.toString()); + properties.setProperty(ACCOUNTS + LOCK_TIMEOUT_MEASUREMENT_UNIT, lockTimeoutMeasurementUnit); return properties; } + + @Override + public boolean equals(final Object other) { + if (this == other) return true; + if (other == null || getClass() != other.getClass()) return false; + LightAppSettings that = (LightAppSettings) other; + return type == that.type && + Objects.equals(address, that.address) && + Objects.equals(port, that.port) && + Objects.equals(protocol, that.protocol) && + Objects.equals(lockTimeout, that.lockTimeout) && + Objects.equals(lockTimeoutMeasurementUnit, that.lockTimeoutMeasurementUnit); + } + + @Override + public int hashCode() { + + return Objects.hash(type, address, port, protocol, lockTimeout, lockTimeoutMeasurementUnit); + } } diff --git a/src/main/java/org/aion/wallet/events/AbstractAccountEvent.java b/src/main/java/org/aion/wallet/events/AbstractAccountEvent.java new file mode 100644 index 00000000..0117c06e --- /dev/null +++ b/src/main/java/org/aion/wallet/events/AbstractAccountEvent.java @@ -0,0 +1,21 @@ +package org.aion.wallet.events; + +public abstract class AbstractAccountEvent extends AbstractEvent { + + public static final String ID = "account.update"; + + private final T payload; + + protected AbstractAccountEvent(final Type eventType, final T payload) { + super(eventType); + this.payload = payload; + } + + public final T getPayload() { + return payload; + } + + public enum Type { + CHANGED, UNLOCKED, ADDED, LOCKED, EXPORT, RECOVERED + } +} diff --git a/src/main/java/org/aion/wallet/ui/events/AbstractUIEvent.java b/src/main/java/org/aion/wallet/events/AbstractEvent.java similarity index 51% rename from src/main/java/org/aion/wallet/ui/events/AbstractUIEvent.java rename to src/main/java/org/aion/wallet/events/AbstractEvent.java index e7f344d1..8a1d7aad 100644 --- a/src/main/java/org/aion/wallet/ui/events/AbstractUIEvent.java +++ b/src/main/java/org/aion/wallet/events/AbstractEvent.java @@ -1,10 +1,10 @@ -package org.aion.wallet.ui.events; +package org.aion.wallet.events; -public class AbstractUIEvent { +public class AbstractEvent { private final T eventType; - protected AbstractUIEvent(T eventType) { + protected AbstractEvent(final T eventType) { this.eventType = eventType; } diff --git a/src/main/java/org/aion/wallet/events/AccountEvent.java b/src/main/java/org/aion/wallet/events/AccountEvent.java new file mode 100644 index 00000000..1812e226 --- /dev/null +++ b/src/main/java/org/aion/wallet/events/AccountEvent.java @@ -0,0 +1,9 @@ +package org.aion.wallet.events; + +import org.aion.wallet.dto.AccountDTO; + +public class AccountEvent extends AbstractAccountEvent { + protected AccountEvent(final Type eventType, final AccountDTO payload) { + super(eventType, payload); + } +} diff --git a/src/main/java/org/aion/wallet/events/AccountListEvent.java b/src/main/java/org/aion/wallet/events/AccountListEvent.java new file mode 100644 index 00000000..ee7d9809 --- /dev/null +++ b/src/main/java/org/aion/wallet/events/AccountListEvent.java @@ -0,0 +1,9 @@ +package org.aion.wallet.events; + +import java.util.Set; + +public class AccountListEvent extends AbstractAccountEvent> { + protected AccountListEvent(final Type eventType, final Set addresses) { + super(eventType, addresses); + } +} diff --git a/src/main/java/org/aion/wallet/events/ErrorEvent.java b/src/main/java/org/aion/wallet/events/ErrorEvent.java new file mode 100644 index 00000000..d2725dbe --- /dev/null +++ b/src/main/java/org/aion/wallet/events/ErrorEvent.java @@ -0,0 +1,21 @@ +package org.aion.wallet.events; + +public class ErrorEvent extends AbstractEvent { + + public static final String ID = "app.error"; + + private final String message; + + protected ErrorEvent(final Type eventType, final String message) { + super(eventType); + this.message = message; + } + + public String getMessage() { + return message; + } + + public enum Type { + FATAL + } +} diff --git a/src/main/java/org/aion/wallet/ui/events/EventBusFactory.java b/src/main/java/org/aion/wallet/events/EventBusFactory.java similarity index 94% rename from src/main/java/org/aion/wallet/ui/events/EventBusFactory.java rename to src/main/java/org/aion/wallet/events/EventBusFactory.java index 40900126..66934b75 100644 --- a/src/main/java/org/aion/wallet/ui/events/EventBusFactory.java +++ b/src/main/java/org/aion/wallet/events/EventBusFactory.java @@ -1,4 +1,4 @@ -package org.aion.wallet.ui.events; +package org.aion.wallet.events; import com.google.common.eventbus.EventBus; diff --git a/src/main/java/org/aion/wallet/events/EventPublisher.java b/src/main/java/org/aion/wallet/events/EventPublisher.java new file mode 100644 index 00000000..d148ba1c --- /dev/null +++ b/src/main/java/org/aion/wallet/events/EventPublisher.java @@ -0,0 +1,82 @@ +package org.aion.wallet.events; + +import org.aion.wallet.connector.dto.SendTransactionDTO; +import org.aion.wallet.dto.AccountDTO; +import org.aion.wallet.dto.LightAppSettings; + +import java.util.List; +import java.util.Set; + +public class EventPublisher { + + public static void fireFatalErrorEncountered(final String message) { + EventBusFactory.getBus(ErrorEvent.ID).post(new ErrorEvent(ErrorEvent.Type.FATAL, message)); + } + + public static void fireMnemonicCreated(final String mnemonic) { + if (mnemonic != null) { + EventBusFactory.getBus(UiMessageEvent.ID) + .post(new UiMessageEvent(UiMessageEvent.Type.MNEMONIC_CREATED, mnemonic)); + } + } + + public static void fireAccountAdded(final AccountDTO account) { + if (account != null) { + EventBusFactory.getBus(AccountEvent.ID).post(new AccountEvent(AccountEvent.Type.ADDED, account)); + } + } + + public static void fireAccountChanged(final AccountDTO account) { + if (account != null) { + EventBusFactory.getBus(AccountEvent.ID).post(new AccountEvent(AccountEvent.Type.CHANGED, account)); + } + } + + public static void fireAccountUnlocked(final AccountDTO account) { + if (account != null) { + EventBusFactory.getBus(AccountEvent.ID).post(new AccountEvent(AccountEvent.Type.UNLOCKED, account)); + } + } + + public static void fireAccountExport(final AccountDTO account) { + if (account != null) { + EventBusFactory.getBus(AccountEvent.ID).post(new AccountEvent(AccountEvent.Type.EXPORT, account)); + } + } + + public static void fireAccountLocked(final AccountDTO account) { + if (account != null) { + EventBusFactory.getBus(AbstractAccountEvent.ID).post(new AccountEvent(AbstractAccountEvent.Type.LOCKED, account)); + } + } + + public static void fireAccountsRecovered(final Set addresses) { + if (addresses != null && !addresses.isEmpty()) { + EventBusFactory.getBus(AbstractAccountEvent.ID).post(new AccountListEvent(AbstractAccountEvent.Type.RECOVERED, addresses)); + } + } + + public static void fireTransactionFinished() { + EventBusFactory.getBus(RefreshEvent.ID).post(new RefreshEvent(RefreshEvent.Type.TRANSACTION_FINISHED)); + } + + public static void fireConnectionEstablished() { + EventBusFactory.getBus(RefreshEvent.ID).post(new RefreshEvent(RefreshEvent.Type.CONNECTED)); + } + + public static void fireConnectionBroken() { + EventBusFactory.getBus(RefreshEvent.ID).post(new RefreshEvent(RefreshEvent.Type.DISCONNECTED)); + } + + public static void fireApplicationSettingsChanged(final LightAppSettings settings) { + EventBusFactory.getBus(SettingsEvent.ID).post(new SettingsEvent(SettingsEvent.Type.CHANGED, settings)); + } + + public static void fireApplicationSettingsApplied(final LightAppSettings settings) { + EventBusFactory.getBus(SettingsEvent.ID).post(new SettingsEvent(SettingsEvent.Type.APPLIED, settings)); + } + + public static void fireTransactionResubmited(final SendTransactionDTO transaction) { + EventBusFactory.getBus(TransactionEvent.ID).post(new TransactionEvent(TransactionEvent.Type.RESUBMIT, transaction)); + } +} diff --git a/src/main/java/org/aion/wallet/ui/events/HeaderPaneButtonEvent.java b/src/main/java/org/aion/wallet/events/HeaderPaneButtonEvent.java similarity index 66% rename from src/main/java/org/aion/wallet/ui/events/HeaderPaneButtonEvent.java rename to src/main/java/org/aion/wallet/events/HeaderPaneButtonEvent.java index 2efc7284..931765e9 100644 --- a/src/main/java/org/aion/wallet/ui/events/HeaderPaneButtonEvent.java +++ b/src/main/java/org/aion/wallet/events/HeaderPaneButtonEvent.java @@ -1,6 +1,6 @@ -package org.aion.wallet.ui.events; +package org.aion.wallet.events; -public class HeaderPaneButtonEvent extends AbstractUIEvent { +public class HeaderPaneButtonEvent extends AbstractEvent { public static final String ID = "ui.header_button"; diff --git a/src/main/java/org/aion/wallet/events/IdleMonitor.java b/src/main/java/org/aion/wallet/events/IdleMonitor.java new file mode 100644 index 00000000..ead76a69 --- /dev/null +++ b/src/main/java/org/aion/wallet/events/IdleMonitor.java @@ -0,0 +1,48 @@ +package org.aion.wallet.events; + + +import javafx.animation.Animation; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; +import javafx.event.Event; +import javafx.event.EventHandler; +import javafx.event.EventType; +import javafx.scene.Scene; +import javafx.util.Duration; + +public class IdleMonitor { + private final Timeline idleTimeline; + + private final EventHandler userEventHandler; + + public IdleMonitor(Duration idleTime, Runnable notifier) { + idleTimeline = new Timeline(new KeyFrame(idleTime, e -> notifier.run())); + idleTimeline.setCycleCount(Animation.INDEFINITE); + + userEventHandler = e -> notIdle(); + + startMonitoring(); + } + + public void register(Scene scene, EventType eventType) { + scene.addEventFilter(eventType, userEventHandler); + } + + public void unregister(Scene scene, EventType eventType) { + scene.removeEventFilter(eventType, userEventHandler); + } + + public void startMonitoring() { + idleTimeline.playFromStart(); + } + + public void stopMonitoring() { + idleTimeline.stop(); + } + + private void notIdle() { + if (idleTimeline.getStatus() == Animation.Status.RUNNING) { + idleTimeline.playFromStart(); + } + } +} diff --git a/src/main/java/org/aion/wallet/events/RefreshEvent.java b/src/main/java/org/aion/wallet/events/RefreshEvent.java new file mode 100644 index 00000000..a029f9fb --- /dev/null +++ b/src/main/java/org/aion/wallet/events/RefreshEvent.java @@ -0,0 +1,17 @@ +package org.aion.wallet.events; + +public class RefreshEvent extends AbstractEvent { + + public static final String ID = "ui.data_refresh"; + + public RefreshEvent(final Type eventType) { + super(eventType); + } + + public enum Type { + CONNECTED, + DISCONNECTED, + TRANSACTION_FINISHED, + TIMER + } +} diff --git a/src/main/java/org/aion/wallet/events/SettingsEvent.java b/src/main/java/org/aion/wallet/events/SettingsEvent.java new file mode 100644 index 00000000..4293e25d --- /dev/null +++ b/src/main/java/org/aion/wallet/events/SettingsEvent.java @@ -0,0 +1,24 @@ +package org.aion.wallet.events; + +import org.aion.wallet.dto.LightAppSettings; + +public class SettingsEvent extends AbstractEvent { + + public static final String ID = "settings.changed"; + + private final LightAppSettings settings; + + public SettingsEvent(final Type type, final LightAppSettings settings) { + super(type); + + this.settings = settings; + } + + public LightAppSettings getSettings() { + return settings; + } + + public enum Type { + CHANGED, APPLIED + } +} diff --git a/src/main/java/org/aion/wallet/events/TransactionEvent.java b/src/main/java/org/aion/wallet/events/TransactionEvent.java new file mode 100644 index 00000000..ea22ea54 --- /dev/null +++ b/src/main/java/org/aion/wallet/events/TransactionEvent.java @@ -0,0 +1,24 @@ +package org.aion.wallet.events; + +import org.aion.wallet.connector.dto.SendTransactionDTO; +import org.aion.wallet.dto.AccountDTO; + +public class TransactionEvent extends AbstractEvent { + + public static final String ID = "transaction.resubmit"; + + private final SendTransactionDTO transaction; + + protected TransactionEvent(final TransactionEvent.Type eventType, final SendTransactionDTO transaction) { + super(eventType); + this.transaction = transaction; + } + + public SendTransactionDTO getTransaction() { + return transaction; + } + + public enum Type { + RESUBMIT + } +} diff --git a/src/main/java/org/aion/wallet/events/UiMessageEvent.java b/src/main/java/org/aion/wallet/events/UiMessageEvent.java new file mode 100644 index 00000000..62176b87 --- /dev/null +++ b/src/main/java/org/aion/wallet/events/UiMessageEvent.java @@ -0,0 +1,21 @@ +package org.aion.wallet.events; + +public class UiMessageEvent extends AbstractEvent { + + public static final String ID = "ui.message"; + + private final String message; + + public UiMessageEvent(final Type eventType, final String message) { + super(eventType); + this.message = message; + } + + public String getMessage() { + return message; + } + + public enum Type { + MNEMONIC_CREATED + } +} diff --git a/src/main/java/org/aion/wallet/ui/events/WindowControlsEvent.java b/src/main/java/org/aion/wallet/events/WindowControlsEvent.java similarity index 71% rename from src/main/java/org/aion/wallet/ui/events/WindowControlsEvent.java rename to src/main/java/org/aion/wallet/events/WindowControlsEvent.java index d109b58f..f36a8feb 100644 --- a/src/main/java/org/aion/wallet/ui/events/WindowControlsEvent.java +++ b/src/main/java/org/aion/wallet/events/WindowControlsEvent.java @@ -1,8 +1,8 @@ -package org.aion.wallet.ui.events; +package org.aion.wallet.events; import javafx.scene.Node; -public class WindowControlsEvent extends AbstractUIEvent { +public class WindowControlsEvent extends AbstractEvent { public static final String ID = "ui.window_controls"; @@ -18,6 +18,6 @@ public Node getSource() { } public enum Type { - MINIMIZE, CLOSE + MINIMIZE, RESTART, CLOSE } } diff --git a/src/main/java/org/aion/wallet/exception/ValidationException.java b/src/main/java/org/aion/wallet/exception/ValidationException.java index 1bbd333a..3623a8c5 100644 --- a/src/main/java/org/aion/wallet/exception/ValidationException.java +++ b/src/main/java/org/aion/wallet/exception/ValidationException.java @@ -6,7 +6,12 @@ public ValidationException(String message) { super(message); } - public ValidationException(String message, Throwable cause) { - super(message, cause); + public ValidationException(Throwable cause) { + super(cause); + } + + @Override + public String getMessage() { + return getCause() != null ? getCause().getMessage() : super.getMessage(); } } diff --git a/src/main/java/org/aion/wallet/log/WalletLoggerFactory.java b/src/main/java/org/aion/wallet/log/WalletLoggerFactory.java index a0e39e45..1408e2f4 100644 --- a/src/main/java/org/aion/wallet/log/WalletLoggerFactory.java +++ b/src/main/java/org/aion/wallet/log/WalletLoggerFactory.java @@ -1,16 +1,30 @@ package org.aion.wallet.log; -import org.aion.api.log.AionLoggerFactory; import org.aion.wallet.util.ConfigUtils; import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; public class WalletLoggerFactory { - public static Logger getLogger(String id) { - if (ConfigUtils.isEmbedded()) { - return org.aion.log.AionLoggerFactory.getLogger(id); - } else { - return AionLoggerFactory.getLogger(id); + private static final Map LOGGER_MAP = new HashMap<>(); + + public static Logger getLogger(final Class clazz) { + return getLogger(clazz.getName()); + } + + public static Logger getLogger(final String id) { + Logger logger = LOGGER_MAP.get(id); + if (logger == null) { + if (ConfigUtils.isEmbedded()) { + logger = org.aion.log.AionLoggerFactory.getLogger(id); + } else { + logger = LoggerFactory.getLogger(id); + } + LOGGER_MAP.put(id, logger); } + return logger; } } diff --git a/src/main/java/org/aion/wallet/storage/ApiType.java b/src/main/java/org/aion/wallet/storage/ApiType.java index c3c58982..35f4dbe1 100644 --- a/src/main/java/org/aion/wallet/storage/ApiType.java +++ b/src/main/java/org/aion/wallet/storage/ApiType.java @@ -1,5 +1,5 @@ package org.aion.wallet.storage; public enum ApiType { - JAVA, WEB3 + JAVA, CORE, WEB3 } diff --git a/src/main/java/org/aion/wallet/storage/WalletStorage.java b/src/main/java/org/aion/wallet/storage/WalletStorage.java index d6dd2759..a5070481 100644 --- a/src/main/java/org/aion/wallet/storage/WalletStorage.java +++ b/src/main/java/org/aion/wallet/storage/WalletStorage.java @@ -2,9 +2,12 @@ import org.aion.api.log.LogEnum; import org.aion.wallet.dto.LightAppSettings; +import org.aion.wallet.exception.ValidationException; import org.aion.wallet.log.WalletLoggerFactory; import org.slf4j.Logger; +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -20,22 +23,42 @@ public class WalletStorage { private static final Logger log = WalletLoggerFactory.getLogger(LogEnum.WLT.name()); - private static final String USER_DIR = "user.dir"; + public static final Path KEYSTORE_PATH; - public static final Path KEYSTORE_PATH = Paths.get(System.getProperty(USER_DIR) + File.separator + "keystore"); + private static final String BLANK = ""; - private static final String HOME_DIR = System.getProperty("user.home"); + private static final String ACCOUNT_NAME_PROP = ".name"; - private static final String STORAGE_DIR = HOME_DIR + File.separator + ".aion"; + private static final String MASTER_DERIVATIONS_PROP = "master.derivations"; - private static final String ACCOUNTS_FILE = STORAGE_DIR + File.separator + "accounts.properties"; + private static final String MASTER_MNEMONIC_PROP = "master.mnemonic"; - private static final String WALLET_FILE = STORAGE_DIR + File.separator + "wallet.properties"; + private static final String MNEMONIC_ENCRYPTION_ALGORITHM = "Blowfish"; - private static final String ACCOUNT_NAME_PROP = ".name"; + private static final String MNEMONIC_STRING_CONVERSION_CHARSET_NAME = "ISO-8859-1"; private static final WalletStorage INST; + private static final String STORAGE_DIR; + + private static final String ACCOUNTS_FILE; + + private static final String WALLET_FILE; + + static { + String storageDir = System.getProperty("local.storage.dir"); + if (storageDir == null || storageDir.equalsIgnoreCase("")) { + storageDir = System.getProperty("user.home") + File.separator + ".aion"; + } + STORAGE_DIR = storageDir; + + KEYSTORE_PATH = Paths.get(STORAGE_DIR + File.separator + "keystore"); + + ACCOUNTS_FILE = STORAGE_DIR + File.separator + "accounts.properties"; + + WALLET_FILE = STORAGE_DIR + File.separator + "wallet.properties"; + } + static { try { INST = new WalletStorage(); @@ -100,7 +123,7 @@ private void saveSettings() { } public String getAccountName(final String address) { - return Optional.ofNullable(accountsProperties.get(address + ACCOUNT_NAME_PROP)).map(Object::toString).orElse(""); + return Optional.ofNullable(accountsProperties.get(address + ACCOUNT_NAME_PROP)).map(Object::toString).orElse(BLANK); } public void setAccountName(final String address, final String accountName) { @@ -110,6 +133,47 @@ public void setAccountName(final String address, final String accountName) { } } + public String getMasterAccountMnemonic(String password) throws ValidationException { + if (password == null || password.equalsIgnoreCase("")) { + throw new ValidationException("Password is not valid"); + } + String encodedMnemonic = accountsProperties.getProperty(MASTER_MNEMONIC_PROP); + if (encodedMnemonic == null) { + throw new ValidationException("No master account present"); + } + + try { + return decryptMnemonic(encodedMnemonic, password); + } catch (Exception e) { + throw new ValidationException("Cannot decrypt your seed"); + } + } + + public void setMasterAccountMnemonic(final String mnemonic, String password) throws ValidationException { + try { + if (mnemonic != null) { + accountsProperties.setProperty(MASTER_MNEMONIC_PROP, encryptMnemonic(mnemonic, password)); + saveSettings(); + } + } catch (Exception e) { + throw new ValidationException("Cannot encode master account key"); + } + } + + public boolean hasMasterAccount() { + String mnemonic = accountsProperties.getProperty(MASTER_MNEMONIC_PROP); + return mnemonic != null && !mnemonic.equalsIgnoreCase(""); + } + + public int getMasterAccountDerivations() { + return Optional.ofNullable(accountsProperties.getProperty(MASTER_DERIVATIONS_PROP)).map(Integer::parseInt).orElse(0); + } + + public void incrementMasterAccountDerivations() { + accountsProperties.setProperty(MASTER_DERIVATIONS_PROP, getMasterAccountDerivations() + 1 + ""); + saveSettings(); + } + public final LightAppSettings getLightAppSettings(final ApiType type) { return new LightAppSettings(lightAppProperties, type); } @@ -120,4 +184,20 @@ public final void saveLightAppSettings(final LightAppSettings lightAppSettings) saveSettings(); } } + + private String encryptMnemonic(String mnemonic, String password) throws Exception { + SecretKeySpec key = new SecretKeySpec(password.getBytes(), MNEMONIC_ENCRYPTION_ALGORITHM); + Cipher cipher = Cipher.getInstance(MNEMONIC_ENCRYPTION_ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, key); + byte[] encrypted = cipher.doFinal(mnemonic.getBytes()); + return new String(encrypted, MNEMONIC_STRING_CONVERSION_CHARSET_NAME); + } + + private String decryptMnemonic(String encryptedMnemonic, String password) throws Exception { + SecretKeySpec skeyspec = new SecretKeySpec(password.getBytes(), MNEMONIC_ENCRYPTION_ALGORITHM); + Cipher cipher = Cipher.getInstance(MNEMONIC_ENCRYPTION_ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, skeyspec); + byte[] decrypted = cipher.doFinal(encryptedMnemonic.getBytes(MNEMONIC_STRING_CONVERSION_CHARSET_NAME)); + return new String(decrypted); + } } diff --git a/src/main/java/org/aion/wallet/ui/MainWindow.java b/src/main/java/org/aion/wallet/ui/MainWindow.java index 514457a6..99ef6946 100644 --- a/src/main/java/org/aion/wallet/ui/MainWindow.java +++ b/src/main/java/org/aion/wallet/ui/MainWindow.java @@ -3,6 +3,7 @@ import com.google.common.eventbus.Subscribe; import javafx.application.Application; import javafx.application.Platform; +import javafx.event.Event; import javafx.fxml.FXMLLoader; import javafx.scene.Node; import javafx.scene.Parent; @@ -12,36 +13,45 @@ import javafx.scene.paint.Color; import javafx.stage.Stage; import javafx.stage.StageStyle; -import org.aion.log.AionLoggerFactory; +import javafx.util.Duration; import org.aion.api.log.LogEnum; import org.aion.wallet.connector.BlockchainConnector; -import org.aion.wallet.ui.events.EventBusFactory; -import org.aion.wallet.ui.events.HeaderPaneButtonEvent; -import org.aion.wallet.ui.events.WindowControlsEvent; +import org.aion.wallet.dto.LightAppSettings; +import org.aion.wallet.events.*; +import org.aion.wallet.log.WalletLoggerFactory; +import org.aion.wallet.ui.components.partials.FatalErrorDialog; import org.aion.wallet.util.AionConstants; import org.aion.wallet.util.DataUpdater; import org.slf4j.Logger; +import java.io.File; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; import java.util.Map; -import java.util.Timer; import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; public class MainWindow extends Application { - private static final Logger log = AionLoggerFactory.getLogger(LogEnum.WLT.name()); + private static final Logger log = WalletLoggerFactory.getLogger(LogEnum.WLT.name()); private static final String TITLE = "Aion Wallet"; private static final String MAIN_WINDOW_FXML = "MainWindow.fxml"; - private static final String AION_LOGO = "components/icons/aion_logo.png"; + private static final String AION_LOGO = "components/icons/aion-icon.png"; + private static final String AION_UI_DIR = System.getProperty("user.dir"); + private static final String AION_EXECUTABLE = "aion_ui.sh"; private final Map panes = new HashMap<>(); - + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + private FatalErrorDialog fatalErrorDialog; private double xOffset; private double yOffset; private Stage stage; - private final Timer timer = new Timer(); + private Scene scene; + private IdleMonitor idleMonitor; + private Duration lockDelayDuration = Duration.seconds(60); @Override public void start(final Stage stage) throws IOException { @@ -55,10 +65,9 @@ public void start(final Stage stage) throws IOException { root.setOnMousePressed(this::handleMousePressed); root.setOnMouseDragged(this::handleMouseDragged); - Scene scene = new Scene(root); + scene = new Scene(root); scene.setFill(Color.TRANSPARENT); - - stage.setOnCloseRequest(t -> shutDown()); + stage.setOnCloseRequest(t -> shutDown(false)); stage.setTitle(TITLE); stage.setScene(scene); @@ -71,16 +80,62 @@ public void start(final Stage stage) throws IOException { panes.put(HeaderPaneButtonEvent.Type.HISTORY, scene.lookup("#historyPane")); panes.put(HeaderPaneButtonEvent.Type.SETTINGS, scene.lookup("#settingsPane")); - timer.schedule( + scheduler.scheduleAtFixedRate( new DataUpdater(), AionConstants.BLOCK_MINING_TIME_MILLIS, - 3 * AionConstants.BLOCK_MINING_TIME_MILLIS + 3 * AionConstants.BLOCK_MINING_TIME_MILLIS, + TimeUnit.MILLISECONDS ); + registerIdleMonitor(); + + fatalErrorDialog = new FatalErrorDialog(); + } + + + private long computeDelay(int lockTimeOut, String lockTimeOutMeasurementUnit) { + if (lockTimeOutMeasurementUnit == null) { + return 60; + } + switch (lockTimeOutMeasurementUnit) { + case "seconds": + return lockTimeOut; + case "minutes": + return lockTimeOut * 60; + case "hours": + return lockTimeOut * 3600; + default: + return 60; + } + } + + private void registerIdleMonitor() { + if (scene == null || lockDelayDuration == null) { + return; + } + if (idleMonitor != null) { + idleMonitor.stopMonitoring(); + idleMonitor = null; + } + idleMonitor = new IdleMonitor(lockDelayDuration, BlockchainConnector.getInstance()::lockAll); + idleMonitor.register(scene, Event.ANY); + } + + @Subscribe + private void handleSettingsChanged(final SettingsEvent event) { + if (SettingsEvent.Type.CHANGED.equals(event.getType())) { + final LightAppSettings settings = event.getSettings(); + if (settings != null) { + lockDelayDuration = Duration.seconds(computeDelay(settings.getUnlockTimeout(), settings.getLockTimeoutMeasurementUnit())); + registerIdleMonitor(); + } + } } private void registerEventBusConsumer() { EventBusFactory.getBus(WindowControlsEvent.ID).register(this); EventBusFactory.getBus(HeaderPaneButtonEvent.ID).register(this); + EventBusFactory.getBus(SettingsEvent.ID).register(this); + EventBusFactory.getBus(ErrorEvent.ID).register(this); } @Subscribe @@ -89,21 +144,22 @@ private void handleWindowControlsEvent(final WindowControlsEvent event) { case MINIMIZE: minimize(event); break; + case RESTART: + shutDown(true); + break; case CLOSE: - shutDown(); + shutDown(false); break; } } @Subscribe private void handleHeaderPaneButtonEvent(final HeaderPaneButtonEvent event) { - if(stage.getScene() == null) { + if (stage.getScene() == null) { return; } - log.debug(event.getType().toString()); - // todo: refactor by adding a view controller - for(Map.Entry entry: panes.entrySet()) { - if(event.getType().equals(entry.getKey())) { + for (Map.Entry entry : panes.entrySet()) { + if (event.getType().equals(entry.getKey())) { entry.getValue().setVisible(true); } else { entry.getValue().setVisible(false); @@ -111,16 +167,37 @@ private void handleHeaderPaneButtonEvent(final HeaderPaneButtonEvent event) { } } + @Subscribe + private void handleErrorEvent(final ErrorEvent errorEvent) { + Platform.runLater(() -> fatalErrorDialog.open(scene.getRoot())); + } + private void minimize(final WindowControlsEvent event) { ((Stage) event.getSource().getScene().getWindow()).setIconified(true); } - private void shutDown() { + private void shutDown(final boolean restart) { Platform.exit(); BlockchainConnector.getInstance().close(); + scheduler.shutdown(); + if (restart) { + restartApplication(); + } Executors.newSingleThreadExecutor().submit(() -> System.exit(0)); - timer.cancel(); - timer.purge(); + } + + private void restartApplication() { + final String executable = AION_UI_DIR + File.separator + AION_EXECUTABLE; + + final ArrayList command = new ArrayList<>(); + command.add(executable); + + final ProcessBuilder builder = new ProcessBuilder(command); + try { + builder.start(); + } catch (IOException e) { + log.error(e.getMessage(), e); + } } private void handleMousePressed(final MouseEvent event) { diff --git a/src/main/java/org/aion/wallet/ui/components/AbstractController.java b/src/main/java/org/aion/wallet/ui/components/AbstractController.java index f23a3b03..59890e60 100644 --- a/src/main/java/org/aion/wallet/ui/components/AbstractController.java +++ b/src/main/java/org/aion/wallet/ui/components/AbstractController.java @@ -8,10 +8,9 @@ import javafx.fxml.Initializable; import javafx.scene.Node; import org.aion.api.log.LogEnum; +import org.aion.wallet.events.EventBusFactory; +import org.aion.wallet.events.RefreshEvent; import org.aion.wallet.log.WalletLoggerFactory; -import org.aion.wallet.ui.events.EventBusFactory; -import org.aion.wallet.ui.events.RefreshEvent; -import org.aion.wallet.util.DataUpdater; import org.slf4j.Logger; import java.net.URL; @@ -23,13 +22,12 @@ public abstract class AbstractController implements Initializable { + protected static final String ERROR_STYLE = "error-label"; private static final Logger log = WalletLoggerFactory.getLogger(LogEnum.WLT.name()); - + private final static ExecutorService API_EXECUTOR = Executors.newSingleThreadExecutor(); @FXML private Node parent; - private final static ExecutorService API_EXECUTOR = Executors.newSingleThreadExecutor(); - @Override public final void initialize(final URL location, final ResourceBundle resources) { registerEventBusConsumer(); @@ -37,14 +35,12 @@ public final void initialize(final URL location, final ResourceBundle resources) } protected void registerEventBusConsumer() { - EventBusFactory.getBus(DataUpdater.UI_DATA_REFRESH).register(this); + EventBusFactory.getBus(RefreshEvent.ID).register(this); } @Subscribe private void handleRefreshEvent(final RefreshEvent event) { - if (isInView()) { - refreshView(event); - } + refreshView(event); } protected final Task getApiTask(final Function consumer, T param) { diff --git a/src/main/java/org/aion/wallet/ui/components/HeaderPaneControls.java b/src/main/java/org/aion/wallet/ui/components/HeaderPaneControls.java index 995f4796..c600379d 100644 --- a/src/main/java/org/aion/wallet/ui/components/HeaderPaneControls.java +++ b/src/main/java/org/aion/wallet/ui/components/HeaderPaneControls.java @@ -12,36 +12,32 @@ import javafx.scene.input.MouseEvent; import javafx.scene.layout.VBox; import org.aion.api.log.LogEnum; -import org.aion.log.AionLoggerFactory; import org.aion.wallet.connector.BlockchainConnector; import org.aion.wallet.dto.AccountDTO; -import org.aion.wallet.ui.events.EventBusFactory; -import org.aion.wallet.ui.events.EventPublisher; -import org.aion.wallet.ui.events.HeaderPaneButtonEvent; -import org.aion.wallet.ui.events.RefreshEvent; +import org.aion.wallet.events.AccountEvent; +import org.aion.wallet.events.EventBusFactory; +import org.aion.wallet.events.HeaderPaneButtonEvent; +import org.aion.wallet.events.RefreshEvent; +import org.aion.wallet.log.WalletLoggerFactory; import org.aion.wallet.util.BalanceUtils; import org.aion.wallet.util.UIUtils; +import org.aion.wallet.util.URLManager; import org.slf4j.Logger; -import java.awt.*; -import java.io.IOException; import java.math.BigInteger; -import java.net.URI; -import java.net.URISyntaxException; import java.net.URL; +import java.util.EnumSet; import java.util.HashMap; import java.util.Map; import java.util.ResourceBundle; public class HeaderPaneControls extends AbstractController { - private static final Logger log = AionLoggerFactory.getLogger(LogEnum.WLT.name()); + private static final Logger log = WalletLoggerFactory.getLogger(LogEnum.WLT.name()); - private static final String AION_URL = "http://www.aion.network"; + private static final String STYLE_DEFAULT = "header-button-default"; - private static final String STYLE_DEFAULT = "default"; - - private static final String STYLE_PRESSED = "pressed"; + private static final String STYLE_PRESSED = "header-button-pressed"; private final BlockchainConnector blockchainConnector = BlockchainConnector.getInstance(); @@ -61,12 +57,12 @@ public class HeaderPaneControls extends AbstractController { private VBox receiveButton; @FXML private VBox historyButton; -// @FXML + // @FXML // private VBox contractsButton; @FXML private VBox settingsButton; - private String accountAddress; + private String accountAddress = ""; @Override public void internalInit(URL location, ResourceBundle resources) { @@ -83,28 +79,23 @@ public void internalInit(URL location, ResourceBundle resources) { @Override protected void registerEventBusConsumer() { super.registerEventBusConsumer(); - EventBusFactory.getBus(EventPublisher.ACCOUNT_CHANGE_EVENT_ID).register(this); + EventBusFactory.getBus(AccountEvent.ID).register(this); } public void openAionWebSite() { - try { - Desktop.getDesktop().browse(new URI(AION_URL)); - } catch (IOException | URISyntaxException e) { - log.error("Exception occurred trying to open website: %s", e.getMessage(), e); - } + URLManager.openDashboard(); } public void handleButtonPressed(final MouseEvent pressed) { for (final Node headerButton : headerButtons.keySet()) { - ObservableList styleClass = headerButton.getStyleClass(); - styleClass.clear(); + headerButton.getStyleClass().clear(); if (pressed.getSource().equals(headerButton)) { - styleClass.add(STYLE_PRESSED); + headerButton.getStyleClass().add(STYLE_PRESSED); setStyleToChildren(headerButton, "header-button-label-pressed"); HeaderPaneButtonEvent headerPaneButtonEvent = headerButtons.get(headerButton); sendPressedEvent(headerPaneButtonEvent); } else { - styleClass.add(STYLE_DEFAULT); + headerButton.getStyleClass().add(STYLE_DEFAULT); setStyleToChildren(headerButton, "header-button-label"); } } @@ -131,26 +122,38 @@ private void sendPressedEvent(final HeaderPaneButtonEvent event) { } @Subscribe - private void handleAccountChanged(final AccountDTO account) { - accountBalance.setVisible(true); - activeAccountLabel.setVisible(true); - activeAccount.setText(account.getName()); - accountAddress = account.getPublicAddress(); - accountBalance.setText(account.getBalance() + BalanceUtils.CCY_SEPARATOR + account.getCurrency()); - UIUtils.setWidth(activeAccount); - UIUtils.setWidth(accountBalance); + private void handleAccountEvent(final AccountEvent event) { + final AccountDTO account = event.getPayload(); + if (EnumSet.of(AccountEvent.Type.CHANGED, AccountEvent.Type.ADDED).contains(event.getType())) { + if (account.isActive()) { + accountBalance.setText(account.getBalance() + BalanceUtils.CCY_SEPARATOR + account.getCurrency()); + accountBalance.setVisible(true); + activeAccount.setText(account.getName()); + activeAccountLabel.setVisible(true); + accountAddress = account.getPublicAddress(); + UIUtils.setWidth(activeAccount); + UIUtils.setWidth(accountBalance); + } + } else if (AccountEvent.Type.LOCKED.equals(event.getType())){ + if (account.getPublicAddress().equals(accountAddress)){ + accountAddress = ""; + activeAccountLabel.setVisible(false); + accountBalance.setVisible(false); + activeAccount.setText(""); + } + } } @Override protected final void refreshView(final RefreshEvent event) { - if (accountAddress != null && !accountAddress.isEmpty()) { + if (!accountAddress.isEmpty()) { final String[] text = accountBalance.getText().split(BalanceUtils.CCY_SEPARATOR); final String currency = text[1]; final Task getBalanceTask = getApiTask(blockchainConnector::getBalance, accountAddress); runApiTask( getBalanceTask, evt -> updateNewBalance(currency, getBalanceTask.getValue()), - getErrorEvent(throwable -> {}, getBalanceTask), + getErrorEvent(t -> {}, getBalanceTask), getEmptyEvent() ); } diff --git a/src/main/java/org/aion/wallet/ui/components/HistoryController.java b/src/main/java/org/aion/wallet/ui/components/HistoryController.java index dcd10dee..2c5db689 100644 --- a/src/main/java/org/aion/wallet/ui/components/HistoryController.java +++ b/src/main/java/org/aion/wallet/ui/components/HistoryController.java @@ -1,9 +1,12 @@ package org.aion.wallet.ui.components; +import com.google.common.collect.Lists; import com.google.common.eventbus.Subscribe; import javafx.beans.property.SimpleStringProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; +import javafx.collections.transformation.SortedList; import javafx.concurrent.Task; import javafx.event.ActionEvent; import javafx.event.Event; @@ -12,38 +15,67 @@ import javafx.scene.control.*; import javafx.scene.control.cell.PropertyValueFactory; import javafx.scene.input.*; +import org.aion.api.log.LogEnum; import org.aion.wallet.connector.BlockchainConnector; import org.aion.wallet.connector.dto.TransactionDTO; import org.aion.wallet.dto.AccountDTO; -import org.aion.wallet.ui.events.EventBusFactory; -import org.aion.wallet.ui.events.EventPublisher; -import org.aion.wallet.ui.events.HeaderPaneButtonEvent; +import org.aion.wallet.events.AccountEvent; +import org.aion.wallet.events.EventBusFactory; +import org.aion.wallet.events.HeaderPaneButtonEvent; +import org.aion.wallet.log.WalletLoggerFactory; import org.aion.wallet.util.AddressUtils; +import org.aion.wallet.util.AionConstants; import org.aion.wallet.util.BalanceUtils; +import org.aion.wallet.util.URLManager; +import org.slf4j.Logger; import java.net.URL; -import java.util.List; -import java.util.Optional; -import java.util.ResourceBundle; +import java.text.SimpleDateFormat; +import java.util.*; import java.util.stream.Collectors; public class HistoryController extends AbstractController { + private static final Logger log = WalletLoggerFactory.getLogger(LogEnum.WLT.name()); + private static final String COPY_MENU = "Copy"; + private static final String LINK_STYLE = "link-style"; + + private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("yyyy/MM/dd - HH.mm.ss"); + private final BlockchainConnector blockchainConnector = BlockchainConnector.getInstance(); @FXML private TableView txTable; + @FXML + private TextField searchField; + + @FXML + private ComboBox searchItem; + private AccountDTO account; + private List completeTransactionList = Lists.newArrayList(); protected void internalInit(final URL location, final ResourceBundle resources) { + initSearchItemDropDown(); buildTableModel(); setEventHandlers(); reloadWalletView(); } + private void initSearchItemDropDown() { + searchItem.setItems(FXCollections.observableArrayList( + "Type", + "Date", + "Transaction hash", + "Value", + "Status" + )); + searchItem.getSelectionModel().select(0); + } + @Subscribe private void handleHeaderPaneButtonEvent(final HeaderPaneButtonEvent event) { if (event.getType().equals(HeaderPaneButtonEvent.Type.HISTORY)) { @@ -52,17 +84,26 @@ private void handleHeaderPaneButtonEvent(final HeaderPaneButtonEvent event) { } @Subscribe - private void handleAccountChanged(final AccountDTO account) { - this.account = account; - if (isInView()) { - reloadWalletView(); + private void handleAccountChanged(final AccountEvent event) { + if (EnumSet.of(AccountEvent.Type.CHANGED, AccountEvent.Type.ADDED).contains(event.getType())) { + this.account = event.getPayload(); + if (isInView()) { + reloadWalletView(); + } else { + txTable.setItems(FXCollections.emptyObservableList()); + } + } else if (AccountEvent.Type.LOCKED.equals(event.getType())) { + if (event.getPayload().equals(account)) { + account = null; + txTable.setItems(FXCollections.emptyObservableList()); + } } } protected void registerEventBusConsumer() { super.registerEventBusConsumer(); EventBusFactory.getBus(HeaderPaneButtonEvent.ID).register(this); - EventBusFactory.getBus(EventPublisher.ACCOUNT_CHANGE_EVENT_ID).register(this); + EventBusFactory.getBus(AccountEvent.ID).register(this); } private void reloadWalletView() { @@ -71,46 +112,86 @@ private void reloadWalletView() { } final Task> getTransactionsTask = getApiTask( address -> blockchainConnector.getLatestTransactions(address).stream() - .map(t -> new TxRow(address, t)) - .collect(Collectors.toList()), + .map(t -> new TxRow(address, t)).collect(Collectors.toList()), account.getPublicAddress() ); runApiTask( getTransactionsTask, - event -> txTable.setItems(FXCollections.observableList(getTransactionsTask.getValue())), + event -> { + final List transactions = getTransactionsTask.getValue(); + completeTransactionList = new ArrayList<>(transactions); + txTable.setItems(FXCollections.observableList(transactions)); + }, getEmptyEvent(), getEmptyEvent() ); } private void buildTableModel() { - final TableColumn typeCol = new TableColumn<>("Type"); - final TableColumn nameCol = new TableColumn<>("Name"); - final TableColumn addrCol = new TableColumn<>("Address"); - final TableColumn hashCol = new TableColumn<>("Tx Hash"); - final TableColumn valueCol = new TableColumn<>("Value"); - typeCol.setCellValueFactory(new PropertyValueFactory<>("type")); - nameCol.setCellValueFactory(new PropertyValueFactory<>("name")); - addrCol.setCellValueFactory(new PropertyValueFactory<>("address")); - hashCol.setCellValueFactory(new PropertyValueFactory<>("txHash")); - valueCol.setCellValueFactory(new PropertyValueFactory<>("value")); - typeCol.prefWidthProperty().bind(txTable.widthProperty().multiply(0.08)); - nameCol.prefWidthProperty().bind(txTable.widthProperty().multiply(0.09)); - addrCol.prefWidthProperty().bind(txTable.widthProperty().multiply(0.36)); - hashCol.prefWidthProperty().bind(txTable.widthProperty().multiply(0.36)); - valueCol.prefWidthProperty().bind(txTable.widthProperty().multiply(0.11)); - - txTable.getColumns().addAll(typeCol, nameCol, addrCol, hashCol, valueCol); + final TableColumn typeCol = getTableColumn("Type", "type", 0.09); + final TableColumn nameCol = getTableColumn("Date", "date", 0.2); + final TableColumn hashCol = getTableColumn("Tx Hash", "txHash", 0.5); + final TableColumn valueCol = getTableColumn("Value", "value", 0.12); + final TableColumn statusCol = getTableColumn("Status", "status", 0.1); + + hashCol.setCellFactory(column -> new TransactionHashCell()); + + txTable.getColumns().addAll(Arrays.asList(typeCol, nameCol, hashCol, valueCol, statusCol)); + } + + private TableColumn getTableColumn(final String header, final String property, final double sizePercent) { + final TableColumn valueCol = new TableColumn<>(header); + valueCol.setCellValueFactory(new PropertyValueFactory<>(property)); + valueCol.prefWidthProperty().bind(txTable.widthProperty().multiply(sizePercent)); + return valueCol; } private void setEventHandlers() { txTable.setOnKeyPressed(new KeyTableCopyEventHandler()); + txTable.setOnMouseClicked(new MouseLinkEventHandler()); + ContextMenu menu = new ContextMenu(); final MenuItem copyItem = new MenuItem(COPY_MENU); copyItem.setOnAction(new ContextMenuTableCopyEventHandler(txTable)); menu.getItems().add(copyItem); txTable.setContextMenu(menu); + + searchField.textProperty().addListener((observable, oldValue, newValue) -> { + final FilteredList filteredData = new FilteredList<>(FXCollections.observableList(completeTransactionList), s -> true); + if (!newValue.isEmpty()) { + filteredData.setPredicate(s -> anyFieldHasString(s, newValue)); + SortedList sortedData = new SortedList<>(filteredData); + sortedData.comparatorProperty().bind(txTable.comparatorProperty()); + txTable.setItems(sortedData); + } + }); + + searchItem.valueProperty().addListener((observable, oldValue, newValue) -> { + final FilteredList filteredData = new FilteredList<>(FXCollections.observableList(completeTransactionList), s -> true); + if(!String.valueOf(newValue).equals(String.valueOf(oldValue))) { + filteredData.setPredicate(s -> anyFieldHasString(s, searchField.getText())); + SortedList sortedData = new SortedList<>(filteredData); + sortedData.comparatorProperty().bind(txTable.comparatorProperty()); + txTable.setItems(sortedData); + } + }); + } + + private boolean anyFieldHasString(final TxRow currentRow, final String searchString) { + switch (searchItem.getSelectionModel().getSelectedIndex()) { + case 0: + return currentRow.getType().toLowerCase().contains(searchString.toLowerCase()); + case 1: + return currentRow.getDate().toLowerCase().contains(searchString.toLowerCase()); + case 2: + return currentRow.getTxHash().toLowerCase().contains(searchString.toLowerCase()); + case 3: + return currentRow.getValue().toLowerCase().contains(searchString.toLowerCase()); + case 4: + return currentRow.getStatus().toLowerCase().contains(searchString.toLowerCase()); + } + return true; } private static class KeyTableCopyEventHandler extends TableCopyEventHandler { @@ -127,6 +208,8 @@ public void handle(final KeyEvent keyEvent) { } private static class ContextMenuTableCopyEventHandler extends TableCopyEventHandler { + + private final TableView txTable; public ContextMenuTableCopyEventHandler(final TableView txTable) { @@ -170,15 +253,40 @@ protected final void copySelectionToClipboard(TableView table) { } } + private static class MouseLinkEventHandler implements EventHandler { + + @Override + public void handle(final MouseEvent mouseEvent) { + if (MouseEvent.MOUSE_CLICKED.equals(mouseEvent.getEventType()) && MouseButton.PRIMARY.equals(mouseEvent.getButton())) { + if (mouseEvent.getSource() instanceof TableView) { + redirect((TableView) mouseEvent.getSource()); + mouseEvent.consume(); + } + } + } + + private void redirect(final TableView table) { + ObservableList positionList = table.getSelectionModel().getSelectedCells(); + for (TablePosition position : positionList) { + int row = position.getRow(); + int col = position.getColumn(); + if (table.getColumns().get(col).getText().equals("Tx Hash")) { + Object cell = table.getColumns().get(col).getCellData(row); + URLManager.openTransaction(cell.toString()); + } + } + } + } + public class TxRow { - private static final String TO = "to"; - private static final String FROM = "from"; + private static final String TO = "outgoing"; + private static final String FROM = "incoming"; private final TransactionDTO transaction; private final SimpleStringProperty type; - private final SimpleStringProperty name; - private final SimpleStringProperty address; + private final SimpleStringProperty date; + private final SimpleStringProperty status; private final SimpleStringProperty value; private final SimpleStringProperty txHash; @@ -186,16 +294,24 @@ public class TxRow { private TxRow(final String requestingAddress, final TransactionDTO dto) { transaction = dto; final AccountDTO fromAccount = blockchainConnector.getAccount(dto.getFrom()); - final AccountDTO toAccount = blockchainConnector.getAccount(dto.getTo()); final String balance = BalanceUtils.formatBalance(dto.getValue()); boolean isFromTx = AddressUtils.equals(requestingAddress, fromAccount.getPublicAddress()); this.type = new SimpleStringProperty(isFromTx ? TO : FROM); - this.name = new SimpleStringProperty(isFromTx ? toAccount.getName() : fromAccount.getName()); - this.address = new SimpleStringProperty(isFromTx ? toAccount.getPublicAddress() : fromAccount.getPublicAddress()); + this.date = new SimpleStringProperty(SIMPLE_DATE_FORMAT.format(new Date(dto.getTimeStamp() * 1000))); + this.status = new SimpleStringProperty(getTransactionStatus(dto)); this.value = new SimpleStringProperty(balance); this.txHash = new SimpleStringProperty(dto.getHash()); } + private String getTransactionStatus(TransactionDTO dto) { + final long diff = dto.getBlockNumber() + AionConstants.VALIDATION_BLOCKS_FOR_TRANSACTIONS - blockchainConnector.getSyncInfo().getNetworkBestBlkNumber(); + if (diff <= 0) { + return "Finished"; + } else { + return Math.abs(diff) + " blocks left"; + } + } + public String getType() { return type.get(); } @@ -204,20 +320,20 @@ public void setType(final String type) { this.type.setValue(type); } - public String getName() { - return name.get(); + public String getDate() { + return date.get(); } public void setName(final String name) { - this.name.setValue(name); + this.date.setValue(name); } - public String getAddress() { - return address.get(); + public String getStatus() { + return status.get(); } - public void setAddress(final String address) { - this.address.setValue(address); + public void setStatus(final String status) { + this.status.setValue(status); } public String getValue() { @@ -240,4 +356,23 @@ public TransactionDTO getTransaction() { return transaction; } } + + private class TransactionHashCell extends TableCell { + @Override + protected void updateItem(final String item, final boolean empty) { + super.updateItem(item, empty); + + setText(empty ? "" : item); + + getStyleClass().clear(); + updateStyles(empty ? null : item); + } + + private void updateStyles(final String item) { + if (item == null) { + return; + } + getStyleClass().add(LINK_STYLE); + } + } } diff --git a/src/main/java/org/aion/wallet/ui/components/OverviewController.java b/src/main/java/org/aion/wallet/ui/components/OverviewController.java index 884f1e92..7cd4f88d 100644 --- a/src/main/java/org/aion/wallet/ui/components/OverviewController.java +++ b/src/main/java/org/aion/wallet/ui/components/OverviewController.java @@ -4,32 +4,49 @@ import javafx.collections.FXCollections; import javafx.concurrent.Task; import javafx.fxml.FXML; +import javafx.scene.control.Button; import javafx.scene.control.ListView; import javafx.scene.input.MouseEvent; +import org.aion.api.log.LogEnum; import org.aion.wallet.connector.BlockchainConnector; +import org.aion.wallet.console.ConsoleManager; import org.aion.wallet.dto.AccountDTO; +import org.aion.wallet.events.*; +import org.aion.wallet.exception.ValidationException; +import org.aion.wallet.log.WalletLoggerFactory; import org.aion.wallet.ui.components.partials.AddAccountDialog; -import org.aion.wallet.ui.events.EventBusFactory; -import org.aion.wallet.ui.events.EventPublisher; -import org.aion.wallet.ui.events.HeaderPaneButtonEvent; -import org.aion.wallet.ui.events.RefreshEvent; +import org.aion.wallet.ui.components.partials.ImportAccountDialog; +import org.aion.wallet.ui.components.partials.UnlockMasterAccountDialog; +import org.slf4j.Logger; import java.net.URL; +import java.util.EnumSet; import java.util.List; import java.util.ResourceBundle; public class OverviewController extends AbstractController { + private static final Logger log = WalletLoggerFactory.getLogger(LogEnum.WLT.name()); + private final BlockchainConnector blockchainConnector = BlockchainConnector.getInstance(); @FXML + private Button addMasterAccountButton; + @FXML + private Button unlockMasterAccountButton; + @FXML private ListView accountListView; private AddAccountDialog addAccountDialog; + private ImportAccountDialog importAccountDialog; + private UnlockMasterAccountDialog unlockMasterAccountDialog; + private AccountDTO account; @Override public void internalInit(final URL location, final ResourceBundle resources) { addAccountDialog = new AddAccountDialog(); + importAccountDialog = new ImportAccountDialog(); + unlockMasterAccountDialog = new UnlockMasterAccountDialog(); reloadAccounts(); } @@ -37,7 +54,17 @@ public void internalInit(final URL location, final ResourceBundle resources) { protected void registerEventBusConsumer() { super.registerEventBusConsumer(); EventBusFactory.getBus(HeaderPaneButtonEvent.ID).register(this); - EventBusFactory.getBus(EventPublisher.ACCOUNT_CHANGE_EVENT_ID).register(this); + EventBusFactory.getBus(AccountEvent.ID).register(this); + } + + private void displayFooterActions() { + if (blockchainConnector.hasMasterAccount() && !blockchainConnector.isMasterAccountUnlocked()) { + unlockMasterAccountButton.setVisible(true); + addMasterAccountButton.setVisible(false); + } else { + unlockMasterAccountButton.setVisible(false); + addMasterAccountButton.setVisible(true); + } } private void reloadAccounts() { @@ -45,9 +72,11 @@ private void reloadAccounts() { runApiTask( getAccountsTask, evt -> reloadAccountObservableList(getAccountsTask.getValue()), - getErrorEvent(throwable -> {}, getAccountsTask), + getErrorEvent(t -> { + }, getAccountsTask), getEmptyEvent() ); + displayFooterActions(); } private void reloadAccountObservableList(List accounts) { @@ -58,28 +87,57 @@ private void reloadAccountObservableList(List accounts) { } @Subscribe - private void handleAccountChanged(final AccountDTO account) { - this.account = account; - // todo: don't reload the account list from blockchain connector - reloadAccounts(); + private void handleAccountEvent(final AccountEvent event) { + final AccountDTO account = event.getPayload(); + if (EnumSet.of(AccountEvent.Type.CHANGED, AccountEvent.Type.ADDED).contains(event.getType())) { + if (account.isActive()) { + this.account = account; + } + reloadAccounts(); + } else if (AccountEvent.Type.LOCKED.equals(event.getType())) { + if (account.equals(this.account)) { + this.account = null; + } + reloadAccounts(); + } + } + + @Override + protected void refreshView(final RefreshEvent event) { + switch (event.getType()) { + case CONNECTED: + case TRANSACTION_FINISHED: + reloadAccounts(); + } } @Subscribe private void handleHeaderPaneButtonEvent(final HeaderPaneButtonEvent event) { if (event.getType().equals(HeaderPaneButtonEvent.Type.OVERVIEW)) { reloadAccounts(); - addAccountDialog.close(); } } - @Subscribe - private void handleRefreshEvent(final RefreshEvent event){ - if (RefreshEvent.Type.OPERATION_FINISHED.equals(event.getType())){ - reloadAccounts(); - } + public void unlockMasterAccount(MouseEvent mouseEvent) { + unlockMasterAccountDialog.open(mouseEvent); + } + + public void openImportAccountDialog(MouseEvent mouseEvent) { + importAccountDialog.open(mouseEvent); } public void openAddAccountDialog(MouseEvent mouseEvent) { + if (this.blockchainConnector.hasMasterAccount()) { + try { + blockchainConnector.createAccount(); + ConsoleManager.addLog("New address created", ConsoleManager.LogType.ACCOUNT); + } catch (ValidationException e) { + ConsoleManager.addLog("Address cannot be created", ConsoleManager.LogType.ACCOUNT, ConsoleManager.LogLevel.WARNING); + log.error(e.getMessage(), e); + // todo: display on yui + } + return; + } addAccountDialog.open(mouseEvent); } } diff --git a/src/main/java/org/aion/wallet/ui/components/ReceiveController.java b/src/main/java/org/aion/wallet/ui/components/ReceiveController.java index 0e1e54fb..29267a9c 100644 --- a/src/main/java/org/aion/wallet/ui/components/ReceiveController.java +++ b/src/main/java/org/aion/wallet/ui/components/ReceiveController.java @@ -1,32 +1,36 @@ package org.aion.wallet.ui.components; import com.google.common.eventbus.Subscribe; +import javafx.embed.swing.SwingFXUtils; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.TextArea; import javafx.scene.control.Tooltip; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; import javafx.scene.input.Clipboard; import javafx.scene.input.ClipboardContent; -import javafx.scene.input.MouseEvent; -import javafx.scene.layout.AnchorPane; import org.aion.wallet.dto.AccountDTO; -import org.aion.wallet.ui.events.EventBusFactory; -import org.aion.wallet.ui.events.EventPublisher; +import org.aion.wallet.events.AccountEvent; +import org.aion.wallet.events.EventBusFactory; import java.net.URL; +import java.util.EnumSet; import java.util.ResourceBundle; public class ReceiveController implements Initializable{ + @FXML + public ImageView qrCode; @FXML private TextArea accountAddress; private Tooltip copiedTooltip; - private AccountDTO accountDTO; + private AccountDTO account; @Override - public void initialize(URL location, ResourceBundle resources) { + public void initialize(final URL location, final ResourceBundle resources) { registerEventBusConsumer(); copiedTooltip = new Tooltip(); copiedTooltip.setText("Copied"); @@ -35,21 +39,30 @@ public void initialize(URL location, ResourceBundle resources) { } private void registerEventBusConsumer() { - EventBusFactory.getBus(EventPublisher.ACCOUNT_CHANGE_EVENT_ID).register(this); + EventBusFactory.getBus(AccountEvent.ID).register(this); } @Subscribe - private void handleAccountChanged(AccountDTO accountDTO) { - this.accountDTO = accountDTO; + private void handleAccountChanged(final AccountEvent event) { + if (EnumSet.of(AccountEvent.Type.CHANGED, AccountEvent.Type.ADDED).contains(event.getType())) { + account = event.getPayload(); + accountAddress.setText(account.getPublicAddress()); - accountAddress.setText(accountDTO.getPublicAddress()); + Image image = SwingFXUtils.toFXImage(account.getQrCode(), null); + qrCode.setImage(image); + } else if (AccountEvent.Type.LOCKED.equals(event.getType())) { + if (event.getPayload().equals(account)) { + account = null; + accountAddress.setText(""); + } + } } - public void onCopyToClipBoard(MouseEvent mouseEvent) { - if(accountDTO != null && accountDTO.getPublicAddress() != null) { + public void onCopyToClipBoard() { + if (account != null && account.getPublicAddress() != null) { final Clipboard clipboard = Clipboard.getSystemClipboard(); final ClipboardContent content = new ClipboardContent(); - content.putString(accountDTO.getPublicAddress()); + content.putString(account.getPublicAddress()); clipboard.setContent(content); copiedTooltip.show(accountAddress.getScene().getWindow()); diff --git a/src/main/java/org/aion/wallet/ui/components/SendController.java b/src/main/java/org/aion/wallet/ui/components/SendController.java index b133748b..f7aeab1c 100644 --- a/src/main/java/org/aion/wallet/ui/components/SendController.java +++ b/src/main/java/org/aion/wallet/ui/components/SendController.java @@ -3,35 +3,41 @@ import com.google.common.eventbus.Subscribe; import javafx.concurrent.Task; import javafx.fxml.FXML; -import javafx.scene.control.Label; -import javafx.scene.control.PasswordField; -import javafx.scene.control.TextArea; -import javafx.scene.control.TextField; +import javafx.scene.control.*; +import javafx.scene.input.MouseEvent; +import org.aion.api.impl.internal.Message; import org.aion.api.log.LogEnum; import org.aion.base.util.TypeConverter; import org.aion.wallet.connector.BlockchainConnector; -import org.aion.wallet.connector.dto.SendRequestDTO; +import org.aion.wallet.connector.dto.SendTransactionDTO; +import org.aion.wallet.connector.dto.TransactionResponseDTO; +import org.aion.wallet.console.ConsoleManager; import org.aion.wallet.dto.AccountDTO; +import org.aion.wallet.events.*; import org.aion.wallet.exception.ValidationException; import org.aion.wallet.log.WalletLoggerFactory; -import org.aion.wallet.ui.events.EventBusFactory; -import org.aion.wallet.ui.events.EventPublisher; -import org.aion.wallet.ui.events.HeaderPaneButtonEvent; -import org.aion.wallet.ui.events.RefreshEvent; -import org.aion.wallet.util.AionConstants; -import org.aion.wallet.util.BalanceUtils; -import org.aion.wallet.util.ConfigUtils; -import org.aion.wallet.util.UIUtils; +import org.aion.wallet.ui.components.partials.TransactionResubmissionDialog; +import org.aion.wallet.util.*; import org.slf4j.Logger; import java.math.BigInteger; import java.net.URL; +import java.util.List; +import java.util.Optional; import java.util.ResourceBundle; public class SendController extends AbstractController { private static final Logger log = WalletLoggerFactory.getLogger(LogEnum.WLT.name()); + private static final String PENDING_MESSAGE = "Sending transaction..."; + + private static final String SUCCESS_MESSAGE = "Transaction finished"; + + private static final Tooltip NRG_LIMIT_TOOLTIP = new Tooltip("NRG limit"); + + private static final Tooltip NRG_PRICE_TOOLTIP = new Tooltip("NRG price"); + private final BlockchainConnector blockchainConnector = BlockchainConnector.getInstance(); @FXML @@ -51,52 +57,136 @@ public class SendController extends AbstractController { @FXML private TextField accountBalance; @FXML - private TextField equivalentEUR; + private Button sendButton; @FXML - private TextField equivalentUSD; + private Label timedoutTransactionsLabel; private AccountDTO account; + private boolean connected; + + private final TransactionResubmissionDialog transactionResubmissionDialog = new TransactionResubmissionDialog(); + + private SendTransactionDTO transactionToResubmit; + + @Override + protected void registerEventBusConsumer() { + super.registerEventBusConsumer(); + EventBusFactory.getBus(HeaderPaneButtonEvent.ID).register(this); + EventBusFactory.getBus(AccountEvent.ID).register(this); + EventBusFactory.getBus(TransactionEvent.ID).register(this); + } + + @Override + protected void internalInit(final URL location, final ResourceBundle resources) { + nrgInput.setTooltip(NRG_LIMIT_TOOLTIP); + nrgPriceInput.setTooltip(NRG_PRICE_TOOLTIP); + setDefaults(); + if (!ConfigUtils.isEmbedded()) { + passwordInput.setVisible(false); + passwordInput.setManaged(false); + } + + toInput.textProperty().addListener(event -> transactionToResubmit = null); + + nrgInput.textProperty().addListener(event -> transactionToResubmit = null); + + nrgPriceInput.textProperty().addListener(event -> transactionToResubmit = null); + + valueInput.textProperty().addListener(event -> transactionToResubmit = null); + } + + @Override + protected void refreshView(final RefreshEvent event) { + switch (event.getType()) { + case CONNECTED: + connected = true; + if (account != null) { + sendButton.setDisable(false); + } + break; + case DISCONNECTED: + connected = false; + sendButton.setDisable(true); + break; + case TRANSACTION_FINISHED: + setDefaults(); + break; + default: + } + setTimedoutTransactionsLabelText(); + } + public void onSendAionClicked() { - final SendRequestDTO dto; + if (account == null) { + return; + } + final SendTransactionDTO dto; try { - dto = mapFormData(); + if(transactionToResubmit != null) { + dto = transactionToResubmit; + } + else { + dto = mapFormData(); + } } catch (ValidationException e) { log.error(e.getMessage(), e); + displayStatus(e.getMessage(), true); return; } - txStatusLabel.setText("Sending transaction..."); + displayStatus(PENDING_MESSAGE, false); - final Task sendTransactionTask = getApiTask(this::sendTransaction, dto); + final Task sendTransactionTask = getApiTask(this::sendTransaction, dto); runApiTask( sendTransactionTask, evt -> handleTransactionFinished(sendTransactionTask.getValue()), - getErrorEvent(t -> txStatusLabel.setText(t.getMessage()), sendTransactionTask), + getErrorEvent(t -> Optional.ofNullable(t.getCause()).ifPresent(cause -> displayStatus(cause.getMessage(), true)), sendTransactionTask), getEmptyEvent() ); } - private void handleTransactionFinished(final String txHash) { - log.info("Transaction finished: " + txHash); - txStatusLabel.setText("Transaction Finished"); - EventPublisher.fireOperationFinished(); + public void onTimedoutTransactionsClick(final MouseEvent mouseEvent) { + transactionResubmissionDialog.open(mouseEvent); } - private String sendTransaction(final SendRequestDTO sendRequestDTO) { - try { - return blockchainConnector.sendTransaction(sendRequestDTO); - } catch (ValidationException e) { - throw new RuntimeException(e); + private void handleTransactionFinished(final TransactionResponseDTO response) { + setTimedoutTransactionsLabelText(); + final String error = response.getError(); + if (error != null) { + final String failReason; + final int responseStatus = response.getStatus(); + if (Message.Retcode.r_tx_Dropped_VALUE == responseStatus) { + failReason = String.format("dropped: %s", error); + } else { + failReason = "timeout"; + } + final String errorMessage = "Transaction " + failReason; + ConsoleManager.addLog(errorMessage, ConsoleManager.LogType.TRANSACTION, ConsoleManager.LogLevel.WARNING); + SendController.log.error("{}: {}", errorMessage, response); + displayStatus(errorMessage, false); + } else { + log.info("{}: {}", SUCCESS_MESSAGE, response); + ConsoleManager.addLog("Transaction sent", ConsoleManager.LogType.TRANSACTION, ConsoleManager.LogLevel.WARNING); + displayStatus(SUCCESS_MESSAGE, false); + EventPublisher.fireTransactionFinished(); } } - @Override - protected void internalInit(final URL location, final ResourceBundle resources) { - setDefaults(); - if (!ConfigUtils.isEmbedded()) { - passwordInput.setVisible(false); - passwordInput.setManaged(false); + private void displayStatus(final String message, final boolean isError) { + if (isError) { + txStatusLabel.getStyleClass().add(ERROR_STYLE); + } else { + txStatusLabel.getStyleClass().removeAll(ERROR_STYLE); + } + txStatusLabel.setText(message); + } + + private TransactionResponseDTO sendTransaction(final SendTransactionDTO sendTransactionDTO) { + try { + return blockchainConnector.sendTransaction(sendTransactionDTO); + } catch (ValidationException e) { + throw new RuntimeException(e); } } @@ -107,31 +197,40 @@ private void setDefaults() { toInput.setText(""); valueInput.setText(""); passwordInput.setText(""); + + setTimedoutTransactionsLabelText(); } - @Override - protected void refreshView(final RefreshEvent event) { - if (RefreshEvent.Type.OPERATION_FINISHED.equals(event.getType())) { - setDefaults(); + private void setTimedoutTransactionsLabelText() { + if(account != null) { + final List timedoutTransactions = blockchainConnector.getAccountManager().getTimedOutTransactions(account.getPublicAddress()); + if(!timedoutTransactions.isEmpty()) { + timedoutTransactionsLabel.setVisible(true); + timedoutTransactionsLabel.getStyleClass().add("warning-link-style"); + timedoutTransactionsLabel.setText("You have transactions that require your attention!"); + } } } @Subscribe - private void handleAccountChanged(final AccountDTO account) { - this.account = account; - - accountAddress.setText(account.getPublicAddress()); - - accountBalance.setVisible(true); - setAccountBalanceText(); - - equivalentEUR.setVisible(true); - equivalentEUR.setText(convertBalanceToCcy(account, AionConstants.AION_TO_EUR) + " " + AionConstants.EUR_CCY); - UIUtils.setWidth(equivalentEUR); - - equivalentUSD.setVisible(true); - equivalentUSD.setText(convertBalanceToCcy(account, AionConstants.AION_TO_USD) + " " + AionConstants.USD_CCY); - UIUtils.setWidth(equivalentUSD); + private void handleAccountEvent(final AccountEvent event) { + final AccountDTO account = event.getPayload(); + if (AccountEvent.Type.CHANGED.equals(event.getType())) { + if (account.isActive()) { + this.account = account; + sendButton.setDisable(!connected); + accountAddress.setText(this.account.getPublicAddress()); + accountBalance.setVisible(true); + setAccountBalanceText(); + } + } else if (AccountEvent.Type.LOCKED.equals(event.getType())) { + if (account.equals(this.account)) { + sendButton.setDisable(true); + accountAddress.setText(""); + accountBalance.setVisible(false); + this.account = null; + } + } } @Subscribe @@ -141,15 +240,17 @@ private void handleHeaderPaneButtonEvent(final HeaderPaneButtonEvent event) { } } - @Override - protected void registerEventBusConsumer() { - super.registerEventBusConsumer(); - EventBusFactory.getBus(HeaderPaneButtonEvent.ID).register(this); - EventBusFactory.getBus(EventPublisher.ACCOUNT_CHANGE_EVENT_ID).register(this); - } - - private double convertBalanceToCcy(final AccountDTO account, final double exchangeRate) { - return Double.parseDouble(account.getBalance()) * exchangeRate; + @Subscribe + private void handleTransactionResubmitEvent(final TransactionEvent event) { + SendTransactionDTO sendTransaction = event.getTransaction(); + sendTransaction.setNrgPrice(BigInteger.valueOf(sendTransaction.getNrgPrice() * 2)); + toInput.setText(sendTransaction.getTo()); + nrgInput.setText(sendTransaction.getNrg().toString()); + nrgPriceInput.setText(String.valueOf(sendTransaction.getNrgPrice())); + valueInput.setText(BalanceUtils.formatBalance(sendTransaction.getValue())); + txStatusLabel.setText(""); + timedoutTransactionsLabel.setVisible(false); + transactionToResubmit = sendTransaction; } private void setAccountBalanceText() { @@ -169,36 +270,50 @@ private void refreshAccountBalance() { account.setBalance(BalanceUtils.formatBalance(getBalanceTask.getValue())); setAccountBalanceText(); }, - getErrorEvent(throwable -> {}, getBalanceTask), + getErrorEvent(t -> {}, getBalanceTask), getEmptyEvent() ); } - private SendRequestDTO mapFormData() throws ValidationException { - final SendRequestDTO dto = new SendRequestDTO(); + private SendTransactionDTO mapFormData() throws ValidationException { + final SendTransactionDTO dto = new SendTransactionDTO(); dto.setFrom(account.getPublicAddress()); + + if (!AddressUtils.isValid(toInput.getText())) { + throw new ValidationException("Address is not a valid AION address!"); + } dto.setTo(toInput.getText()); - dto.setPassword(passwordInput.getText()); try { - dto.setNrg(TypeConverter.StringNumberAsBigInt(nrgInput.getText()).longValue()); + final long nrg = TypeConverter.StringNumberAsBigInt(nrgInput.getText()).longValue(); + if (nrg <= 0) { + throw new ValidationException("Nrg must be greater than 0!"); + } + dto.setNrg(nrg); } catch (NumberFormatException e) { - throw new ValidationException("Nrg must be a valid number"); + throw new ValidationException("Nrg must be a valid number!"); } try { - dto.setNrgPrice(TypeConverter.StringNumberAsBigInt(nrgPriceInput.getText())); + final BigInteger nrgPrice = TypeConverter.StringNumberAsBigInt(nrgPriceInput.getText()); + dto.setNrgPrice(nrgPrice); + if (nrgPrice.compareTo(AionConstants.DEFAULT_NRG_PRICE) < 0) { + throw new ValidationException(String.format("Nrg price must be greater than %s!", AionConstants.DEFAULT_NRG_PRICE)); + } } catch (NumberFormatException e) { - throw new ValidationException("Nrg price must be a valid number"); + throw new ValidationException("Nrg price must be a valid number!"); } try { - dto.setValue(BalanceUtils.extractBalance(valueInput.getText())); + final BigInteger value = BalanceUtils.extractBalance(valueInput.getText()); + if (value.compareTo(BigInteger.ZERO) <= 0) { + throw new ValidationException("Amount must be greater than 0"); + } + dto.setValue(value); } catch (NumberFormatException e) { - throw new ValidationException("Value must be a number"); + throw new ValidationException("Amount must be a number"); } return dto; } - } diff --git a/src/main/java/org/aion/wallet/ui/components/SettingsController.java b/src/main/java/org/aion/wallet/ui/components/SettingsController.java index 90b183a8..5ace700a 100644 --- a/src/main/java/org/aion/wallet/ui/components/SettingsController.java +++ b/src/main/java/org/aion/wallet/ui/components/SettingsController.java @@ -1,16 +1,26 @@ package org.aion.wallet.ui.components; import com.google.common.eventbus.Subscribe; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import javafx.fxml.FXML; +import javafx.scene.Scene; +import javafx.scene.control.ComboBox; import javafx.scene.control.Label; +import javafx.scene.control.TextArea; import javafx.scene.control.TextField; +import javafx.scene.layout.StackPane; +import javafx.stage.Stage; import org.aion.api.log.LogEnum; import org.aion.wallet.connector.BlockchainConnector; +import org.aion.wallet.console.ConsoleManager; import org.aion.wallet.dto.LightAppSettings; +import org.aion.wallet.events.EventBusFactory; +import org.aion.wallet.events.EventPublisher; +import org.aion.wallet.events.HeaderPaneButtonEvent; +import org.aion.wallet.events.SettingsEvent; +import org.aion.wallet.exception.ValidationException; import org.aion.wallet.log.WalletLoggerFactory; -import org.aion.wallet.ui.events.EventBusFactory; -import org.aion.wallet.ui.events.EventPublisher; -import org.aion.wallet.ui.events.HeaderPaneButtonEvent; import org.slf4j.Logger; import java.net.URL; @@ -19,18 +29,19 @@ public class SettingsController extends AbstractController { private static final Logger log = WalletLoggerFactory.getLogger(LogEnum.WLT.name()); + private static final String DEFAULT_PROTOCOL = "tcp"; private final BlockchainConnector blockchainConnector = BlockchainConnector.getInstance(); - - @FXML - public TextField protocol; @FXML public TextField address; @FXML public TextField port; @FXML public Label notification; - + @FXML + private TextField timeout; + @FXML + private ComboBox timeoutMeasurementUnit; private LightAppSettings settings; @Override @@ -41,12 +52,48 @@ protected void internalInit(final URL location, final ResourceBundle resources) @Override protected void registerEventBusConsumer() { EventBusFactory.getBus(HeaderPaneButtonEvent.ID).register(this); + EventBusFactory.getBus(SettingsEvent.ID).register(this); } public void changeSettings() { - EventPublisher.fireApplicationSettingsChanged(new LightAppSettings(address.getText().trim(), port.getText().trim(), - protocol.getText().trim(), settings.getType())); - notification.setText("Changes applied"); + final LightAppSettings newSettings; + try { + newSettings = new LightAppSettings( + address.getText().trim(), + port.getText().trim(), + DEFAULT_PROTOCOL, + settings.getType(), + Integer.parseInt(timeout.getText()), + getSelectedTimeoutMeasurementUnit() + + ); + displayNotification("", false); + EventPublisher.fireApplicationSettingsChanged(newSettings); + } catch (ValidationException e) { + ConsoleManager.addLog("Could not update settings", ConsoleManager.LogType.SETTINGS, ConsoleManager.LogLevel.WARNING); + log.error(e.getMessage(), e); + displayNotification(e.getMessage(), true); + } + } + + public void openConsole() { + ConsoleManager.show(); + } + + private String getSelectedTimeoutMeasurementUnit() { + String measurementUnit = null; + switch (timeoutMeasurementUnit.getSelectionModel().getSelectedIndex()) { + case 0: + measurementUnit = "seconds"; + break; + case 1: + measurementUnit = "minutes"; + break; + case 2: + measurementUnit = "hours"; + break; + } + return measurementUnit; } @Subscribe @@ -56,11 +103,56 @@ private void handleHeaderPaneButtonEvent(final HeaderPaneButtonEvent event) { } } + @Subscribe + private void handleSettingsChanged(final SettingsEvent event) { + if (SettingsEvent.Type.APPLIED.equals(event.getType())) { + displayNotification("Changes applied", false); + ConsoleManager.addLog("Settings updated", ConsoleManager.LogType.SETTINGS); + } + } + private void reloadView() { settings = blockchainConnector.getSettings(); - protocol.setText(settings.getProtocol()); address.setText(settings.getAddress()); port.setText(settings.getPort()); - notification.setText(""); + timeout.setText(settings.getUnlockTimeout().toString()); + timeoutMeasurementUnit.setItems(getTimeoutMeasurementUnits()); + setInitialMeasurementUnit(settings.getLockTimeoutMeasurementUnit()); + displayNotification("", false); + } + + private void setInitialMeasurementUnit(String unlockTimeoutMeasurementUnit) { + int initialIndex = 0; + switch (unlockTimeoutMeasurementUnit) { + case "seconds": + initialIndex = 0; + break; + case "minutes": + initialIndex = 1; + break; + case "hours": + initialIndex = 2; + break; + } + timeoutMeasurementUnit.getSelectionModel().select(initialIndex); + } + + private ObservableList getTimeoutMeasurementUnits() { + ObservableList options = + FXCollections.observableArrayList( + "seconds", + "minutes", + "hours" + ); + return options; + } + + private void displayNotification(final String message, final boolean isError) { + if (isError) { + notification.getStyleClass().add(ERROR_STYLE); + } else { + notification.getStyleClass().removeAll(ERROR_STYLE); + } + notification.setText(message); } } diff --git a/src/main/java/org/aion/wallet/ui/components/account/AccountCellItem.java b/src/main/java/org/aion/wallet/ui/components/account/AccountCellItem.java index bdf18c0d..e0d00996 100644 --- a/src/main/java/org/aion/wallet/ui/components/account/AccountCellItem.java +++ b/src/main/java/org/aion/wallet/ui/components/account/AccountCellItem.java @@ -1,35 +1,57 @@ package org.aion.wallet.ui.components.account; +import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; -import javafx.scene.control.ContentDisplay; -import javafx.scene.control.ListCell; -import javafx.scene.control.TextField; +import javafx.geometry.Insets; +import javafx.scene.Node; +import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseEvent; +import javafx.scene.layout.HBox; import org.aion.wallet.dto.AccountDTO; +import org.aion.wallet.events.EventPublisher; +import org.aion.wallet.ui.components.partials.SaveKeystoreDialog; import org.aion.wallet.ui.components.partials.UnlockAccountDialog; -import org.aion.wallet.ui.events.EventPublisher; import org.aion.wallet.util.BalanceUtils; import org.aion.wallet.util.UIUtils; import java.io.IOException; import java.io.InputStream; -public class AccountCellItem extends ListCell{ +public class AccountCellItem extends ListCell { private static final String ICON_CONNECTED = "/org/aion/wallet/ui/components/icons/icon-connected-50.png"; + private static final String ICON_DISCONNECTED = "/org/aion/wallet/ui/components/icons/icon-disconnected-50.png"; private static final String ICON_EDIT = "/org/aion/wallet/ui/components/icons/pencil-edit-button.png"; + private static final String ICON_CONFIRM = "/org/aion/wallet/ui/components/icons/icons8-checkmark-50.png"; private static final String NAME_INPUT_FIELDS_SELECTED_STYLE = "name-input-fields-selected"; + private static final String NAME_INPUT_FIELDS_STYLE = "name-input-fields"; + private static final Tooltip CONNECT_ACCOUNT_TOOLTIP = new Tooltip("Connect with this account"); + + private static final Tooltip CONNECTED_ACCOUNT_TOOLTIP = new Tooltip("Connected account"); + + private static final Tooltip EDIT_NAME_TOOLTIP = new Tooltip("Edit account name"); + + private static final Tooltip EXPORT_ACCOUNT_TOOLTIP = new Tooltip("Export to keystore"); + + private final UnlockAccountDialog accountUnlockDialog = new UnlockAccountDialog(); + + private final SaveKeystoreDialog saveKeystoreDialog = new SaveKeystoreDialog(); + + @FXML + private TextField importedLabel; + @FXML + private HBox nameBox; @FXML private TextField name; @FXML @@ -40,13 +62,14 @@ public class AccountCellItem extends ListCell{ private ImageView accountSelectButton; @FXML private ImageView editNameButton; + @FXML + private ImageView accountExportButton; private boolean nameInEditMode; - private final UnlockAccountDialog accountUnlockDialog = new UnlockAccountDialog(); - public AccountCellItem() { loadFXML(); + addToolTips(); publicAddress.setPrefWidth(575); } @@ -62,9 +85,13 @@ private void loadFXML() { } } + private void addToolTips() { + Tooltip.install(editNameButton, EDIT_NAME_TOOLTIP); + Tooltip.install(accountExportButton, EXPORT_ACCOUNT_TOOLTIP); + } + private void submitNameOnEnterPressed(final KeyEvent event) { if (event.getCode().equals(KeyCode.ENTER)) { - submitName(); updateNameFieldOnSave(); } } @@ -73,10 +100,8 @@ private void submitName() { name.setEditable(false); final AccountDTO accountDTO = getItem(); accountDTO.setName(name.getText()); - EventPublisher.fireAccountChanged(accountDTO); - updateItem(accountDTO, false); - accountDTO.setActive(true); updateItem(accountDTO, false); + EventPublisher.fireAccountChanged(accountDTO); } @Override @@ -89,33 +114,49 @@ protected void updateItem(AccountDTO item, boolean empty) { name.setText(item.getName()); UIUtils.setWidth(name); + final ObservableList children = nameBox.getChildren(); + children.removeAll(importedLabel, name); + if (item.isImported()) { + children.addAll(importedLabel, name); + } else { + children.add(name); + } + publicAddress.setText(item.getPublicAddress()); + publicAddress.setPadding(new Insets(5, 0, 0, 10)); + balance.setText(item.getBalance() + BalanceUtils.CCY_SEPARATOR + item.getCurrency()); UIUtils.setWidth(balance); if (item.isActive()) { final InputStream resource = getClass().getResourceAsStream(ICON_CONNECTED); accountSelectButton.setImage(new Image(resource)); + Tooltip.uninstall(accountSelectButton, CONNECT_ACCOUNT_TOOLTIP); + Tooltip.install(accountSelectButton, CONNECTED_ACCOUNT_TOOLTIP); } else { final InputStream resource = getClass().getResourceAsStream(ICON_DISCONNECTED); accountSelectButton.setImage(new Image(resource)); + Tooltip.uninstall(accountSelectButton, CONNECTED_ACCOUNT_TOOLTIP); + Tooltip.install(accountSelectButton, CONNECT_ACCOUNT_TOOLTIP); } setContentDisplay(ContentDisplay.GRAPHIC_ONLY); } } - public void onDisconnectedClicked(MouseEvent mouseEvent) { + @FXML + public void onDisconnectedClicked(final MouseEvent mouseEvent) { final AccountDTO modifiedAccount = this.getItem(); - if(!modifiedAccount.isUnlocked()) { + if (!modifiedAccount.isUnlocked()) { accountUnlockDialog.open(mouseEvent); - EventPublisher.fireUnlockAccount(modifiedAccount); - } - else { + EventPublisher.fireAccountUnlocked(modifiedAccount); + } else { + modifiedAccount.setActive(true); EventPublisher.fireAccountChanged(modifiedAccount); } } + @FXML public void onNameFieldClicked() { if (!nameInEditMode) { name.setEditable(true); @@ -132,6 +173,18 @@ public void onNameFieldClicked() { } } + @FXML + public void onExportClicked(final MouseEvent mouseEvent){ + final AccountDTO account = getItem(); + if (!account.isUnlocked()) { + accountUnlockDialog.open(mouseEvent); + EventPublisher.fireAccountUnlocked(account); + } else { + saveKeystoreDialog.open(mouseEvent); + EventPublisher.fireAccountExport(account); + } + } + private void updateNameFieldOnSave() { if (name.getText() != null && getItem() != null && getItem().getName() != null) { name.getStyleClass().clear(); diff --git a/src/main/java/org/aion/wallet/ui/components/partials/AboutDialog.java b/src/main/java/org/aion/wallet/ui/components/partials/AboutDialog.java new file mode 100644 index 00000000..06d2937d --- /dev/null +++ b/src/main/java/org/aion/wallet/ui/components/partials/AboutDialog.java @@ -0,0 +1,44 @@ +package org.aion.wallet.ui.components.partials; + +import javafx.fxml.FXMLLoader; +import javafx.scene.Node; +import javafx.scene.input.InputEvent; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.Pane; +import javafx.stage.Popup; +import org.aion.api.log.LogEnum; +import org.aion.wallet.log.WalletLoggerFactory; +import org.slf4j.Logger; + +import java.io.IOException; + +public class AboutDialog { + private static final Logger log = WalletLoggerFactory.getLogger(LogEnum.WLT.name()); + + public void open(final MouseEvent mouseEvent) { + Popup popup = new Popup(); + popup.setAutoHide(true); + popup.setAutoFix(true); + + Pane addAccountDialog; + try { + addAccountDialog = FXMLLoader.load(getClass().getResource("AboutDialog.fxml")); + } catch (IOException e) { + log.error(e.getMessage(), e); + return; + } + + Node eventSource = (Node) mouseEvent.getSource(); + final double windowX = eventSource.getScene().getWindow().getX(); + final double windowY = eventSource.getScene().getWindow().getY(); + popup.setX(windowX + eventSource.getScene().getWidth() / 2 - addAccountDialog.getPrefWidth() / 2); + popup.setY(windowY + eventSource.getScene().getHeight() / 2 - addAccountDialog.getPrefHeight() / 2); + popup.getContent().addAll(addAccountDialog); + popup.show(eventSource.getScene().getWindow()); + } + + public void close(final InputEvent eventSource) { + ((Node) eventSource.getSource()).getScene().getWindow().hide(); + } + +} diff --git a/src/main/java/org/aion/wallet/ui/components/partials/AboutPage.java b/src/main/java/org/aion/wallet/ui/components/partials/AboutPage.java new file mode 100644 index 00000000..8269d5eb --- /dev/null +++ b/src/main/java/org/aion/wallet/ui/components/partials/AboutPage.java @@ -0,0 +1,11 @@ +package org.aion.wallet.ui.components.partials; + +import javafx.scene.input.MouseEvent; + +public class AboutPage { + private final AboutDialog aboutDialog = new AboutDialog(); + + public void openAboutDialog(MouseEvent mouseEvent) { + aboutDialog.open(mouseEvent); + } +} diff --git a/src/main/java/org/aion/wallet/ui/components/partials/AddAccountDialog.java b/src/main/java/org/aion/wallet/ui/components/partials/AddAccountDialog.java index 1417a64b..f8112a5f 100644 --- a/src/main/java/org/aion/wallet/ui/components/partials/AddAccountDialog.java +++ b/src/main/java/org/aion/wallet/ui/components/partials/AddAccountDialog.java @@ -1,71 +1,116 @@ package org.aion.wallet.ui.components.partials; +import io.github.novacrypto.bip39.MnemonicValidator; +import io.github.novacrypto.bip39.Validation.InvalidChecksumException; +import io.github.novacrypto.bip39.Validation.InvalidWordCountException; +import io.github.novacrypto.bip39.Validation.UnexpectedWhiteSpaceException; +import io.github.novacrypto.bip39.Validation.WordNotFoundException; +import io.github.novacrypto.bip39.wordlists.English; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.control.PasswordField; import javafx.scene.control.TextField; +import javafx.scene.input.InputEvent; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseEvent; import javafx.scene.layout.Pane; import javafx.stage.Popup; -import org.aion.api.log.AionLoggerFactory; import org.aion.api.log.LogEnum; import org.aion.wallet.connector.BlockchainConnector; -import org.aion.wallet.ui.events.EventBusFactory; -import org.aion.wallet.ui.events.HeaderPaneButtonEvent; +import org.aion.wallet.console.ConsoleManager; +import org.aion.wallet.events.EventPublisher; +import org.aion.wallet.exception.ValidationException; +import org.aion.wallet.log.WalletLoggerFactory; import org.slf4j.Logger; import java.io.IOException; public class AddAccountDialog { - private static final Logger log = AionLoggerFactory.getLogger(LogEnum.WLT.name()); + private static final Logger log = WalletLoggerFactory.getLogger(LogEnum.WLT.name()); - private ImportAccountDialog importAccountDialog = new ImportAccountDialog(); + private final MnemonicDialog mnemonicDialog = new MnemonicDialog(); + private final BlockchainConnector blockchainConnector = BlockchainConnector.getInstance(); + @FXML + public TextField mnemonicTextField; + @FXML + public PasswordField mnemonicPasswordField; @FXML private TextField newAccountName; - @FXML private PasswordField newPassword; - @FXML private PasswordField retypedPassword; - @FXML private Label validationError; - private final Popup popup = new Popup(); - - private final BlockchainConnector blockchainConnector = BlockchainConnector.getInstance(); - - public void createAccount() { + public void createAccount(final InputEvent mouseEvent) { resetValidation(); - if (validateFields()) { - blockchainConnector.createAccount(newPassword.getText(), newAccountName.getText()); - EventBusFactory.getBus(HeaderPaneButtonEvent.ID).post(new HeaderPaneButtonEvent(HeaderPaneButtonEvent.Type.OVERVIEW)); - } - else { + + if (!validateFields()) { String error = ""; - if(newPassword.getText().isEmpty() || retypedPassword.getText().isEmpty()) { + if (newPassword.getText().isEmpty() || retypedPassword.getText().isEmpty()) { error = "Please complete the fields!"; - } - else if(!newPassword.getText().equals(retypedPassword.getText())) { + } else if (!newPassword.getText().equals(retypedPassword.getText())) { error = "Passwords don't match!"; } showInvalidFieldsError(error); + return; + } + + try { + String mnemonic = blockchainConnector.createMasterAccount(newPassword.getText(), newAccountName.getText()); + ConsoleManager.addLog("Master account created -> name: " + newAccountName.getText(), ConsoleManager.LogType.ACCOUNT); + + if (mnemonic != null) { + mnemonicDialog.open(mouseEvent); + EventPublisher.fireMnemonicCreated(mnemonic); + } + } catch (ValidationException e) { + ConsoleManager.addLog("Master account could not be created", ConsoleManager.LogType.ACCOUNT, ConsoleManager.LogLevel.WARNING); + showInvalidFieldsError(e.getMessage()); } } - public void uploadKeystoreFile(MouseEvent e) { - importAccountDialog.open(e); + public void importMnemonic(final InputEvent mouseEvent) { + final String mnemonic = mnemonicTextField.getText(); + final String mnemonicPassword = mnemonicPasswordField.getText(); + if (mnemonic != null && !mnemonic.isEmpty() && mnemonicPassword != null && !mnemonicPassword.isEmpty()) { + try { + MnemonicValidator + .ofWordList(English.INSTANCE) + .validate(mnemonic); + blockchainConnector.importMasterAccount(mnemonic, mnemonicPassword); + ConsoleManager.addLog("Master account imported", ConsoleManager.LogType.ACCOUNT); + this.close(mouseEvent); + } catch (UnexpectedWhiteSpaceException | InvalidWordCountException | InvalidChecksumException | WordNotFoundException | ValidationException e) { + ConsoleManager.addLog("Could not import master account", ConsoleManager.LogType.ACCOUNT, ConsoleManager.LogLevel.WARNING); + showInvalidFieldsError(getMnemonicValidationErrorMessage(e)); + log.error(e.getMessage(), e); + } + } else { + showInvalidFieldsError("Please complete the fields!"); + } + } + + private String getMnemonicValidationErrorMessage(Exception e) { + if (e instanceof UnexpectedWhiteSpaceException) { + return "There are spaces in the mnemonic!"; + } else if (e instanceof InvalidWordCountException) { + return "Mnemonic word length is invalid!"; + } else if (e instanceof InvalidChecksumException) { + return "Invalid mnemonic!"; + } else if (e instanceof WordNotFoundException) { + return "Word in mnemonic was not found!"; + } else return e.getMessage(); } private boolean validateFields() { - if(newPassword == null || newPassword.getText() == null || retypedPassword == null || retypedPassword.getText() == null) { + if (newPassword == null || newPassword.getText() == null || retypedPassword == null || retypedPassword.getText() == null) { return false; } @@ -78,12 +123,13 @@ public void resetValidation() { validationError.setVisible(false); } - private void showInvalidFieldsError(String message) { + private void showInvalidFieldsError(final String message) { validationError.setVisible(true); validationError.setText(message); } - public void open(MouseEvent mouseEvent) { + public void open(final MouseEvent mouseEvent) { + Popup popup = new Popup(); popup.setAutoHide(true); popup.setAutoFix(true); @@ -104,14 +150,21 @@ public void open(MouseEvent mouseEvent) { popup.show(eventSource.getScene().getWindow()); } - public void close() { - popup.hide(); + public void close(final InputEvent eventSource) { + ((Node) eventSource.getSource()).getScene().getWindow().hide(); + } + + @FXML + private void submitCreate(final KeyEvent event) { + if (event.getCode().equals(KeyCode.ENTER)) { + createAccount(event); + } } @FXML - private void submitOnEnterPressed(final KeyEvent event) { + private void submitImport(final KeyEvent event) { if (event.getCode().equals(KeyCode.ENTER)) { - createAccount(); + importMnemonic(event); } } } diff --git a/src/main/java/org/aion/wallet/ui/components/partials/ConnectivityStatusController.java b/src/main/java/org/aion/wallet/ui/components/partials/ConnectivityStatusController.java index 9d08988a..3d942a3c 100644 --- a/src/main/java/org/aion/wallet/ui/components/partials/ConnectivityStatusController.java +++ b/src/main/java/org/aion/wallet/ui/components/partials/ConnectivityStatusController.java @@ -5,9 +5,10 @@ import javafx.scene.control.Label; import org.aion.wallet.connector.BlockchainConnector; import org.aion.wallet.ui.components.AbstractController; -import org.aion.wallet.ui.events.RefreshEvent; +import org.aion.wallet.events.RefreshEvent; import java.net.URL; +import java.util.EnumSet; import java.util.ResourceBundle; public class ConnectivityStatusController extends AbstractController { @@ -27,12 +28,12 @@ public void internalInit(final URL location, final ResourceBundle resources) { @Override protected final void refreshView(final RefreshEvent event) { - if (RefreshEvent.Type.TIMER.equals(event.getType())) { - final Task getConnectedStatusTask = getApiTask(o -> blockchainConnector.getConnectionStatusByConnectedPeers(), null); + if (EnumSet.of(RefreshEvent.Type.TIMER, RefreshEvent.Type.CONNECTED).contains(event.getType())) { + final Task getConnectedStatusTask = getApiTask(o -> blockchainConnector.getConnectionStatus(), null); runApiTask( getConnectedStatusTask, evt -> setConnectivityLabel(getConnectedStatusTask.getValue()), - getErrorEvent(throwable -> {}, getConnectedStatusTask), + getErrorEvent(t -> {}, getConnectedStatusTask), getEmptyEvent()); } } diff --git a/src/main/java/org/aion/wallet/ui/components/partials/FatalErrorDialog.java b/src/main/java/org/aion/wallet/ui/components/partials/FatalErrorDialog.java new file mode 100644 index 00000000..66f218d6 --- /dev/null +++ b/src/main/java/org/aion/wallet/ui/components/partials/FatalErrorDialog.java @@ -0,0 +1,64 @@ +package org.aion.wallet.ui.components.partials; + +import javafx.fxml.FXMLLoader; +import javafx.fxml.Initializable; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.input.InputEvent; +import javafx.scene.layout.Pane; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import org.aion.api.log.LogEnum; +import org.aion.wallet.events.EventBusFactory; +import org.aion.wallet.events.WindowControlsEvent; +import org.aion.wallet.log.WalletLoggerFactory; +import org.slf4j.Logger; + +import java.io.IOException; +import java.net.URL; +import java.util.ResourceBundle; + +public class FatalErrorDialog implements Initializable { + + private static final Logger log = WalletLoggerFactory.getLogger(LogEnum.WLT.name()); + + @Override + public void initialize(final URL location, final ResourceBundle resources) { + } + + public void open(final Node eventSource) { + StackPane pane = new StackPane(); + Pane mnemonicDialog; + try { + mnemonicDialog = FXMLLoader.load(getClass().getResource("FatalErrorDialog.fxml")); + } catch (IOException e) { + log.error(e.getMessage(), e); + return; + } + pane.getChildren().add(mnemonicDialog); + Scene secondScene = new Scene(pane, mnemonicDialog.getPrefWidth(), mnemonicDialog.getPrefHeight()); + secondScene.setFill(Color.TRANSPARENT); + + Stage popup = new Stage(); + popup.setTitle("Mnemonic"); + popup.setScene(secondScene); + + popup.setX(eventSource.getScene().getWindow().getX() + eventSource.getScene().getWidth() / 2 - mnemonicDialog.getPrefWidth() / 2); + popup.setY(eventSource.getScene().getWindow().getY() + eventSource.getScene().getHeight() / 2 - mnemonicDialog.getPrefHeight() / 2); + popup.initModality(Modality.APPLICATION_MODAL); + popup.initStyle(StageStyle.TRANSPARENT); + + popup.show(); + } + + public void close(InputEvent eventSource) { + ((Node) eventSource.getSource()).getScene().getWindow().hide(); + } + + public void restartWallet() { + EventBusFactory.getBus(WindowControlsEvent.ID).post(new WindowControlsEvent(WindowControlsEvent.Type.RESTART, null)); + } +} diff --git a/src/main/java/org/aion/wallet/ui/components/partials/ImportAccountDialog.java b/src/main/java/org/aion/wallet/ui/components/partials/ImportAccountDialog.java index 6a54aee6..0b780f70 100644 --- a/src/main/java/org/aion/wallet/ui/components/partials/ImportAccountDialog.java +++ b/src/main/java/org/aion/wallet/ui/components/partials/ImportAccountDialog.java @@ -19,13 +19,14 @@ import javafx.stage.Modality; import javafx.stage.Stage; import javafx.stage.StageStyle; -import org.aion.api.log.AionLoggerFactory; import org.aion.api.log.LogEnum; import org.aion.base.util.Hex; import org.aion.wallet.connector.BlockchainConnector; +import org.aion.wallet.console.ConsoleManager; import org.aion.wallet.dto.AccountDTO; +import org.aion.wallet.events.EventPublisher; import org.aion.wallet.exception.ValidationException; -import org.aion.wallet.ui.events.EventPublisher; +import org.aion.wallet.log.WalletLoggerFactory; import org.slf4j.Logger; import java.io.File; @@ -36,12 +37,13 @@ public class ImportAccountDialog implements Initializable { - private static final Logger log = AionLoggerFactory.getLogger(LogEnum.WLT.name()); + private static final Logger log = WalletLoggerFactory.getLogger(LogEnum.WLT.name()); private static final String PK_RADIO_BUTTON_ID = "PK_RB"; private static final String KEYSTORE_RADIO_BUTTON_ID = "KEYSTORE_RB"; + private final BlockchainConnector blockchainConnector = BlockchainConnector.getInstance(); @FXML @@ -80,7 +82,7 @@ public class ImportAccountDialog implements Initializable { private byte[] keystoreFile; public void uploadKeystoreFile() throws IOException { - resetValidation(null); + resetValidation(); FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("Open UTC Keystore File"); File file = fileChooser.showOpenDialog(null); @@ -94,47 +96,68 @@ public void uploadKeystoreFile() throws IOException { public void importAccount(InputEvent eventSource) { AccountDTO account = null; + final boolean shouldKeep = rememberAccount.isSelected(); if (importKeystoreView.isVisible()) { - String password = keystorePassword.getText(); - if (!password.isEmpty() && keystoreFile != null) { - try { - account = blockchainConnector.addKeystoreUTCFile(keystoreFile, password, rememberAccount.isSelected()); - } catch (final ValidationException e) { - log.error(e.getMessage(), e); - return; - } - } - else { - validationError.setText("Please complete the fields!"); - validationError.setVisible(true); - return; + account = getAccountFromKeyStore(shouldKeep); + } else if (importPrivateKeyView.isVisible()) { + account = getAccountFromPrivateKey(shouldKeep); + } + + if (account != null) { + EventPublisher.fireAccountChanged(account); + this.close(eventSource); + } + } + + private AccountDTO getAccountFromKeyStore(final boolean shouldKeep) { + String password = keystorePassword.getText(); + if (!password.isEmpty() && keystoreFile != null) { + try { + AccountDTO dto = blockchainConnector.importKeystoreFile(keystoreFile, password, shouldKeep); + ConsoleManager.addLog("Keystore imported", ConsoleManager.LogType.ACCOUNT); + return dto; + } catch (final ValidationException e) { + ConsoleManager.addLog("Keystore could not be imported", ConsoleManager.LogType.ACCOUNT, ConsoleManager.LogLevel.WARNING); + log.error(e.getMessage(), e); + displayError(e.getMessage()); + return null; } } else { - String password = privateKeyPassword.getText(); - String privateKey = privateKeyInput.getText(); - if (password != null && !password.isEmpty() && privateKey != null && !privateKey.isEmpty()) { - byte[] raw = Hex.decode(privateKey.startsWith("0x") ? privateKey.substring(2) : privateKey); - if(raw == null) { - log.error("Invalid private key: " + privateKey); - return; - } - try { - account = blockchainConnector.addPrivateKey(raw, password, rememberAccount.isSelected()); - } catch (ValidationException e) { - log.error(e.getMessage(), e); - return; - } + displayError("Please complete the fields!"); + return null; + } + } + + private AccountDTO getAccountFromPrivateKey(final boolean shouldKeep) { + String password = privateKeyPassword.getText(); + String privateKey = privateKeyInput.getText(); + if (password != null && !password.isEmpty() && privateKey != null && !privateKey.isEmpty()) { + byte[] raw = Hex.decode(privateKey.startsWith("0x") ? privateKey.substring(2) : privateKey); + if (raw == null) { + final String errorMessage = "Invalid private key: " + privateKey; + log.error(errorMessage); + displayError(errorMessage); + return null; } - else { - validationError.setText("Please complete the fields!"); - validationError.setVisible(true); - return; + try { + AccountDTO dto = blockchainConnector.importPrivateKey(raw, password, shouldKeep); + ConsoleManager.addLog("Private key imported", ConsoleManager.LogType.ACCOUNT); + return dto; + } catch (ValidationException e) { + ConsoleManager.addLog("Private key could not be imported", ConsoleManager.LogType.ACCOUNT, ConsoleManager.LogLevel.WARNING); + log.error(e.getMessage(), e); + displayError(e.getMessage()); + return null; } + } else { + displayError("Please complete the fields!"); + return null; } - if(account != null) { - EventPublisher.fireAccountChanged(account); - } - this.close(eventSource); + } + + private void displayError(final String message) { + validationError.setText(message); + validationError.setVisible(true); } public void open(MouseEvent mouseEvent) { @@ -182,7 +205,7 @@ private void submitOnEnterPressed(final KeyEvent event) { } - public void resetValidation(MouseEvent mouseEvent) { + public void resetValidation() { validationError.setVisible(false); } diff --git a/src/main/java/org/aion/wallet/ui/components/partials/MnemonicDialog.java b/src/main/java/org/aion/wallet/ui/components/partials/MnemonicDialog.java new file mode 100644 index 00000000..10191271 --- /dev/null +++ b/src/main/java/org/aion/wallet/ui/components/partials/MnemonicDialog.java @@ -0,0 +1,78 @@ +package org.aion.wallet.ui.components.partials; + +import com.google.common.eventbus.Subscribe; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.fxml.Initializable; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.TextArea; +import javafx.scene.input.InputEvent; +import javafx.scene.layout.Pane; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import org.aion.api.log.LogEnum; +import org.aion.wallet.events.EventBusFactory; +import org.aion.wallet.events.UiMessageEvent; +import org.aion.wallet.log.WalletLoggerFactory; +import org.slf4j.Logger; + +import java.io.IOException; +import java.net.URL; +import java.util.ResourceBundle; + +public class MnemonicDialog implements Initializable{ + private static final Logger log = WalletLoggerFactory.getLogger(LogEnum.WLT.name()); + + @FXML + private TextArea mnemonicTextArea; + + public void open(InputEvent mouseEvent) { + StackPane pane = new StackPane(); + Pane mnemonicDialog; + try { + mnemonicDialog = FXMLLoader.load(getClass().getResource("MnemonicDialog.fxml")); + } catch (IOException e) { + log.error(e.getMessage(), e); + return; + } + pane.getChildren().add(mnemonicDialog); + Scene secondScene = new Scene(pane, mnemonicDialog.getPrefWidth(), mnemonicDialog.getPrefHeight()); + secondScene.setFill(Color.TRANSPARENT); + + Stage popup = new Stage(); + popup.setTitle("Mnemonic"); + popup.setScene(secondScene); + + Node eventSource = (Node) mouseEvent.getSource(); + popup.setX(eventSource.getScene().getWindow().getX() + eventSource.getScene().getWidth() / 2 - mnemonicDialog.getPrefWidth() / 2); + popup.setY(eventSource.getScene().getWindow().getY() + eventSource.getScene().getHeight() / 2 - mnemonicDialog.getPrefHeight() / 2); + popup.initModality(Modality.APPLICATION_MODAL); + popup.initStyle(StageStyle.TRANSPARENT); + + popup.show(); + } + + public void close(InputEvent eventSource) { + ((Node) eventSource.getSource()).getScene().getWindow().hide(); + } + + @Override + public void initialize(URL location, ResourceBundle resources) { + registerEventBusConsumer(); + } + @Subscribe + private void handleReceivedMnemonic(UiMessageEvent event) { + if (UiMessageEvent.Type.MNEMONIC_CREATED.equals(event.getType())) { + mnemonicTextArea.setText(event.getMessage()); + mnemonicTextArea.setEditable(false); + } + } + + private void registerEventBusConsumer() { + EventBusFactory.getBus(UiMessageEvent.ID).register(this); + } +} diff --git a/src/main/java/org/aion/wallet/ui/components/partials/PeerCountController.java b/src/main/java/org/aion/wallet/ui/components/partials/PeerCountController.java index 97288545..50156888 100644 --- a/src/main/java/org/aion/wallet/ui/components/partials/PeerCountController.java +++ b/src/main/java/org/aion/wallet/ui/components/partials/PeerCountController.java @@ -1,17 +1,22 @@ package org.aion.wallet.ui.components.partials; -import com.google.common.eventbus.Subscribe; import javafx.concurrent.Task; import javafx.fxml.FXML; import javafx.scene.control.Label; import org.aion.wallet.connector.BlockchainConnector; +import org.aion.wallet.events.RefreshEvent; import org.aion.wallet.ui.components.AbstractController; -import org.aion.wallet.ui.events.RefreshEvent; import java.net.URL; +import java.util.EnumSet; import java.util.ResourceBundle; public class PeerCountController extends AbstractController { + + private static final String ONE = " peer"; + + private static final String MANY = " peers"; + @FXML private Label peerCount; @@ -23,22 +28,18 @@ public void internalInit(final URL location, final ResourceBundle resources) { @Override protected final void refreshView(final RefreshEvent event) { - if (RefreshEvent.Type.TIMER.equals(event.getType())) { + if (EnumSet.of(RefreshEvent.Type.TIMER, RefreshEvent.Type.CONNECTED).contains(event.getType())) { final Task getPeerCountTask = getApiTask(o -> blockchainConnector.getPeerCount(), null); runApiTask( getPeerCountTask, evt -> setPeerCount(getPeerCountTask.getValue()), - getErrorEvent(throwable -> {}, getPeerCountTask), + getErrorEvent(t -> {}, getPeerCountTask), getEmptyEvent() ); } } - private void setPeerCount(int numberOfPeers) { - if(numberOfPeers == 1) { - peerCount.setText(numberOfPeers + " peer"); - return; - } - peerCount.setText(numberOfPeers + " peers"); + private void setPeerCount(final int numberOfPeers) { + peerCount.setText(numberOfPeers + (numberOfPeers == 1 ? ONE : MANY)); } } diff --git a/src/main/java/org/aion/wallet/ui/components/partials/SaveKeystoreDialog.java b/src/main/java/org/aion/wallet/ui/components/partials/SaveKeystoreDialog.java new file mode 100644 index 00000000..89db72bc --- /dev/null +++ b/src/main/java/org/aion/wallet/ui/components/partials/SaveKeystoreDialog.java @@ -0,0 +1,179 @@ +package org.aion.wallet.ui.components.partials; + +import com.google.common.eventbus.Subscribe; +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.fxml.Initializable; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; +import javafx.scene.input.InputEvent; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.Pane; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import javafx.stage.DirectoryChooser; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import org.aion.api.log.LogEnum; +import org.aion.mcf.account.Keystore; +import org.aion.wallet.connector.BlockchainConnector; +import org.aion.wallet.console.ConsoleManager; +import org.aion.wallet.dto.AccountDTO; +import org.aion.wallet.events.AccountEvent; +import org.aion.wallet.events.EventBusFactory; +import org.aion.wallet.exception.ValidationException; +import org.aion.wallet.log.WalletLoggerFactory; +import org.slf4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.ResourceBundle; + +public class SaveKeystoreDialog implements Initializable { + + private static final Logger log = WalletLoggerFactory.getLogger(LogEnum.WLT.name()); + + private static final Tooltip LOCKED_PASSWORD = new Tooltip("Can't change stored account password"); + private static final Tooltip NEW_PASSWORD = new Tooltip("New keystore file password"); + private static final String PASSWORD_PLACEHOLDER = " "; + private static final String CHOOSER_TITLE = "Keystore Destination"; + + private final BlockchainConnector blockchainConnector = BlockchainConnector.getInstance(); + private AccountDTO account; + private String destinationDirectory; + + @FXML + private PasswordField keystorePassword; + @FXML + private Label validationError; + @FXML + private TextField keystoreTextView; + + @Override + public void initialize(final URL location, final ResourceBundle resources) { + registerEventBusConsumer(); + Platform.runLater(() -> keystorePassword.requestFocus()); + } + + private void registerEventBusConsumer() { + EventBusFactory.getBus(AccountEvent.ID).register(this); + } + + public void open(final MouseEvent mouseEvent) { + final StackPane pane = new StackPane(); + final Pane saveKeystoreDialog; + try { + saveKeystoreDialog = FXMLLoader.load(getClass().getResource("SaveKeystoreDialog.fxml")); + } catch (IOException e) { + log.error(e.getMessage(), e); + return; + } + pane.getChildren().add(saveKeystoreDialog); + final Scene secondScene = new Scene(pane, saveKeystoreDialog.getPrefWidth(), saveKeystoreDialog.getPrefHeight()); + secondScene.setFill(Color.TRANSPARENT); + + final Stage popup = new Stage(); + popup.setTitle("Export Account"); + popup.setScene(secondScene); + + final Node eventSource = (Node) mouseEvent.getSource(); + popup.setX(eventSource.getScene().getWindow().getX() + eventSource.getScene().getWidth() / 2 - saveKeystoreDialog.getPrefWidth() / 2); + popup.setY(eventSource.getScene().getWindow().getY() + eventSource.getScene().getHeight() / 2 - saveKeystoreDialog.getPrefHeight() / 2); + popup.initModality(Modality.APPLICATION_MODAL); + popup.initStyle(StageStyle.TRANSPARENT); + + popup.show(); + } + + @Subscribe + private void handleUnlockStarted(final AccountEvent event) { + if (AccountEvent.Type.EXPORT.equals(event.getType())) { + this.account = event.getPayload(); + if (isRemembered()) { + keystorePassword.setEditable(false); + keystorePassword.setText(PASSWORD_PLACEHOLDER); + Tooltip.uninstall(keystorePassword, NEW_PASSWORD); + Tooltip.install(keystorePassword, LOCKED_PASSWORD); + } else { + keystorePassword.setEditable(true); + keystorePassword.setText(""); + Tooltip.uninstall(keystorePassword, LOCKED_PASSWORD); + Tooltip.install(keystorePassword, NEW_PASSWORD); + } + } + } + + @FXML + private void saveKeystore(final InputEvent event) { + final String password = keystorePassword.getText(); + if (isRemembered() || (password != null && !password.isEmpty())) { + try { + blockchainConnector.exportAccount(account, password, destinationDirectory); + final String infoMsg = "Account: " + account.getPublicAddress() + " exported to " + destinationDirectory; + ConsoleManager.addLog(infoMsg, ConsoleManager.LogType.ACCOUNT); + log.info(infoMsg); + close(event); + } catch (ValidationException e) { + ConsoleManager.addLog("Account: " + account.getPublicAddress() + " could not be exported", ConsoleManager.LogType.ACCOUNT, ConsoleManager.LogLevel.WARNING); + validationError.setText(e.getMessage()); + validationError.setVisible(true); + log.error(e.getMessage(), e); + } + } else { + validationError.setText("Please insert a password!"); + validationError.setVisible(true); + } + } + + @FXML + private void chooseExportLocation(final MouseEvent mouseEvent) { + resetValidation(); + final DirectoryChooser chooser = new DirectoryChooser(); + chooser.setTitle(CHOOSER_TITLE); + final File file = chooser.showDialog(null); + if (file != null) { + destinationDirectory = file.getAbsolutePath(); + keystoreTextView.setText(destinationDirectory); + } + } + + @FXML + private void submitOnEnterPressed(final KeyEvent event) { + if (event.getCode().equals(KeyCode.ENTER)) { + saveKeystore(event); + } + } + + @FXML + private void clickPassword(final MouseEvent mouseEvent) { + if (!keystorePassword.isEditable()) { + validationError.setText(LOCKED_PASSWORD.getText()); + validationError.setVisible(true); + } else { + resetValidation(); + } + } + + @FXML + private void close(final InputEvent event) { + ((Node) event.getSource()).getScene().getWindow().hide(); + } + + private boolean isRemembered() { + return account.isImported() && Keystore.exist(account.getPublicAddress()); + } + + private void resetValidation() { + validationError.setVisible(false); + validationError.setText(""); + } +} diff --git a/src/main/java/org/aion/wallet/ui/components/partials/SyncStatusController.java b/src/main/java/org/aion/wallet/ui/components/partials/SyncStatusController.java index 36a87c61..bba20a04 100644 --- a/src/main/java/org/aion/wallet/ui/components/partials/SyncStatusController.java +++ b/src/main/java/org/aion/wallet/ui/components/partials/SyncStatusController.java @@ -3,40 +3,56 @@ import javafx.concurrent.Task; import javafx.fxml.FXML; import javafx.scene.control.Label; +import javafx.scene.control.Tooltip; +import javafx.scene.image.ImageView; import org.aion.wallet.connector.BlockchainConnector; import org.aion.wallet.connector.dto.SyncInfoDTO; import org.aion.wallet.ui.components.AbstractController; -import org.aion.wallet.ui.events.RefreshEvent; -import org.aion.wallet.util.SyncStatusFormatter; +import org.aion.wallet.events.RefreshEvent; +import org.aion.wallet.util.SyncStatusUtils; import java.net.URL; +import java.util.EnumSet; import java.util.ResourceBundle; public class SyncStatusController extends AbstractController { private final BlockchainConnector blockchainConnector = BlockchainConnector.getInstance(); + @FXML + private ImageView progressBarIcon; + @FXML private Label progressBarLabel; + private final Tooltip syncTooltip = new Tooltip(); + @Override protected void internalInit(URL location, ResourceBundle resources) { + syncTooltip.setText("Loading..."); + Tooltip.install(progressBarIcon, syncTooltip); + Tooltip.install(progressBarLabel, syncTooltip); } @Override protected final void refreshView(final RefreshEvent event) { - if (RefreshEvent.Type.TIMER.equals(event.getType())) { + if (EnumSet.of(RefreshEvent.Type.TIMER, RefreshEvent.Type.CONNECTED).contains(event.getType())) { final Task getSyncInfoTask = getApiTask(o -> blockchainConnector.getSyncInfo(), null); runApiTask( getSyncInfoTask, evt -> setSyncStatus(getSyncInfoTask.getValue()), - getErrorEvent(throwable -> {}, getSyncInfoTask), + getErrorEvent(t -> {}, getSyncInfoTask), getEmptyEvent() ); } } - private void setSyncStatus(SyncInfoDTO syncInfo) { - progressBarLabel.setText(SyncStatusFormatter.formatSyncStatusByBlockNumbers(syncInfo)); + private void setSyncStatus(final SyncInfoDTO syncInfo) { + progressBarLabel.setText(getSyncLabelText(syncInfo)); + syncTooltip.setText(syncInfo.getChainBestBlkNumber() + "/" + syncInfo.getNetworkBestBlkNumber() + " blocks"); + } + + private String getSyncLabelText(final SyncInfoDTO syncInfo) { + return SyncStatusUtils.formatSyncStatus(syncInfo); } } diff --git a/src/main/java/org/aion/wallet/ui/components/partials/TransactionResubmissionDialog.java b/src/main/java/org/aion/wallet/ui/components/partials/TransactionResubmissionDialog.java new file mode 100644 index 00000000..fe471740 --- /dev/null +++ b/src/main/java/org/aion/wallet/ui/components/partials/TransactionResubmissionDialog.java @@ -0,0 +1,141 @@ +package org.aion.wallet.ui.components.partials; + +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.fxml.Initializable; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.input.InputEvent; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Pane; +import javafx.scene.layout.VBox; +import javafx.stage.Popup; +import org.aion.api.log.LogEnum; +import org.aion.wallet.connector.BlockchainConnector; +import org.aion.wallet.connector.dto.SendTransactionDTO; +import org.aion.wallet.console.ConsoleManager; +import org.aion.wallet.dto.AccountDTO; +import org.aion.wallet.events.EventPublisher; +import org.aion.wallet.log.WalletLoggerFactory; +import org.aion.wallet.util.BalanceUtils; +import org.slf4j.Logger; + +import java.io.IOException; +import java.net.URL; +import java.util.Optional; +import java.util.ResourceBundle; + +public class TransactionResubmissionDialog implements Initializable { + + private static final Logger log = WalletLoggerFactory.getLogger(LogEnum.WLT.name()); + private final Popup popup = new Popup(); + private final BlockchainConnector blockchainConnector = BlockchainConnector.getInstance(); + + @FXML + private VBox transactions; + + public void open(final MouseEvent mouseEvent) { + popup.setAutoHide(true); + popup.setAutoFix(true); + + Pane resubmitTransactionDialog; + try { + resubmitTransactionDialog = FXMLLoader.load(getClass().getResource("TransactionResubmissionDialog.fxml")); + } catch (IOException e) { + log.error(e.getMessage(), e); + return; + } + + Node eventSource = (Node) mouseEvent.getSource(); + final double windowX = eventSource.getScene().getWindow().getX(); + final double windowY = eventSource.getScene().getWindow().getY(); + popup.setX(windowX + eventSource.getScene().getWidth() / 2 - resubmitTransactionDialog.getPrefWidth() / 2); + popup.setY(windowY + eventSource.getScene().getHeight() / 2 - resubmitTransactionDialog.getPrefHeight() / 2); + popup.getContent().addAll(resubmitTransactionDialog); + popup.show(eventSource.getScene().getWindow()); + } + + public void close(InputEvent eventSource) { + ((Node) eventSource.getSource()).getScene().getWindow().hide(); + } + + @Override + public void initialize(URL location, ResourceBundle resources) { + displayTransactions(); + } + + private void displayTransactions() { + addHeaderForTable(); + final Optional first = blockchainConnector.getAccounts().stream().filter(AccountDTO::isActive).findFirst(); + final String publicAddress; + if (first.isPresent()) { + publicAddress = first.get().getPublicAddress(); + for (SendTransactionDTO unsentTransaction : blockchainConnector.getAccountManager().getTimedOutTransactions(publicAddress)) { + HBox row = new HBox(); + row.setSpacing(10); + row.setAlignment(Pos.CENTER); + row.setPrefWidth(600); + + Label to = new Label(unsentTransaction.getTo()); + to.setPrefWidth(350); + to.getStyleClass().add("transaction-row-text"); + row.getChildren().add(to); + + Label value = new Label(BalanceUtils.formatBalance(unsentTransaction.getValue())); + value.setPrefWidth(100); + value.setPadding(new Insets(0.0, 0.0, 0.0, 10.0)); + value.getStyleClass().add("transaction-row-text"); + row.getChildren().add(value); + + Label nonce = new Label(unsentTransaction.getNonce().toString()); + nonce.setPrefWidth(50); + nonce.setPadding(new Insets(0.0, 0.0, 0.0, 5.0)); + nonce.getStyleClass().add("transaction-row-text"); + row.getChildren().add(nonce); + + Button resubmitTransaction = new Button(); + resubmitTransaction.setText("Resubmit"); + resubmitTransaction.setPrefWidth(100); + resubmitTransaction.getStyleClass().add("submit-button-small"); + resubmitTransaction.setOnMouseClicked(event -> { + close(event); + blockchainConnector.getAccountManager().removeTimedOutTransaction(unsentTransaction); + ConsoleManager.addLog("Transaction timeout treated", ConsoleManager.LogType.TRANSACTION); + EventPublisher.fireTransactionResubmited(unsentTransaction); + }); + row.getChildren().add(resubmitTransaction); + + transactions.getChildren().add(row); + } + } + } + + private void addHeaderForTable() { + HBox header = new HBox(); + header.setSpacing(10); + header.setPrefWidth(400); + header.setAlignment(Pos.CENTER); + header.getStyleClass().add("transaction-row"); + + Label to = new Label("To address"); + to.setPrefWidth(250); + to.getStyleClass().add("transaction-table-header-text"); + header.getChildren().add(to); + + Label value = new Label("Value"); + value.setPrefWidth(75); + value.getStyleClass().add("transaction-table-header-text"); + header.getChildren().add(value); + + Label nonce = new Label("Nonce"); + nonce.setPrefWidth(75); + nonce.getStyleClass().add("transaction-table-header-text"); + header.getChildren().add(nonce); + + transactions.getChildren().add(header); + } +} diff --git a/src/main/java/org/aion/wallet/ui/components/partials/UnlockAccountDialog.java b/src/main/java/org/aion/wallet/ui/components/partials/UnlockAccountDialog.java index 64f8b281..c295973c 100644 --- a/src/main/java/org/aion/wallet/ui/components/partials/UnlockAccountDialog.java +++ b/src/main/java/org/aion/wallet/ui/components/partials/UnlockAccountDialog.java @@ -1,11 +1,11 @@ package org.aion.wallet.ui.components.partials; import com.google.common.eventbus.Subscribe; +import javafx.application.Platform; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.fxml.Initializable; import javafx.scene.Node; -import javafx.scene.Scene; import javafx.scene.control.Label; import javafx.scene.control.PasswordField; import javafx.scene.input.InputEvent; @@ -13,41 +13,40 @@ import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseEvent; import javafx.scene.layout.Pane; -import javafx.scene.layout.StackPane; -import javafx.scene.paint.Color; -import javafx.stage.Modality; import javafx.stage.Popup; -import javafx.stage.Stage; -import javafx.stage.StageStyle; -import org.aion.api.log.AionLoggerFactory; import org.aion.api.log.LogEnum; -import org.aion.crypto.ECKey; -import org.aion.mcf.account.Keystore; +import org.aion.wallet.connector.BlockchainConnector; +import org.aion.wallet.console.ConsoleManager; import org.aion.wallet.dto.AccountDTO; -import org.aion.wallet.ui.events.EventBusFactory; -import org.aion.wallet.ui.events.EventPublisher; -import org.aion.wallet.util.DataUpdater; +import org.aion.wallet.events.AccountEvent; +import org.aion.wallet.events.EventBusFactory; +import org.aion.wallet.exception.ValidationException; +import org.aion.wallet.log.WalletLoggerFactory; import org.slf4j.Logger; import java.io.IOException; import java.net.URL; -import java.util.Map; import java.util.ResourceBundle; -public class UnlockAccountDialog implements Initializable{ - private static final Logger log = AionLoggerFactory.getLogger(LogEnum.WLT.name()); +public class UnlockAccountDialog implements Initializable { + private static final Logger log = WalletLoggerFactory.getLogger(LogEnum.WLT.name()); + + private final Popup popup = new Popup(); + private final BlockchainConnector blockchainConnector = BlockchainConnector.getInstance(); @FXML private PasswordField unlockPassword; - @FXML private Label validationError; - private AccountDTO account; - private final Popup popup = new Popup(); + @Override + public void initialize(final URL location, final ResourceBundle resources) { + registerEventBusConsumer(); + Platform.runLater(() -> unlockPassword.requestFocus()); + } - public void open(MouseEvent mouseEvent) { + public void open(final MouseEvent mouseEvent) { popup.setAutoHide(true); popup.setAutoFix(true); @@ -68,41 +67,37 @@ public void open(MouseEvent mouseEvent) { popup.show(eventSource.getScene().getWindow()); } - public void unlockAccount(InputEvent event) { - if(unlockPassword.getText() != null && !unlockPassword.getText().isEmpty()) { - ECKey storedKey = Keystore.getKey(account.getPublicAddress(), unlockPassword.getText()); - if(storedKey != null) { - account.setPrivateKey(storedKey.getPrivKeyBytes()); - EventPublisher.fireAccountChanged(account); - this.close(event); - } - else { - validationError.setText("The password is incorrect!"); + private void close(final InputEvent event) { + ((Node) event.getSource()).getScene().getWindow().hide(); + } + + public void unlockAccount(final InputEvent event) { + final String password = unlockPassword.getText(); + if (password != null && !password.isEmpty()) { + try { + blockchainConnector.unlockAccount(account, password); + ConsoleManager.addLog("Account" + account.getPublicAddress() + " unlocked", ConsoleManager.LogType.ACCOUNT); + close(event); + } catch (ValidationException e) { + ConsoleManager.addLog("Account" + account.getPublicAddress() + " could not be unlocked", ConsoleManager.LogType.ACCOUNT, ConsoleManager.LogLevel.WARNING); + validationError.setText(e.getMessage()); validationError.setVisible(true); } - } - else { + } else { validationError.setText("Please insert a password!"); validationError.setVisible(true); } } - @Override - public void initialize(URL location, ResourceBundle resources) { - registerEventBusConsumer(); - } - - public void resetValidation(MouseEvent mouseEvent) { + public void resetValidation() { validationError.setVisible(false); } - public void close(InputEvent event) { - ((Node) event.getSource()).getScene().getWindow().hide(); - } - @Subscribe - private void handleUnlockStarted(AccountDTO account) { - this.account = account; + private void handleUnlockStarted(final AccountEvent event) { + if (AccountEvent.Type.UNLOCKED.equals(event.getType())) { + this.account = event.getPayload(); + } } @FXML @@ -111,7 +106,8 @@ private void submitOnEnterPressed(final KeyEvent event) { unlockAccount(event); } } + private void registerEventBusConsumer() { - EventBusFactory.getBus(EventPublisher.ACCOUNT_UNLOCK_EVENT_ID).register(this); + EventBusFactory.getBus(AccountEvent.ID).register(this); } } diff --git a/src/main/java/org/aion/wallet/ui/components/partials/UnlockMasterAccountDialog.java b/src/main/java/org/aion/wallet/ui/components/partials/UnlockMasterAccountDialog.java new file mode 100644 index 00000000..bba361dd --- /dev/null +++ b/src/main/java/org/aion/wallet/ui/components/partials/UnlockMasterAccountDialog.java @@ -0,0 +1,93 @@ +package org.aion.wallet.ui.components.partials; + +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.fxml.Initializable; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.input.InputEvent; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.Pane; +import javafx.stage.Popup; +import org.aion.api.log.LogEnum; +import org.aion.wallet.connector.BlockchainConnector; +import org.aion.wallet.console.ConsoleManager; +import org.aion.wallet.log.WalletLoggerFactory; +import org.slf4j.Logger; + +import java.io.IOException; +import java.net.URL; +import java.util.ResourceBundle; + +public class UnlockMasterAccountDialog implements Initializable { + + private static final Logger log = WalletLoggerFactory.getLogger(LogEnum.WLT.name()); + private final Popup popup = new Popup(); + private final BlockchainConnector blockchainConnector = BlockchainConnector.getInstance(); + @FXML + private PasswordField passwordField; + @FXML + private Label validationError; + + @Override + public void initialize(final URL location, final ResourceBundle resources) { + Platform.runLater(() -> passwordField.requestFocus()); + } + + public void open(MouseEvent mouseEvent) { + popup.setAutoHide(true); + popup.setAutoFix(true); + + Pane addAccountDialog; + try { + addAccountDialog = FXMLLoader.load(getClass().getResource("UnlockMasterAccountDialog.fxml")); + } catch (IOException e) { + log.error(e.getMessage(), e); + return; + } + + Node eventSource = (Node) mouseEvent.getSource(); + final double windowX = eventSource.getScene().getWindow().getX(); + final double windowY = eventSource.getScene().getWindow().getY(); + popup.setX(windowX + eventSource.getScene().getWidth() / 2 - addAccountDialog.getPrefWidth() / 2); + popup.setY(windowY + eventSource.getScene().getHeight() / 2 - addAccountDialog.getPrefHeight() / 2); + popup.getContent().addAll(addAccountDialog); + popup.show(eventSource.getScene().getWindow()); + } + + public void close(InputEvent eventSource) { + ((Node) eventSource.getSource()).getScene().getWindow().hide(); + } + + @FXML + private void submitOnEnterPressed(final KeyEvent event) { + if (event.getCode().equals(KeyCode.ENTER)) { + unlockMasterAccount(event); + } + } + + @FXML + private void unlockMasterAccount(final InputEvent mouseEvent) { + try { + blockchainConnector.unlockMasterAccount(passwordField.getText()); + ConsoleManager.addLog("Master account unlocked", ConsoleManager.LogType.ACCOUNT); + close(mouseEvent); + } catch (Exception e) { + ConsoleManager.addLog("Could not unlock master account", ConsoleManager.LogType.ACCOUNT, ConsoleManager.LogLevel.WARNING); + showInvalidFieldsError(e.getMessage()); + } + } + + public void resetValidation() { + validationError.setVisible(false); + } + + private void showInvalidFieldsError(String message) { + validationError.setVisible(true); + validationError.setText(message); + } +} diff --git a/src/main/java/org/aion/wallet/ui/components/partials/WindowControls.java b/src/main/java/org/aion/wallet/ui/components/partials/WindowControls.java index 1746eaf6..e84ed483 100644 --- a/src/main/java/org/aion/wallet/ui/components/partials/WindowControls.java +++ b/src/main/java/org/aion/wallet/ui/components/partials/WindowControls.java @@ -4,8 +4,8 @@ import javafx.fxml.FXML; import javafx.scene.Node; import javafx.scene.input.MouseEvent; -import org.aion.wallet.ui.events.EventBusFactory; -import org.aion.wallet.ui.events.WindowControlsEvent; +import org.aion.wallet.events.EventBusFactory; +import org.aion.wallet.events.WindowControlsEvent; public class WindowControls { diff --git a/src/main/java/org/aion/wallet/ui/events/EventPublisher.java b/src/main/java/org/aion/wallet/ui/events/EventPublisher.java deleted file mode 100644 index 3cca162b..00000000 --- a/src/main/java/org/aion/wallet/ui/events/EventPublisher.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.aion.wallet.ui.events; - -import org.aion.wallet.dto.AccountDTO; -import org.aion.wallet.dto.LightAppSettings; -import org.aion.wallet.util.DataUpdater; - -public class EventPublisher { - public static final String ACCOUNT_CHANGE_EVENT_ID = "account.changed"; - public static final String ACCOUNT_UNLOCK_EVENT_ID = "account.unlock"; - public static final String SETTINGS_CHANGED_ID = "settings.changed"; - - public static void fireAccountChanged(final AccountDTO account) { - if (account != null) { - EventBusFactory.getBus(ACCOUNT_CHANGE_EVENT_ID).post(account); - } - } - - public static void fireUnlockAccount(final AccountDTO account) { - if (account != null) { - EventBusFactory.getBus(ACCOUNT_UNLOCK_EVENT_ID).post(account); - } - } - - public static void fireOperationFinished(){ - EventBusFactory.getBus(DataUpdater.UI_DATA_REFRESH).post(new RefreshEvent(RefreshEvent.Type.OPERATION_FINISHED)); - } - - public static void fireApplicationSettingsChanged(final LightAppSettings settings){ - EventBusFactory.getBus(SETTINGS_CHANGED_ID).post(settings); - } -} diff --git a/src/main/java/org/aion/wallet/ui/events/RefreshEvent.java b/src/main/java/org/aion/wallet/ui/events/RefreshEvent.java deleted file mode 100644 index f8612ef5..00000000 --- a/src/main/java/org/aion/wallet/ui/events/RefreshEvent.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.aion.wallet.ui.events; - -public class RefreshEvent extends AbstractUIEvent{ - - public RefreshEvent(Enum eventType) { - super(eventType); - } - - public enum Type { - TIMER, OPERATION_FINISHED - } -} diff --git a/src/main/java/org/aion/wallet/util/AddressUtils.java b/src/main/java/org/aion/wallet/util/AddressUtils.java index faa6202a..750fe110 100644 --- a/src/main/java/org/aion/wallet/util/AddressUtils.java +++ b/src/main/java/org/aion/wallet/util/AddressUtils.java @@ -3,11 +3,19 @@ import org.aion.base.util.TypeConverter; public class AddressUtils { - public static boolean isValid(String address) { - return address != null && !address.equalsIgnoreCase(""); + + public static boolean isValid(final String address) { + return address != null && !address.isEmpty() && isAionAddress(address); } - public static boolean equals(String addrOne, String addrTwo) { + public static boolean equals(final String addrOne, final String addrTwo) { return TypeConverter.toJsonHex(addrOne).equals(TypeConverter.toJsonHex(addrTwo)); } + + private static boolean isAionAddress(final String address) { + final boolean isFull = address.startsWith("0xa") && address.length() == 66; + final boolean isStripped = address.startsWith("a") && address.length() == 64; + final String strippedAddress = isFull ? address.substring(2) : (isStripped ? address : ""); + return strippedAddress.matches("[0-9a-fA-F]+"); + } } diff --git a/src/main/java/org/aion/wallet/util/AionConstants.java b/src/main/java/org/aion/wallet/util/AionConstants.java index e522d065..28f03ea9 100644 --- a/src/main/java/org/aion/wallet/util/AionConstants.java +++ b/src/main/java/org/aion/wallet/util/AionConstants.java @@ -6,6 +6,8 @@ public class AionConstants { private AionConstants() {} + public static final String AION_URL = "http://mainnet.aion.network"; + private static final long AMP = (long) 1E9; public final static String CCY = "AION"; @@ -18,17 +20,5 @@ private AionConstants() {} public static final Long BLOCK_MINING_TIME_MILLIS = BLOCK_MINING_TIME_SECONDS * 1000L; - public static final Integer MAX_BLOCKS_FOR_LATEST_TRANSACTIONS_QUERY = 100000; - - // todo: will we be able to access this from AccountManager? - - public static final Integer DEFAULT_WALLET_UNLOCK_DURATION = 1000; - - public static final String EUR_CCY = "EUR"; - - public static final String USD_CCY = "USD"; - - public static final double AION_TO_EUR = 2.46; - - public static final double AION_TO_USD = 3.05; + public static final int VALIDATION_BLOCKS_FOR_TRANSACTIONS = 50; } diff --git a/src/main/java/org/aion/wallet/util/BalanceUtils.java b/src/main/java/org/aion/wallet/util/BalanceUtils.java index 6e26752c..31be6820 100644 --- a/src/main/java/org/aion/wallet/util/BalanceUtils.java +++ b/src/main/java/org/aion/wallet/util/BalanceUtils.java @@ -26,9 +26,4 @@ public static String formatBalance(final BigInteger balance) { public static BigInteger extractBalance(final String formattedBalance) { return new BigDecimal(formattedBalance).multiply(WEI_MULTIPLIER).toBigInteger(); } - - //TODO will be done in future story - public static String convertBalance() { - return null; - } } diff --git a/src/main/java/org/aion/wallet/util/CryptoUtils.java b/src/main/java/org/aion/wallet/util/CryptoUtils.java new file mode 100644 index 00000000..2b2ffb9f --- /dev/null +++ b/src/main/java/org/aion/wallet/util/CryptoUtils.java @@ -0,0 +1,55 @@ +package org.aion.wallet.util; + +import io.github.novacrypto.bip39.SeedCalculator; +import org.aion.crypto.ECKey; +import org.aion.crypto.ed25519.ECKeyEd25519; +import org.aion.wallet.exception.ValidationException; + +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +public final class CryptoUtils { + + private static final byte[] ED25519_KEY = "ed25519 seed".getBytes(); + private static final String HMAC_SHA512_ALGORITHM = "HmacSHA512"; + private static final String DEFAULT_MNEMONIC_PASSPHRASE = ""; + private static final int HARDENED_KEY_MULTIPLIER = 0x80000000; + private static final ECKeyEd25519 EC_KEY_FACTORY = new ECKeyEd25519(); + + private CryptoUtils() {} + + public static ECKey getBip39ECKey(final String mnemonic) throws ValidationException { + final byte[] seed = new SeedCalculator().calculateSeed(mnemonic, DEFAULT_MNEMONIC_PASSPHRASE); + return getECKey(getSha512(ED25519_KEY, seed)); + + } + + public static byte[] getSha512(final byte[] secret, final byte[] hashData) throws ValidationException { + final byte[] bytes; + try { + final Mac mac = Mac.getInstance(HMAC_SHA512_ALGORITHM); + final SecretKey key = new SecretKeySpec(secret, HMAC_SHA512_ALGORITHM); + mac.init(key); + bytes = mac.doFinal(hashData); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new ValidationException(e); + } + return bytes; + } + + public static byte[] getHardenedNumber(final int number) { + final ByteBuffer byteBuffer = ByteBuffer.allocate(4); + byteBuffer.order(ByteOrder.BIG_ENDIAN); + byteBuffer.putInt(number | HARDENED_KEY_MULTIPLIER); + return byteBuffer.array(); + } + + public static ECKey getECKey(final byte[] privateKey) { + return EC_KEY_FACTORY.fromPrivate(privateKey); + } +} diff --git a/src/main/java/org/aion/wallet/util/DataUpdater.java b/src/main/java/org/aion/wallet/util/DataUpdater.java index ed348b30..4d05c373 100644 --- a/src/main/java/org/aion/wallet/util/DataUpdater.java +++ b/src/main/java/org/aion/wallet/util/DataUpdater.java @@ -2,16 +2,14 @@ import com.google.common.eventbus.EventBus; import javafx.application.Platform; -import org.aion.wallet.ui.events.EventBusFactory; -import org.aion.wallet.ui.events.RefreshEvent; +import org.aion.wallet.events.EventBusFactory; +import org.aion.wallet.events.RefreshEvent; import java.util.TimerTask; public class DataUpdater extends TimerTask { - public static final String UI_DATA_REFRESH = "ui.data_refresh"; - - private final EventBus eventBus = EventBusFactory.getBus(UI_DATA_REFRESH); + private final EventBus eventBus = EventBusFactory.getBus(RefreshEvent.ID); @Override public void run() { diff --git a/src/main/java/org/aion/wallet/util/QRCodeUtils.java b/src/main/java/org/aion/wallet/util/QRCodeUtils.java new file mode 100644 index 00000000..5a32f6dc --- /dev/null +++ b/src/main/java/org/aion/wallet/util/QRCodeUtils.java @@ -0,0 +1,63 @@ +package org.aion.wallet.util; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.qrcode.QRCodeWriter; +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; + +import java.awt.image.BufferedImage; +import java.util.HashMap; + +/** + * @author Sergiu-Paul Falcusan + * -cheers + */ +public class QRCodeUtils { + private static final int MIN_QRCODE_WIDTH = 150; + private static final int MIN_QRCODE_HEIGHT = 150; + private static final int WHITE = 255 << 16 | 255 << 8 | 255; + private static final int BLACK = 0; + + /** + * Encode a string into a QR Code (Default size Width 150px and Height 150px) + * @param content string to be converted + * @return Returns a BufferedImage that can be used further for showing it + */ + public static BufferedImage writeQRCode(final String content) { + return writeQRCode(content, MIN_QRCODE_WIDTH, MIN_QRCODE_HEIGHT); + } + + /** + * Encode a string into a QR Code + * @param content string to be converted + * @param width Width of QR image + * @param height Height of QR image + * @return Returns a BufferedImage that can be used further for showing it + */ + public static BufferedImage writeQRCode(final String content, final int width, final int height) { + try { + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + + HashMap hintMap = new HashMap<>(); + hintMap.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.Q); + hintMap.put(EncodeHintType.MARGIN, 0); + + QRCodeWriter writer = new QRCodeWriter(); + BitMatrix bitMatrix = writer.encode(content, BarcodeFormat.QR_CODE, width, height, hintMap); + + for (int i = 0; i < width; i++) { + for (int j = 0; j < height; j++) { + image.setRGB(i, j, bitMatrix.get(i, j) ? BLACK : WHITE); + } + } + + return image; + } catch (WriterException e) { + e.printStackTrace(); + } + + return null; + } +} diff --git a/src/main/java/org/aion/wallet/util/SyncStatusFormatter.java b/src/main/java/org/aion/wallet/util/SyncStatusUtils.java similarity index 79% rename from src/main/java/org/aion/wallet/util/SyncStatusFormatter.java rename to src/main/java/org/aion/wallet/util/SyncStatusUtils.java index 3a400bcd..377c89c6 100644 --- a/src/main/java/org/aion/wallet/util/SyncStatusFormatter.java +++ b/src/main/java/org/aion/wallet/util/SyncStatusUtils.java @@ -2,8 +2,7 @@ import org.aion.wallet.connector.dto.SyncInfoDTO; -public class SyncStatusFormatter { - +public class SyncStatusUtils { private static final int SECONDS_IN_A_MINUTE = 60; private static final int SECONDS_IN_A_HOUR = 3600; private static final int SECONDS_IN_A_DAY = 86400; @@ -13,11 +12,10 @@ public class SyncStatusFormatter { private static final int MINUTES_IN_AN_HOUR = 60; private static final int SYNC_STATUS_DISPLAY_UNIT_LIMIT = 2; - public static String formatSyncStatus(SyncInfoDTO syncInfo) { + public static String formatSyncStatus(final SyncInfoDTO syncInfo) { if(syncInfo != null) { if(syncInfo.getNetworkBestBlkNumber() > 0) { - long seconds = (syncInfo.getNetworkBestBlkNumber() - syncInfo.getChainBestBlkNumber()) - * AionConstants.BLOCK_MINING_TIME_SECONDS; + long seconds = (syncInfo.getNetworkBestBlkNumber() - syncInfo.getChainBestBlkNumber()) * AionConstants.BLOCK_MINING_TIME_SECONDS; if((int) seconds < SECONDS_IN_A_MINUTE) { return UP_TO_DATE; } @@ -28,11 +26,7 @@ public static String formatSyncStatus(SyncInfoDTO syncInfo) { return UNDEFINED; } - public static String formatSyncStatusByBlockNumbers(SyncInfoDTO syncInfo) { - return syncInfo.getChainBestBlkNumber() + "/" + syncInfo.getNetworkBestBlkNumber() + " total blocks"; - } - - private static String getSyncStatusBySeconds(long seconds) { + private static String getSyncStatusBySeconds(final long seconds) { int minutes = (int) seconds / SECONDS_IN_A_MINUTE; int hours = (int) seconds / SECONDS_IN_A_HOUR; int days = (int) seconds / SECONDS_IN_A_DAY; diff --git a/src/main/java/org/aion/wallet/util/URLManager.java b/src/main/java/org/aion/wallet/util/URLManager.java new file mode 100644 index 00000000..8df00afb --- /dev/null +++ b/src/main/java/org/aion/wallet/util/URLManager.java @@ -0,0 +1,47 @@ +package org.aion.wallet.util; + +import org.aion.api.log.LogEnum; +import org.aion.wallet.log.WalletLoggerFactory; +import org.slf4j.Logger; + +import java.awt.*; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; + +public class URLManager { + + private static final Logger log = WalletLoggerFactory.getLogger(LogEnum.WLT.name()); + + private static final String TRANSACTION_URL = "/#/transaction/"; + + public static void openDashboard() { + openURL(AionConstants.AION_URL); + } + + public static void openTransaction(final String transactionHash) { + if (transactionHash != null && !transactionHash.isEmpty()) { + openURL(AionConstants.AION_URL + TRANSACTION_URL + transactionHash); + } + } + + private static void openURL(final String URL) { + if (URL != null) { + final String os = System.getProperty("os.name").toLowerCase(); + if (os.contains("win")) { + try { + Desktop.getDesktop().browse(new URI(URL)); + } catch (IOException | URISyntaxException e) { + log.error("Exception occurred trying to open website: %s", e.getMessage(), e); + } + } else if (os.contains("nix") || os.contains("nux") || os.indexOf("aix") > 0) + try { + if (Runtime.getRuntime().exec(new String[]{"which", "xdg-open"}).getInputStream().read() != -1) { + Runtime.getRuntime().exec(new String[]{"xdg-open", URL}); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} diff --git a/src/main/resources/log4j.properties b/src/main/resources/log4j.properties index da63ad24..9ba3a466 100644 --- a/src/main/resources/log4j.properties +++ b/src/main/resources/log4j.properties @@ -1,8 +1,6 @@ # Root logger option log4j.rootLogger=INFO, stdout - - # Direct log messages to stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.Target=System.out diff --git a/src/main/resources/org/aion/wallet/ui/MainWindow.fxml b/src/main/resources/org/aion/wallet/ui/MainWindow.fxml index a4ba2b9d..23e10677 100644 --- a/src/main/resources/org/aion/wallet/ui/MainWindow.fxml +++ b/src/main/resources/org/aion/wallet/ui/MainWindow.fxml @@ -4,7 +4,7 @@ + prefHeight="570.0" prefWidth="860.0" styleClass="main-stage"> diff --git a/src/main/resources/org/aion/wallet/ui/components/FooterPane.fxml b/src/main/resources/org/aion/wallet/ui/components/FooterPane.fxml index d7326db7..3ea50884 100644 --- a/src/main/resources/org/aion/wallet/ui/components/FooterPane.fxml +++ b/src/main/resources/org/aion/wallet/ui/components/FooterPane.fxml @@ -1,15 +1,15 @@ - + AnchorPane.bottomAnchor="0" AnchorPane.leftAnchor="0" AnchorPane.rightAnchor="0.0"> + - diff --git a/src/main/resources/org/aion/wallet/ui/components/HeaderPane.fxml b/src/main/resources/org/aion/wallet/ui/components/HeaderPane.fxml index 53c3b1a0..47c0546b 100644 --- a/src/main/resources/org/aion/wallet/ui/components/HeaderPane.fxml +++ b/src/main/resources/org/aion/wallet/ui/components/HeaderPane.fxml @@ -18,7 +18,7 @@ diff --git a/src/main/resources/org/aion/wallet/ui/components/account/AccountListViewItem.fxml b/src/main/resources/org/aion/wallet/ui/components/account/AccountListViewItem.fxml index c6887632..0c5f14cb 100644 --- a/src/main/resources/org/aion/wallet/ui/components/account/AccountListViewItem.fxml +++ b/src/main/resources/org/aion/wallet/ui/components/account/AccountListViewItem.fxml @@ -3,19 +3,22 @@ - - + - + - + + + + @@ -35,6 +38,14 @@ + + + + + diff --git a/src/main/resources/org/aion/wallet/ui/components/buttons/HomeButton.fxml b/src/main/resources/org/aion/wallet/ui/components/buttons/HomeButton.fxml index 58198650..a216f02c 100644 --- a/src/main/resources/org/aion/wallet/ui/components/buttons/HomeButton.fxml +++ b/src/main/resources/org/aion/wallet/ui/components/buttons/HomeButton.fxml @@ -3,8 +3,5 @@ - - - diff --git a/src/main/resources/org/aion/wallet/ui/components/buttons/SettingsButton.fxml b/src/main/resources/org/aion/wallet/ui/components/buttons/SettingsButton.fxml index be22674c..cc7b91f1 100644 --- a/src/main/resources/org/aion/wallet/ui/components/buttons/SettingsButton.fxml +++ b/src/main/resources/org/aion/wallet/ui/components/buttons/SettingsButton.fxml @@ -3,8 +3,5 @@ - - - diff --git a/src/main/resources/org/aion/wallet/ui/components/contentPane.css b/src/main/resources/org/aion/wallet/ui/components/contentPane.css index 880b1b09..bcd268df 100644 --- a/src/main/resources/org/aion/wallet/ui/components/contentPane.css +++ b/src/main/resources/org/aion/wallet/ui/components/contentPane.css @@ -1,6 +1,6 @@ #contentPane { -fx-background-color: WHITE; - -fx-border-color:#40bded; + -fx-border-color:#4221CC; -fx-border-width: 1 0 1 0; } @@ -31,7 +31,7 @@ } .button { - -fx-background-color: #40bded; + -fx-background-color: #4221cc; -fx-background-radius: 0,0,0,0; -fx-text-fill: white; -fx-font-family: 'Open Sans Bold'; @@ -42,7 +42,7 @@ -fx-background-color: #ececec; } -.accounts-list{ +.accounts-list { -fx-background-insets: 0; -fx-padding: 0; } @@ -55,7 +55,7 @@ } .line{ - -fx-stroke: #40bded; + -fx-stroke: #4221CC; } .header-text { @@ -76,7 +76,7 @@ -fx-font-family: 'Open Sans Regular'; -fx-font-size: 16px; -fx-display-caret: false; - -fx-border-color: #40bded; + -fx-border-color: #4221CC; -fx-border-width: 0 0 1 0; } @@ -86,3 +86,115 @@ -fx-text-fill: BLACK; -fx-display-caret: false; } + +.link-style { + -fx-text-fill: blue; + -fx-underline: true ; + -fx-cursor: hand; +} + +.error-label { + -fx-text-fill: RED; +} + +.warning-link-style { + -fx-text-fill: #FF9900; + -fx-underline: true ; + -fx-cursor: hand; +} + +/* Scrollbar */ + +/* Scroll bar policy below */ + +.custom-scrollbar .scroll-bar:horizontal , +.custom-scrollbar .scroll-bar:vertical{ + -fx-background-color: transparent; +} + +/* ------------------------------------------------------------------------------------- */ +/** EVENT CSS **/ +/* ------------------------------------------------------------------------------------- */ + +/* The main scrollbar **track** CSS class on event of "hover" and "pressed" */ +.custom-scrollbar .scroll-bar:vertical:hover .track, +.custom-scrollbar .scroll-bar:vertical:pressed .track { + -fx-background-color: derive(#434343, 20%); + -fx-opacity: 0.2; + -fx-background-radius: 0em; +} + +/* The main scrollbar **thumb** CSS class on event of "hover" and "pressed" */ +.custom-scrollbar .scroll-bar .thumb:hover, +.custom-scrollbar .scroll-bar .thumb:pressed { + -fx-background-color: derive(black, 50%); +} + +.custom-scrollbar .increment-button:hover ,.custom-scrollbar .decrement-button:hover { + -fx-background-color:derive(gray,100%); + -fx-border-color:derive(gray, 80%); + -fx-padding:10px; +} + +/* The increment and decrement button CSS class of scrollbar */ +.custom-scrollbar .increment-button ,.custom-scrollbar .decrement-button { + -fx-background-color:transparent; + -fx-background-radius: 2em; +} + +/* The main scrollbar **thumb** CSS class which we drag every time (movable) */ +.custom-scrollbar .scroll-bar:horizontal .thumb, +.custom-scrollbar .scroll-bar:vertical .thumb { + -fx-background-color:derive(black, 90%); + -fx-background-insets: 2, 0, 0; + -fx-background-radius: 2em; +} + +/* The main scrollbar **track** CSS class */ +.custom-scrollbar .scroll-bar:horizontal .track, +.custom-scrollbar .scroll-bar:vertical .track { + -fx-background-color:transparent; + -fx-border-color:transparent; + -fx-background-radius: 0em; + -fx-border-radius:2em; +} + +/* The increment and decrement button CSS class of scrollbar */ +.custom-scrollbar .scroll-bar:horizontal .increment-button , +.custom-scrollbar .scroll-bar:horizontal .decrement-button { + -fx-background-color:transparent; + -fx-background-radius: 0em; + -fx-padding:0 0 10 0; +} + +/* The increment and decrement button CSS class of scrollbar */ +.custom-scrollbar .scroll-bar:vertical .increment-button , +.custom-scrollbar .scroll-bar:vertical .decrement-button { + -fx-background-color:transparent; + -fx-background-radius: 0em; + -fx-padding:0 10 0 0; +} + +.custom-scrollbar .scroll-bar .increment-arrow, +.custom-scrollbar .scroll-bar .decrement-arrow { + -fx-padding:0; +} + +/* Hide horizontal scroll-bar */ +.table-view *.scroll-bar:horizontal *.increment-button, +.table-view *.scroll-bar:horizontal *.decrement-button { + -fx-background-color: null; + -fx-background-radius: 0; + -fx-background-insets: 0; + -fx-padding: 0; +} + +.table-view *.scroll-bar:horizontal *.increment-arrow, +.table-view *.scroll-bar:horizontal *.decrement-arrow { + -fx-background-color: null; + -fx-background-radius: 0; + -fx-background-insets: 0; + -fx-padding: 0; + -fx-shape: null; +} + diff --git a/src/main/resources/org/aion/wallet/ui/components/footer-pane.css b/src/main/resources/org/aion/wallet/ui/components/footer-pane.css index 09177a67..7687761a 100644 --- a/src/main/resources/org/aion/wallet/ui/components/footer-pane.css +++ b/src/main/resources/org/aion/wallet/ui/components/footer-pane.css @@ -7,10 +7,10 @@ } .connected-label { - -fx-background-color: #12354b; + -fx-background-color: #4221CC; -fx-background-radius: 20 20 20 20; -fx-padding: 5 15 5 15; - -fx-text-fill: #40bded; + -fx-text-fill: #5AF0BD; -fx-font-family: 'Open Sans Regular'; -fx-font-size: 14px; -fx-text-alignment: CENTER; @@ -27,3 +27,10 @@ -fx-font-family: 'Open Sans Regular'; -fx-font-size: 14px; } + +.version-style { + -fx-text-fill: black; + -fx-font-family: 'Open Sans Regular'; + -fx-font-size: 14px; + -fx-cursor: hand; +} diff --git a/src/main/resources/org/aion/wallet/ui/components/header.css b/src/main/resources/org/aion/wallet/ui/components/header.css index a9d63b73..d66b4a9b 100644 --- a/src/main/resources/org/aion/wallet/ui/components/header.css +++ b/src/main/resources/org/aion/wallet/ui/components/header.css @@ -2,13 +2,13 @@ -fx-background-color: white; } -.pressed{ - -fx-border-color: #40bded; +.header-button-pressed{ + -fx-border-color: #4221CC; -fx-border-width: 0 0 5 0; } .header-button-label-pressed { - -fx-text-fill: #40bded; + -fx-text-fill: #4221CC; -fx-text-alignment: CENTER; -fx-font-family: 'Open Sans Regular'; -fx-font-size: 18px; @@ -60,6 +60,6 @@ -fx-font-family: 'Open Sans Regular'; -fx-font-size: 18px; -fx-display-caret: false; - -fx-border-color: #40bded; + -fx-border-color: #4221CC; -fx-border-width: 0 0 1 0; } \ No newline at end of file diff --git a/src/main/resources/org/aion/wallet/ui/components/icons/aion-icon.png b/src/main/resources/org/aion/wallet/ui/components/icons/aion-icon.png new file mode 100644 index 00000000..ba615439 Binary files /dev/null and b/src/main/resources/org/aion/wallet/ui/components/icons/aion-icon.png differ diff --git a/src/main/resources/org/aion/wallet/ui/components/icons/aion_logo.png b/src/main/resources/org/aion/wallet/ui/components/icons/aion_logo.png index baba6865..1d461c8b 100644 Binary files a/src/main/resources/org/aion/wallet/ui/components/icons/aion_logo.png and b/src/main/resources/org/aion/wallet/ui/components/icons/aion_logo.png differ diff --git a/src/main/resources/org/aion/wallet/ui/components/icons/aion_logo_light.png b/src/main/resources/org/aion/wallet/ui/components/icons/aion_logo_light.png index 28106a74..1d461c8b 100644 Binary files a/src/main/resources/org/aion/wallet/ui/components/icons/aion_logo_light.png and b/src/main/resources/org/aion/wallet/ui/components/icons/aion_logo_light.png differ diff --git a/src/main/resources/org/aion/wallet/ui/components/icons/centrys-logo.png b/src/main/resources/org/aion/wallet/ui/components/icons/centrys-logo.png new file mode 100644 index 00000000..1428e5c1 Binary files /dev/null and b/src/main/resources/org/aion/wallet/ui/components/icons/centrys-logo.png differ diff --git a/src/main/resources/org/aion/wallet/ui/components/icons/icon-connected-50.png b/src/main/resources/org/aion/wallet/ui/components/icons/icon-connected-50.png index dc7f6204..7da9b53f 100644 Binary files a/src/main/resources/org/aion/wallet/ui/components/icons/icon-connected-50.png and b/src/main/resources/org/aion/wallet/ui/components/icons/icon-connected-50.png differ diff --git a/src/main/resources/org/aion/wallet/ui/components/icons/icon-disconnected-50.png b/src/main/resources/org/aion/wallet/ui/components/icons/icon-disconnected-50.png index fe87dee1..db281072 100644 Binary files a/src/main/resources/org/aion/wallet/ui/components/icons/icon-disconnected-50.png and b/src/main/resources/org/aion/wallet/ui/components/icons/icon-disconnected-50.png differ diff --git a/src/main/resources/org/aion/wallet/ui/components/icons/icon-export.png b/src/main/resources/org/aion/wallet/ui/components/icons/icon-export.png new file mode 100644 index 00000000..161d77de Binary files /dev/null and b/src/main/resources/org/aion/wallet/ui/components/icons/icon-export.png differ diff --git a/src/main/resources/org/aion/wallet/ui/components/icons/imported.png b/src/main/resources/org/aion/wallet/ui/components/icons/imported.png new file mode 100644 index 00000000..819af820 Binary files /dev/null and b/src/main/resources/org/aion/wallet/ui/components/icons/imported.png differ diff --git a/src/main/resources/org/aion/wallet/ui/components/icons/last-block-256x256.png b/src/main/resources/org/aion/wallet/ui/components/icons/last-block-256x256.png index 9897e690..76bedc24 100644 Binary files a/src/main/resources/org/aion/wallet/ui/components/icons/last-block-256x256.png and b/src/main/resources/org/aion/wallet/ui/components/icons/last-block-256x256.png differ diff --git a/src/main/resources/org/aion/wallet/ui/components/icons/minimize-50.png b/src/main/resources/org/aion/wallet/ui/components/icons/minimize-50.png index b745f90a..fe56b3fd 100644 Binary files a/src/main/resources/org/aion/wallet/ui/components/icons/minimize-50.png and b/src/main/resources/org/aion/wallet/ui/components/icons/minimize-50.png differ diff --git a/src/main/resources/org/aion/wallet/ui/components/icons/peers-256x256.png b/src/main/resources/org/aion/wallet/ui/components/icons/peers-256x256.png index 9e4ec1f8..1d6732fd 100644 Binary files a/src/main/resources/org/aion/wallet/ui/components/icons/peers-256x256.png and b/src/main/resources/org/aion/wallet/ui/components/icons/peers-256x256.png differ diff --git a/src/main/resources/org/aion/wallet/ui/components/icons/search_icon.png b/src/main/resources/org/aion/wallet/ui/components/icons/search_icon.png new file mode 100644 index 00000000..5b6402f5 Binary files /dev/null and b/src/main/resources/org/aion/wallet/ui/components/icons/search_icon.png differ diff --git a/src/main/resources/org/aion/wallet/ui/components/icons/shutdown-50.png b/src/main/resources/org/aion/wallet/ui/components/icons/shutdown-50.png index 6f86d557..e2323887 100644 Binary files a/src/main/resources/org/aion/wallet/ui/components/icons/shutdown-50.png and b/src/main/resources/org/aion/wallet/ui/components/icons/shutdown-50.png differ diff --git a/src/main/resources/org/aion/wallet/ui/components/icons/total_blocks-256x256.png b/src/main/resources/org/aion/wallet/ui/components/icons/total_blocks-256x256.png index 634c58b8..376d0e3b 100644 Binary files a/src/main/resources/org/aion/wallet/ui/components/icons/total_blocks-256x256.png and b/src/main/resources/org/aion/wallet/ui/components/icons/total_blocks-256x256.png differ diff --git a/src/main/resources/org/aion/wallet/ui/components/partials/About.fxml b/src/main/resources/org/aion/wallet/ui/components/partials/About.fxml new file mode 100644 index 00000000..5ab7b848 --- /dev/null +++ b/src/main/resources/org/aion/wallet/ui/components/partials/About.fxml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/src/main/resources/org/aion/wallet/ui/components/partials/AboutDialog.fxml b/src/main/resources/org/aion/wallet/ui/components/partials/AboutDialog.fxml new file mode 100644 index 00000000..df14d4d8 --- /dev/null +++ b/src/main/resources/org/aion/wallet/ui/components/partials/AboutDialog.fxml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/org/aion/wallet/ui/components/partials/AccountsOverview.fxml b/src/main/resources/org/aion/wallet/ui/components/partials/AccountsOverview.fxml index e99829f7..4ebffce2 100644 --- a/src/main/resources/org/aion/wallet/ui/components/partials/AccountsOverview.fxml +++ b/src/main/resources/org/aion/wallet/ui/components/partials/AccountsOverview.fxml @@ -6,20 +6,24 @@ - - + - + + diff --git a/src/main/resources/org/aion/wallet/ui/components/partials/HistoryPane.fxml b/src/main/resources/org/aion/wallet/ui/components/partials/HistoryPane.fxml index 5f8820fe..b90ab168 100644 --- a/src/main/resources/org/aion/wallet/ui/components/partials/HistoryPane.fxml +++ b/src/main/resources/org/aion/wallet/ui/components/partials/HistoryPane.fxml @@ -4,15 +4,26 @@ - + + + + + diff --git a/src/main/resources/org/aion/wallet/ui/components/partials/ImportAccountDialog.fxml b/src/main/resources/org/aion/wallet/ui/components/partials/ImportAccountDialog.fxml index 794c5f04..18e89e05 100644 --- a/src/main/resources/org/aion/wallet/ui/components/partials/ImportAccountDialog.fxml +++ b/src/main/resources/org/aion/wallet/ui/components/partials/ImportAccountDialog.fxml @@ -15,7 +15,7 @@ - + @@ -57,7 +57,7 @@