diff --git a/src/main/java/com/donlaiq/command/NodeStarter.java b/src/main/java/com/donlaiq/command/NodeStarter.java new file mode 100644 index 0000000..7d763c6 --- /dev/null +++ b/src/main/java/com/donlaiq/command/NodeStarter.java @@ -0,0 +1,143 @@ +/** + * It starts the node and verify its availability before to operate on it. + * + * @author donlaiq + */ + +package com.donlaiq.command; +import java.io.File; +import java.util.Properties; +import java.util.Scanner; + +public class NodeStarter { + + // System command to start a Command Line Interface (sh for Linux, cmd.exe for Windows) + private String systemCommand; + + // Parameter to run the system command (-c for Linux, /k for Windows) + private String systemCommandParameter; + + // Absolute path to the directory holding the commands to interact with the node + private String nodeAbsolutePath; + + // Command to start the node + private String startCommand; + + // Command to make some request to the running node + private String cliCommand; + + // Process running the node + private Process cryptoNodeProcess; + + /* + * The commands are readed from an external file trying to make the application as extensible as possible. + */ + public NodeStarter() + { + try + { + Properties walletProperties = new Properties(); + walletProperties.load(NodeStarter.class.getClassLoader().getResourceAsStream("resources/wallet.properties")); + nodeAbsolutePath = walletProperties.getProperty("node.path"); + startCommand = walletProperties.getProperty("start.command"); + cliCommand = walletProperties.getProperty("cli.command"); + systemCommand = walletProperties.getProperty("system.command"); + systemCommandParameter = walletProperties.getProperty("system.command.parameter"); + } + catch(Exception e) + { + e.printStackTrace(); + } + + } + + + /* + * It tries to start the node. + */ + public boolean startNode() + { + try + { + File file = new File(nodeAbsolutePath + startCommand); + if(file.exists()) + { + ProcessBuilder bitcoinzCli = new ProcessBuilder(); + bitcoinzCli.command(systemCommand, systemCommandParameter, nodeAbsolutePath + startCommand); + cryptoNodeProcess = bitcoinzCli.start(); + } + else + throw new Exception(); + } + catch(Exception e) + { + if(cryptoNodeProcess != null) + { + cryptoNodeProcess.descendants().forEach(sub->sub.destroy()); + cryptoNodeProcess.destroy(); + } + System.out.println("The path to the command doesn't exists."); + return false; + } + return true; + } + + /* + * There's a delay between the node is started and its availability to further use. + * It keeps trying till the node is usable. + */ + public void waitUntilNodeIsAvailable() + { + Process commandProcess = null; + Scanner in = null; + boolean isStarted = false; + try + { + File file = new File(nodeAbsolutePath + cliCommand); + if(file.exists()) + { + ProcessBuilder pb = new ProcessBuilder(systemCommand, systemCommandParameter, nodeAbsolutePath + cliCommand + " getwalletinfo"); + + while(!isStarted) + { + commandProcess = pb.start(); + + in = new Scanner(commandProcess.getInputStream()); + if(in.hasNextLine()) + isStarted = true; + + Thread.sleep(1000); + } + } + else + throw new Exception(); + } + catch(Exception e) + { + System.out.println("The path to the command doesn't exists."); + } + finally + { + if(in != null) + in.close(); + if(commandProcess != null) + { + commandProcess.descendants().forEach(sub->sub.destroy()); + commandProcess.destroy(); + } + } + } + + + /* + * When the application finishes, it tries to stop the node and release its resources. + */ + public void releaseResources() + { + if(cryptoNodeProcess != null) + { + cryptoNodeProcess.descendants().forEach(sub->sub.destroy()); + cryptoNodeProcess.destroy(); + } + } +} diff --git a/src/main/java/com/donlaiq/command/ProcessHandlerWrapper.java b/src/main/java/com/donlaiq/command/ProcessHandlerWrapper.java new file mode 100644 index 0000000..a0a681a --- /dev/null +++ b/src/main/java/com/donlaiq/command/ProcessHandlerWrapper.java @@ -0,0 +1,198 @@ +/** + * Wrapper to put together all the cli commands used by the application. + * + * @author donlaiq + */ + +package com.donlaiq.command; + +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.Scanner; + +import com.donlaiq.command.factory.ReturnSingleOutputProcessHandler; +import com.donlaiq.command.factory.ProcessHandler; +import com.donlaiq.command.factory.ReturnNothingProcessHandler; +import com.donlaiq.command.factory.TAddressBalanceProcessHandler; +import com.donlaiq.command.factory.TAddressFinderProcessHandler; +import com.donlaiq.command.factory.TotalBalanceProcessHandler; +import com.donlaiq.command.factory.TransactionListProcessHandler; +import com.donlaiq.command.factory.ZAddressFinderProcessHandler; +import com.donlaiq.controller.model.Address; +import com.donlaiq.controller.model.Transaction; + +public class ProcessHandlerWrapper { + + private Properties properties, commandProperties; + + public ProcessHandlerWrapper() + { + try + { + properties = new Properties(); + properties.load(TransactionListProcessHandler.class.getClassLoader().getResourceAsStream("resources/wallet.properties")); + + commandProperties = new Properties(); + commandProperties.load(TransactionListProcessHandler.class.getClassLoader().getResourceAsStream("resources/command.properties")); + } + catch(Exception e) {} + } + + /* + * Get a list with the last 100 transactions, where some address of the wallet is associated with them. + */ + public List getTransactionList() + { + ProcessHandler processHandler = new TransactionListProcessHandler(" " + commandProperties.getProperty("list.transactions")); + return (List)processHandler.executeProcess(); + } + + /* + * Get the balances of the wallet. Two balances and the final sum of them, if there are more than one kind of address. + */ + public String[] getBalances() + { + ProcessHandler processHandler = new TotalBalanceProcessHandler(" " + commandProperties.getProperty("get.total.balance")); + return (String[])processHandler.executeProcess(); + } + + /* + * Get the list of transparent addresses. + */ + public List
getTAddressesList() + { + ProcessHandler processHandler = new TAddressFinderProcessHandler(" " + commandProperties.getProperty("get.addresses.by.account")); + TAddressBalanceProcessHandler tAddressBalanceProcessHandler = new TAddressBalanceProcessHandler(" " + commandProperties.getProperty("t.address.balance")); + tAddressBalanceProcessHandler.setAllAddresses((List)processHandler.executeProcess()); + return (List
)tAddressBalanceProcessHandler.executeProcess(); + } + + /* + * Get the list of private addresses. + */ + public List
getZAddressesList() + { + List
zAddressList = new ArrayList
(); + ProcessHandler processHandler = new ZAddressFinderProcessHandler(" " + commandProperties.getProperty("z.list.addresses")); + List zAddresses = (List)processHandler.executeProcess(); + for(String address : zAddresses) + { + ProcessHandler zAddressBalanceProcessHandler = new ReturnSingleOutputProcessHandler(" " + commandProperties.getProperty("z.get.balance") + " " + address); + zAddressList.add(new Address(address, (String)zAddressBalanceProcessHandler.executeProcess())); + } + return zAddressList; + } + + /* + * Get the private key of a transparent address. + */ + public String getTPrivateKey(String publicKey) + { + ProcessHandler processHandler = new ReturnSingleOutputProcessHandler(" " + commandProperties.getProperty("dump.priv.key") + " \"" + publicKey + "\""); + return (String) processHandler.executeProcess(); + } + + /* + * Get the private key of a private address. + */ + public String getZPrivateKey(String publicKey) + { + ProcessHandler processHandler = new ReturnSingleOutputProcessHandler(" " + commandProperties.getProperty("z.export.key") + " \"" + publicKey + "\""); + return (String) processHandler.executeProcess(); + } + + /* + * Import a transparent address to the wallet given its private key. + */ + public void importTPrivateKey(String privateKey, String rescan) + { + ProcessHandler processHandler = new ReturnNothingProcessHandler(" " + commandProperties.getProperty("import.priv.key") + " \"" + privateKey + "\" \"\" " + rescan); + processHandler.executeProcess(); + } + + /* + * Import a private address to the wallet given its private key. + */ + public void importZPrivateKey(String privateKey, String rescan) + { + ProcessHandler processHandler = new ReturnNothingProcessHandler(" " + commandProperties.getProperty("z.import.key") + " \"" + privateKey + "\" " + rescan); + processHandler.executeProcess(); + } + + /* + * Generate a new transparent address. + */ + public String newTAddress() + { + ProcessHandler processHandler = new ReturnSingleOutputProcessHandler(" " + commandProperties.getProperty("get.new.address")); + return (String)processHandler.executeProcess(); + } + + /* + * Generate a new private address. + */ + public String newZAddress() + { + ProcessHandler processHandler = new ReturnSingleOutputProcessHandler(" " + commandProperties.getProperty("z.get.new.address")); + return (String)processHandler.executeProcess(); + } + + /* + * It calls a RESTful API to get the amount of blocks of a full synchronized node. + */ + private String getTotalBlockCount() + { + String totalBlocks = null; + try + { + String uri = properties.getProperty("blockchain.explorer.blockcount.api"); + URL url = new URL(uri); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + Scanner scanner = new Scanner(connection.getInputStream()); + if(scanner.hasNext()) + totalBlocks = scanner.nextLine(); + scanner.close(); + } + catch(Exception e) + { + e.printStackTrace(); + } + return totalBlocks; + } + + /* + * Returns the percentage of synchronization of the local node. + */ + public String getBlockchainPercentage() + { + String totalBlocks = getTotalBlockCount(); + + ProcessHandler processHandler = new ReturnSingleOutputProcessHandler(" " + commandProperties.getProperty("get.block.count")); + String totalDownloadedBlocks = (String)processHandler.executeProcess(); + + if(totalBlocks != null && totalDownloadedBlocks != null && !totalBlocks.equals("") && !totalDownloadedBlocks.equals("")) + { + Double percentage = (Double.valueOf(totalDownloadedBlocks) / Double.valueOf(totalBlocks)) * 100.0; + //System.out.println(percentage); + String stringPercentage = String.valueOf(percentage); + if(stringPercentage.length() >= 5) + return stringPercentage.substring(0, 5); + return stringPercentage; + } + return ""; + } + + + /* + * It allows to send coins to another address. + */ + public void sendMoney(String addressFrom, String addressTo, String amount) + { + ProcessHandler processHandler = new ReturnNothingProcessHandler(" " + commandProperties.getProperty("send.many") + " \"" + addressFrom + "\" \"[{\\\"address\\\":\\\"" + addressTo + "\\\", \\\"amount\\\":" + amount + "}]\""); + processHandler.executeProcess(); + } +} diff --git a/src/main/java/com/donlaiq/command/factory/ProcessHandler.java b/src/main/java/com/donlaiq/command/factory/ProcessHandler.java new file mode 100644 index 0000000..2c898c9 --- /dev/null +++ b/src/main/java/com/donlaiq/command/factory/ProcessHandler.java @@ -0,0 +1,94 @@ +/** + * Class abstracting the process of calling a new process. + * It sets all the boilerplate to call a process and through an abstract method let the subclasses doing the particular job. + * + * @author donlaiq + */ + +package com.donlaiq.command.factory; + +import java.io.InputStream; +import java.util.Properties; +import java.util.Scanner; + +public abstract class ProcessHandler { + + protected Process commandProcess; + + // Reads the output of a process line by line + protected Scanner scanner; + + private String nodeAbsolutePath, cliCommand, systemCommand, systemCommandParameter; + private String command; + + + //protected int lengthTAddress, lengthTPrivateKey, lengthZAddress, lengthZPrivateKey; + + + public ProcessHandler(String command) + { + this.command = command; + scanner = null; + try + { + Properties walletProperties = new Properties(); + walletProperties.load(ProcessHandler.class.getClassLoader().getResourceAsStream("resources/wallet.properties")); + nodeAbsolutePath = walletProperties.getProperty("node.path"); + cliCommand = walletProperties.getProperty("cli.command"); + systemCommand = walletProperties.getProperty("system.command"); + systemCommandParameter = walletProperties.getProperty("system.command.parameter"); + /*lengthTAddress = Integer.valueOf(walletProperties.getProperty("length.t.address")); + lengthTPrivateKey = Integer.valueOf(walletProperties.getProperty("length.t.private.key")); + lengthZAddress = Integer.valueOf(walletProperties.getProperty("length.z.address")); + lengthZPrivateKey = Integer.valueOf(walletProperties.getProperty("length.z.private.key"));*/ + } + catch(Exception e) + { + e.printStackTrace(); + } + } + + /* + * Abstract method all the subclasses may implement, doing a particular job. + */ + public abstract Object doSomething(); + + /* + * Boilerplate stuff to create a process and release its resources after the work it's done. + */ + public Object executeProcess() + { + Object object = null; + try + { + ProcessBuilder pb = new ProcessBuilder(systemCommand, systemCommandParameter, nodeAbsolutePath + cliCommand + command); + commandProcess = pb.start(); + + initializeScanner(commandProcess.getInputStream()); + object = doSomething(); + /*scanner.close(); + scanner = null;*/ + } + catch(Exception e) + { + e.printStackTrace(); + } + finally + { + //if(scanner != null) + scanner.close(); + commandProcess.descendants().forEach(sub->sub.destroy()); + commandProcess.destroy(); + } + return object; + } + + /* + * Given the difficulty to test the functioning of a process, this method allows to fake the output of a process. + */ + public void initializeScanner(InputStream inputStream) + { + if(scanner == null) + scanner = new Scanner(inputStream); + } +} diff --git a/src/main/java/com/donlaiq/command/factory/ReturnNothingProcessHandler.java b/src/main/java/com/donlaiq/command/factory/ReturnNothingProcessHandler.java new file mode 100644 index 0000000..ace023d --- /dev/null +++ b/src/main/java/com/donlaiq/command/factory/ReturnNothingProcessHandler.java @@ -0,0 +1,21 @@ +/** + * A command returning nothing after its processing. + * + * @author donlaiq + */ + +package com.donlaiq.command.factory; + +public class ReturnNothingProcessHandler extends ProcessHandler{ + + public ReturnNothingProcessHandler(String command) + { + super(command); + } + + @Override + public Object doSomething() + { + return null; + } +} diff --git a/src/main/java/com/donlaiq/command/factory/ReturnSingleOutputProcessHandler.java b/src/main/java/com/donlaiq/command/factory/ReturnSingleOutputProcessHandler.java new file mode 100644 index 0000000..cc75727 --- /dev/null +++ b/src/main/java/com/donlaiq/command/factory/ReturnSingleOutputProcessHandler.java @@ -0,0 +1,25 @@ +/** + * A command returning a single line. + * + * @author donlaiq + */ + +package com.donlaiq.command.factory; + +public class ReturnSingleOutputProcessHandler extends ProcessHandler{ + + public ReturnSingleOutputProcessHandler(String command) + { + super(command); + } + + @Override + public Object doSomething() + { + String privateKey = ""; + if(scanner.hasNextLine()) + privateKey = scanner.nextLine(); + return privateKey; + } + +} diff --git a/src/main/java/com/donlaiq/command/factory/TAddressBalanceProcessHandler.java b/src/main/java/com/donlaiq/command/factory/TAddressBalanceProcessHandler.java new file mode 100644 index 0000000..ad2487a --- /dev/null +++ b/src/main/java/com/donlaiq/command/factory/TAddressBalanceProcessHandler.java @@ -0,0 +1,66 @@ +/** + * A command returning a list of transparent addresses with its corresponding balance. + * + * @author donlaiq + */ + +package com.donlaiq.command.factory; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +import com.donlaiq.controller.model.Address; + +public class TAddressBalanceProcessHandler extends ProcessHandler{ + + private List allAddresses; + + public TAddressBalanceProcessHandler(String command) + { + super(command); + } + + @Override + public Object doSomething() + { + List
addresses = new ArrayList
(); + List listedAddresses = new ArrayList(); + + String address = ""; + String balance = ""; + + while(scanner.hasNext()) + { + String line = scanner.nextLine(); + if(line.contains("\"") && !line.contains("\"\"") && line.split("\"")[1].matches("^[a-zA-Z0-9]*$")) + { + address = line.split("\"")[1]; + listedAddresses.add(address); + } + else if(Pattern.compile("[0-9]").matcher(line).find()) + { + if(line.endsWith(",")) + balance = line.substring(0, line.length()-1).trim(); + else + balance = line.substring(0, line.length()).trim(); + addresses.add(new Address(address, balance)); + } + } + + for(String addr : allAddresses) + if(!listedAddresses.contains(addr)) + addresses.add(new Address(addr, "0.0")); + + return addresses; + } + + + /* + * This class needs the output of a previous process to do its job, so it works like a pipe between processes. + */ + public void setAllAddresses(List allAddreses) + { + this.allAddresses = allAddreses; + } +} diff --git a/src/main/java/com/donlaiq/command/factory/TAddressFinderProcessHandler.java b/src/main/java/com/donlaiq/command/factory/TAddressFinderProcessHandler.java new file mode 100644 index 0000000..a1dbf99 --- /dev/null +++ b/src/main/java/com/donlaiq/command/factory/TAddressFinderProcessHandler.java @@ -0,0 +1,34 @@ +/** + * A command returning all the transparent addresses in the wallet. + * + * @author donlaiq + */ + +package com.donlaiq.command.factory; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +public class TAddressFinderProcessHandler extends ProcessHandler{ + + public TAddressFinderProcessHandler(String command) + { + super(command); + } + + @Override + public Object doSomething() { + List allAddresses = new ArrayList(); + while(scanner.hasNext()) + { + String line = scanner.nextLine(); + if(Pattern.compile("[0-9]").matcher(line).find()) + { + allAddresses.add(line.split("\"")[1]); + } + } + return allAddresses; + } + +} diff --git a/src/main/java/com/donlaiq/command/factory/TotalBalanceProcessHandler.java b/src/main/java/com/donlaiq/command/factory/TotalBalanceProcessHandler.java new file mode 100644 index 0000000..d94ce9a --- /dev/null +++ b/src/main/java/com/donlaiq/command/factory/TotalBalanceProcessHandler.java @@ -0,0 +1,49 @@ +/** + * A command returning the balance of the wallet, differentiating between transparent and private addresses and summing the whole balance. + * + * @author donlaiq + */ + +package com.donlaiq.command.factory; + +import java.util.regex.Pattern; + +public class TotalBalanceProcessHandler extends ProcessHandler{ + + public TotalBalanceProcessHandler(String command) + { + super(command); + } + + @Override + public Object doSomething() + { + String[] balances = new String[3]; + while(scanner.hasNext()) + { + String line = scanner.nextLine(); + if(line.contains("transparent")) + { + String aux = line.split(":")[1]; + balances[0] = aux.substring(1, aux.length()-1).split("\"")[1]; + } + else if(line.contains("private")) + { + String aux = line.split(":")[1]; + balances[1] = aux.substring(1, aux.length()-1).split("\"")[1]; + } + else if(line.contains("total")) + { + String aux = line.split(":")[1]; + balances[2] = aux.substring(1, aux.length()-1).split("\"")[1]; + } + else if(Pattern.compile("[0-9]").matcher(line).find()) + { + balances[0] = line.trim(); + balances[1] = "0.0"; + balances[2] = "0.0"; + } + } + return balances; + } +} diff --git a/src/main/java/com/donlaiq/command/factory/TransactionListProcessHandler.java b/src/main/java/com/donlaiq/command/factory/TransactionListProcessHandler.java new file mode 100644 index 0000000..212c709 --- /dev/null +++ b/src/main/java/com/donlaiq/command/factory/TransactionListProcessHandler.java @@ -0,0 +1,85 @@ +/** + * A command returning the last N transactions involving addresses from the wallet. + * + * @author donlaiq + */ + +package com.donlaiq.command.factory; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Properties; +import java.util.TimeZone; + +import com.donlaiq.controller.model.Transaction; + +public class TransactionListProcessHandler extends ProcessHandler{ + private Properties properties; + + public TransactionListProcessHandler(String command) + { + super(command); + try + { + properties = new Properties(); + properties.load(TransactionListProcessHandler.class.getClassLoader().getResourceAsStream("resources/wallet.properties")); + } + catch(Exception e) {} + } + + @Override + public Object doSomething() + { + String address = ""; + String amount = ""; + String time = ""; + String txid = ""; + List transactionList = new ArrayList(); + while(scanner.hasNext()) + { + String line = scanner.nextLine(); + if(line.contains("address")) + address = line.split(":")[1].split("\"")[1]; + else if(line.contains("amount")) + amount = line.split(":")[1].substring(1, line.split(":")[1].length()-1); + else if(line.contains("txid")) + txid = line.split(":")[1].split("\"")[1]; + else if(line.contains("\"time\"")) + { + Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone(properties.getProperty("time.zone"))); + calendar.setTime(new Date(Long.valueOf(line.split(":")[1].substring(1, line.split(":")[1].length()-1)) * 1000L)); + int year = calendar.get(Calendar.YEAR); + int month = calendar.get(Calendar.MONTH); + String monthToString = String.valueOf(month+1); + if(monthToString.length() < 2) + monthToString = "0" + monthToString; + int day = calendar.get(Calendar.DAY_OF_MONTH); + String dayToString = String.valueOf(day); + if(dayToString.length() < 2) + dayToString = "0" + dayToString; + int hour = calendar.get(Calendar.HOUR_OF_DAY); + int minute = calendar.get(Calendar.MINUTE); + int second = calendar.get(Calendar.SECOND); + String hourToString = String.valueOf(hour); + if(hourToString.length() < 2) + hourToString = "0" + hourToString; + String minuteToString = String.valueOf(minute); + if(minuteToString.length() < 2) + minuteToString = "0" + minuteToString; + String secondToString = String.valueOf(second); + if(secondToString.length() < 2) + secondToString = "0" + secondToString; + + time = String.valueOf(year) + "-" + monthToString + "-" + dayToString + " " + hourToString + ":" + minuteToString + ":" + secondToString; + + transactionList.add(new Transaction(time, amount, address, txid)); + } + } + + return transactionList; + } + + +} diff --git a/src/main/java/com/donlaiq/command/factory/ZAddressFinderProcessHandler.java b/src/main/java/com/donlaiq/command/factory/ZAddressFinderProcessHandler.java new file mode 100644 index 0000000..0ec53c7 --- /dev/null +++ b/src/main/java/com/donlaiq/command/factory/ZAddressFinderProcessHandler.java @@ -0,0 +1,35 @@ +/** + * A command returning all the private addresses in the wallet. + * + * @author donlaiq + */ + +package com.donlaiq.command.factory; + +import java.util.ArrayList; +import java.util.List; + +import com.donlaiq.command.factory.ProcessHandler; + +public class ZAddressFinderProcessHandler extends ProcessHandler{ + + public ZAddressFinderProcessHandler(String command) + { + super(command); + } + + @Override + public Object doSomething() + { + List zAddresses = new ArrayList(); + while(scanner.hasNext()) + { + String line = scanner.nextLine(); + if(line.contains("z")) + { + zAddresses.add(line.split("\"")[1]); + } + } + return zAddresses; + } +} diff --git a/src/main/java/com/donlaiq/command/service/ImportService.java b/src/main/java/com/donlaiq/command/service/ImportService.java new file mode 100644 index 0000000..3a0eef0 --- /dev/null +++ b/src/main/java/com/donlaiq/command/service/ImportService.java @@ -0,0 +1,29 @@ +/** + * JavaFX service enclosing a JavaFX task to import concurrently several addresses. + * + * @author donlaiq + */ + +package com.donlaiq.command.service; + +import java.io.File; + +import javafx.concurrent.Service; +import javafx.concurrent.Task; + +public class ImportService extends Service{ + + private File file; + + public ImportService(File file) + { + this.file = file; + } + + @Override + protected Task createTask() + { + return new ImportTask(file); + } + +} diff --git a/src/main/java/com/donlaiq/command/service/ImportTask.java b/src/main/java/com/donlaiq/command/service/ImportTask.java new file mode 100644 index 0000000..2d8a25f --- /dev/null +++ b/src/main/java/com/donlaiq/command/service/ImportTask.java @@ -0,0 +1,88 @@ +/** + * Task to handle concurrently the import of the several addresses on the wallet. + * It waits for a UTF-8 text file having a "-" character in every line, where every line represents an address. + * If the left side of the character "-" starts with a z, then the address to import is a Z address. + * If the left side of the character "-" starts with anything else, then it tries to import the transparent address handled by the wallet. + * The right side of the character "-" should be the private key of the address + * + * @donlaiq + */ + +package com.donlaiq.command.service; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +import com.donlaiq.command.ProcessHandlerWrapper; + +import javafx.concurrent.Task; + +public class ImportTask extends Task{ + + private ProcessHandlerWrapper processHandlerWrapper; + private File file; + + + public ImportTask(File file) + { + this.file = file; + this.processHandlerWrapper = new ProcessHandlerWrapper(); + } + + @Override + protected Boolean call() throws Exception + { + + List lines = new ArrayList(); + FileInputStream fileReader = new FileInputStream(file); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(fileReader, "UTF-8")); + String line = bufferedReader.readLine(); + while(line != null) + { + lines.add(line); + line = bufferedReader.readLine(); + } + + bufferedReader.close(); + + for(int i = 0; i < lines.size(); i++) + { + if(lines.get(i).contains("-")) + { + // if the user is trying to import an address from a node just with transparent addresses, + // then the file should follow the format *- for every line. + // After the last address to import, it does a rescan to look for the last 100 transactions + // associated the addresses of the wallet. + if(i == lines.size() - 1) + { + if(lines.get(i).startsWith("z")) + processHandlerWrapper.importZPrivateKey(lines.get(i).split("-")[1].trim(), "true"); + else + processHandlerWrapper.importTPrivateKey(lines.get(i).split("-")[1].trim(), "true"); + } + else + { + if(lines.get(i).startsWith("z")) + processHandlerWrapper.importZPrivateKey(lines.get(i).split("-")[1].trim(), "false"); + else + processHandlerWrapper.importTPrivateKey(lines.get(i).split("-")[1].trim(), "false"); + } + + } + else + { + bufferedReader.close(); + throw new Exception(); + } + + } + + + return true; + } + +} diff --git a/src/main/java/com/donlaiq/command/service/LastTransactionService.java b/src/main/java/com/donlaiq/command/service/LastTransactionService.java new file mode 100644 index 0000000..3d84cfa --- /dev/null +++ b/src/main/java/com/donlaiq/command/service/LastTransactionService.java @@ -0,0 +1,39 @@ +/** + * JavaFX service to look for concurrently the last 100 transactions associated with the addresses on the wallet. + * + * @author donlaiq + */ + +package com.donlaiq.command.service; + +import java.util.List; + +import com.donlaiq.command.ProcessHandlerWrapper; +import com.donlaiq.controller.model.Transaction; + +import javafx.concurrent.Service; +import javafx.concurrent.Task; + +public class LastTransactionService extends Service>{ + + private ProcessHandlerWrapper processHandlerWrapper; + + public LastTransactionService() + { + this.processHandlerWrapper = new ProcessHandlerWrapper(); + } + + @Override + protected Task> createTask() { + return new Task>() { + + @Override + protected List call() throws Exception { + return processHandlerWrapper.getTransactionList(); + } + + }; + } + +} + diff --git a/src/main/java/com/donlaiq/command/service/NodeTask.java b/src/main/java/com/donlaiq/command/service/NodeTask.java new file mode 100644 index 0000000..43d4cf5 --- /dev/null +++ b/src/main/java/com/donlaiq/command/service/NodeTask.java @@ -0,0 +1,40 @@ +/** + * JavaFX task to start the node and wait for it availability. + * + * @author donlaiq + */ + +package com.donlaiq.command.service; + +import com.donlaiq.command.NodeStarter; + +import javafx.concurrent.Task; + +public class NodeTask extends Task{ + + private NodeStarter nodeStarter; + private boolean isStartedNode; + + public NodeTask() + { + nodeStarter = new NodeStarter(); + isStartedNode = nodeStarter.startNode(); + } + + @Override + protected Boolean call() throws Exception + { + if(isStartedNode) + { + nodeStarter.waitUntilNodeIsAvailable(); + return true; + } + return false; + } + + + public NodeStarter getNodeStarter() + { + return nodeStarter; + } +} diff --git a/src/main/java/com/donlaiq/controller/MessageController.java b/src/main/java/com/donlaiq/controller/MessageController.java new file mode 100644 index 0000000..e69917f --- /dev/null +++ b/src/main/java/com/donlaiq/controller/MessageController.java @@ -0,0 +1,51 @@ +/** + * JavaFX controller for the popup window showing useful messages to the user. + * + * @author donlaiq + */ + +package com.donlaiq.controller; + +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.stage.Stage; + +public class MessageController { + + private Stage stage; + private String firstLabel, secondLabel; + + @FXML + private Label label1, label2; + + MessageController(Stage stage, String firstLabel, String secondLabel) + { + this.stage = stage; + this.firstLabel = firstLabel; + this.secondLabel = secondLabel; + } + + + /* + * Set the message using two labels in a stack + */ + @FXML + protected void initialize() + { + label1.setText(firstLabel); + label2.setText(secondLabel); + } + + + /* + * After press the Ok button + */ + @FXML + public void closeAlert(ActionEvent event) + { + stage.close(); + WalletController.isMessageShown = false; + } + +} diff --git a/src/main/java/com/donlaiq/controller/MessagePopup.java b/src/main/java/com/donlaiq/controller/MessagePopup.java new file mode 100644 index 0000000..a81c4d0 --- /dev/null +++ b/src/main/java/com/donlaiq/controller/MessagePopup.java @@ -0,0 +1,57 @@ +/** + * JavaFX popup window showing useful messages to the user. + * + * @author donlaiq + */ + +package com.donlaiq.controller; + +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.layout.Pane; +import javafx.stage.Modality; +import javafx.stage.Stage; + +public class MessagePopup extends Stage{ + private String firstLine, secondLine, title; + private Stage parentStage; + + public MessagePopup(Stage parentStage, String title, String firstLine, String secondLine) + { + this.title = title; + this.firstLine = firstLine; + this.secondLine = secondLine; + this.parentStage = parentStage; + + initialize(); + } + + private void initialize() + { + initModality(Modality.WINDOW_MODAL); + initOwner(parentStage); + setTitle(title); + + FXMLLoader fxmlLoader = null; + fxmlLoader = new FXMLLoader(getClass().getResource("messagePopup.fxml")); + + fxmlLoader.setController(new MessageController(this, firstLine, secondLine)); + try + { + Pane mainPane = (Pane)fxmlLoader.load(); + + Scene scene = new Scene(mainPane); + mainPane.getStylesheets().add(MessagePopup.class.getClassLoader().getResource("resources/messagePopupStyle.css").toExternalForm()); + setScene(scene); + } + catch(Exception e) + { + e.printStackTrace(); + } + } + + public void display() + { + show(); + } +} diff --git a/src/main/java/com/donlaiq/controller/NewAddressesController.java b/src/main/java/com/donlaiq/controller/NewAddressesController.java new file mode 100644 index 0000000..6061a19 --- /dev/null +++ b/src/main/java/com/donlaiq/controller/NewAddressesController.java @@ -0,0 +1,72 @@ +/** + * JavaFx controller for the popup window to ask if the user really wants to create a new address. + * + * @author donlaiq + */ + +package com.donlaiq.controller; + +import java.util.Properties; + +import com.donlaiq.command.ProcessHandlerWrapper; + +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.stage.Stage; + +public class NewAddressesController { + private boolean isTAddress; + private Stage parentStage, grandparentStage; + private WalletController walletController; + private Properties popupProperties; + + @FXML + private Label label1, label2; + + public NewAddressesController(boolean isTAddress, Stage parentStage, WalletController walletController, Properties popupProperties) + { + this.isTAddress = isTAddress; + this.parentStage = parentStage; + this.walletController = walletController; + this.popupProperties = popupProperties; + } + + @FXML + protected void initialize() + { + label1.setText(popupProperties.getProperty("popup.new.t.address.first.line")); + label2.setText(popupProperties.getProperty("popup.new.t.address.second.line")); + if(!isTAddress) + label2.setText(popupProperties.getProperty("popup.new.z.address.second.line")); + } + + /* + * The user accepts to create a new address. + */ + @FXML + public void doAction(ActionEvent event) + { + String newAddress = ""; + ProcessHandlerWrapper processHandlerWrapper = new ProcessHandlerWrapper(); + if(isTAddress) + newAddress = processHandlerWrapper.newTAddress(); + else + newAddress = processHandlerWrapper.newZAddress(); + walletController.loadAdresseses(); + parentStage.close(); + + MessagePopup mp = new MessagePopup(grandparentStage, popupProperties.getProperty("popup.new.address.created.title"), popupProperties.getProperty("popup.new.address.created.first.line"), newAddress); + mp.display(); + } + + /* + * The user cancel the creation of a new address. + */ + @FXML + public void cancel(ActionEvent event) + { + parentStage.close(); + } + +} diff --git a/src/main/java/com/donlaiq/controller/SendMoneyController.java b/src/main/java/com/donlaiq/controller/SendMoneyController.java new file mode 100644 index 0000000..012e640 --- /dev/null +++ b/src/main/java/com/donlaiq/controller/SendMoneyController.java @@ -0,0 +1,66 @@ +/** + * JavaFX popup window asking to the user to confirm the transaction of coins. + * + * @author donlaiq + */ + +package com.donlaiq.controller; + +import java.util.Properties; + +import com.donlaiq.command.ProcessHandlerWrapper; + +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.stage.Stage; + +public class SendMoneyController { + + private Stage parentStage, grandparentStage; + private Properties popupProperties; + private String addressFrom, addressTo, amount; + + @FXML + private Label label1, label2; + + public SendMoneyController(Stage parentStage, Properties popupProperties, String addressFrom, String addressTo, String amount) + { + this.parentStage = parentStage; + this.popupProperties = popupProperties; + this.addressFrom = addressFrom; + this.addressTo = addressTo; + this.amount = amount; + } + + @FXML + protected void initialize() + { + label1.setText(popupProperties.getProperty("popup.send.money.first.line").replace("${coin.code}", amount + " " + popupProperties.getProperty("coin.code"))); + label2.setText(addressTo); + } + + /* + * The user proceeds with the transaction. + */ + @FXML + public void doAction(ActionEvent event) + { + ProcessHandlerWrapper processHandlerWrapper = new ProcessHandlerWrapper(); + processHandlerWrapper.sendMoney(addressFrom, addressTo, amount); + parentStage.close(); + + MessagePopup mp = new MessagePopup(grandparentStage, popupProperties.getProperty("popup.money.sent.title"), popupProperties.getProperty("popup.money.sent.first.line"), popupProperties.getProperty("popup.money.sent.second.line")); + mp.display(); + } + + /* + * The user cancels the transaction. + */ + @FXML + public void cancel(ActionEvent event) + { + parentStage.close(); + } + +} diff --git a/src/main/java/com/donlaiq/controller/WalletController.java b/src/main/java/com/donlaiq/controller/WalletController.java new file mode 100644 index 0000000..8023950 --- /dev/null +++ b/src/main/java/com/donlaiq/controller/WalletController.java @@ -0,0 +1,746 @@ +/** + * JavaFX controller for the main GUI of the application. + * + * @author donlaiq + */ + +package com.donlaiq.controller; + +import javafx.animation.Timeline; +import javafx.animation.TranslateTransition; +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.concurrent.Service; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuItem; +import javafx.scene.control.RadioMenuItem; +import javafx.scene.control.SeparatorMenuItem; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.TextField; +import javafx.scene.control.ToggleGroup; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.scene.control.cell.TextFieldTableCell; +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 javafx.scene.layout.BorderPane; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Pane; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.stage.DirectoryChooser; +import javafx.stage.FileChooser; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.util.Callback; +import javafx.util.Duration; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.Scanner; +import java.util.Timer; +import java.util.TimerTask; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.donlaiq.command.NodeStarter; +import com.donlaiq.command.ProcessHandlerWrapper; +import com.donlaiq.command.service.ImportService; +import com.donlaiq.command.service.ImportTask; +import com.donlaiq.command.service.LastTransactionService; +import com.donlaiq.controller.model.Address; +import com.donlaiq.controller.model.Transaction; +import com.donlaiq.main.SplashScreen; + +public class WalletController { + + private NodeStarter nodeStarter; + private ProcessHandlerWrapper processHandlerWrapper; + private ObservableList
tAddressList, zAddressList; + private ObservableList transactionList; + + private boolean isImportingAddresses = false; + private boolean isTryingToUpdate = false; + + private TimerTask updateTask; + private Timer timer; + private Stage stage; + + // Workaround to avoid multiple popup windows after double click. + public static boolean isMessageShown; + + private boolean isTwoKindOfAddresses; + + private Properties stringProperties, walletProperties; + + @FXML + private TextField destinationAddress, amountToSend; + + @FXML + private Button sendButton, donateButton/*, backupButton, importButton, newTButton, newZButton*/; + + @FXML + private Label progressLabel, tBalanceLabel, zBalanceLabel, totalBalanceLabel, percentageLabel, donateLabel, btczLabel, + tB, zB, totalB, myAddressesLabel, destinationAddressLabel, amountToSendLabel, codeLabel; + + @FXML + private TableView tableView; + + @FXML + private TableView
addressTable; + + @FXML + private TableColumn dateColumn, amountColumn, addressColumn, txidColumn; + + @FXML + private TableColumn myAddressColumn, myBalanceColumn; + + @FXML + private ImageView arrow, mainLogo, background; + + @FXML + private GridPane donateGrid; + + @FXML + private BorderPane sendBorderPane; + + @FXML + private StackPane iconP; + + @FXML + private Menu mainMenu, languageMenu, walletMenu; + + @FXML + private MenuItem /*aboutMenu,*/ quitMenu, backupMenu, importMenu, newTMenu, newZMenu; + + @FXML + private VBox leftVBox; + + @FXML + private AnchorPane leftAnchorPane; + + + public WalletController(NodeStarter nodeSarter, Stage stage) + { + this.nodeStarter = nodeSarter; + this.stage = stage; + this.processHandlerWrapper = new ProcessHandlerWrapper(); + + try + { + walletProperties = new Properties(); + walletProperties.load(WalletController.class.getClassLoader().getResourceAsStream("resources/wallet.properties")); + + stringProperties = new Properties(); + BufferedReader in = new BufferedReader(new InputStreamReader(WalletController.class.getClassLoader().getResourceAsStream("resources/english.properties"), walletProperties.getProperty("encode"))); + stringProperties.load(in); + + isTwoKindOfAddresses = Boolean.valueOf(walletProperties.getProperty("two.kind.of.addresses")); + } + catch(Exception e) + { + e.printStackTrace(); + } + } + + @FXML + protected void initialize() + { + try + { + background.setImage(new Image(WalletController.class.getClassLoader().getResourceAsStream("resources/background.png"))); + mainLogo.setImage(new Image(WalletController.class.getClassLoader().getResourceAsStream("resources/mainLogo.png"))); + arrow.setImage(new Image(WalletController.class.getClassLoader().getResourceAsStream("resources/arrow.png"))); + } + catch(Exception e) { + e.printStackTrace(); + } + + loadWidgetsStrings(); + + dateColumn.setCellValueFactory(new PropertyValueFactory("date")); + amountColumn.setCellValueFactory(new PropertyValueFactory("amount")); + addressColumn.setCellValueFactory(new PropertyValueFactory("address")); + txidColumn.setCellValueFactory(new PropertyValueFactory("txid")); + + Callback, TableCell> defaultTableViewCellFactory = TextFieldTableCell.forTableColumn(); + + Callback, TableCell> tableViewCellFactory = col -> { + TableCell cell = defaultTableViewCellFactory.call(col); + cell.itemProperty().addListener((obs, oldValue, newValue) -> { + if(newValue != null) + { + cell.addEventFilter(MouseEvent.MOUSE_CLICKED, new EventHandler() { + @Override + public void handle(MouseEvent event) { + if (event.getClickCount() == 2 && !WalletController.isMessageShown) + { + TableCell c = (TableCell) event.getSource(); + + Clipboard clipboard = Clipboard.getSystemClipboard(); + ClipboardContent content = new ClipboardContent(); + content.putString(c.getText()); + clipboard.setContent(content); + + createAndDisplayPopupMessage(stringProperties.getProperty("popup.to.clipboard.title"), c.getText(), stringProperties.getProperty("popup.to.clipboard.second.line")); + + WalletController.isMessageShown = true; + } + } + }); + } + }); + return cell; + }; + dateColumn.setCellFactory(tableViewCellFactory); + amountColumn.setCellFactory(tableViewCellFactory); + addressColumn.setCellFactory(tableViewCellFactory); + txidColumn.setCellFactory(tableViewCellFactory); + + + + myAddressColumn.setCellValueFactory(new PropertyValueFactory("addressString")); + myBalanceColumn.setCellValueFactory(new PropertyValueFactory("balance")); + Callback, TableCell> defaultTableViewCellFactoryMyAddresses = TextFieldTableCell.forTableColumn(); + + Callback, TableCell> tableViewCellFactoryMyAddresses = col -> { + TableCell cell = defaultTableViewCellFactoryMyAddresses.call(col); + cell.itemProperty().addListener((obs, oldValue, newValue) -> { + if(newValue != null) + { + cell.addEventFilter(MouseEvent.MOUSE_CLICKED, new EventHandler() { + @Override + public void handle(MouseEvent event) { + if (event.getClickCount() == 2 && !WalletController.isMessageShown) { + TableCell c = (TableCell) event.getSource(); + + Clipboard clipboard = Clipboard.getSystemClipboard(); + ClipboardContent content = new ClipboardContent(); + content.putString(c.getText()); + clipboard.setContent(content); + + createAndDisplayPopupMessage(stringProperties.getProperty("popup.to.clipboard.title"), c.getText(), stringProperties.getProperty("popup.to.clipboard.second.line")); + + WalletController.isMessageShown = true; + } + } + }); + } + }); + return cell; + }; + myAddressColumn.setCellFactory(tableViewCellFactoryMyAddresses); + myBalanceColumn.setCellFactory(tableViewCellFactoryMyAddresses); + + + + progressLabel.setText("Synchronized"); + + arrow.setFitHeight(40); + + ToggleGroup languageGroup = new ToggleGroup(); + + RadioMenuItem englishItem = new RadioMenuItem(); + RadioMenuItem foreignItem = new RadioMenuItem(); + englishItem.setToggleGroup(languageGroup); + englishItem.setSelected(true); + englishItem.setText(stringProperties.getProperty("english.language")); + englishItem.setOnAction(e -> { + try + { + englishItem.setSelected(true); + stringProperties = new Properties(); + BufferedReader in = new BufferedReader(new InputStreamReader(WalletController.class.getClassLoader().getResourceAsStream("resources/english.properties"), walletProperties.getProperty("encode"))); + stringProperties.load(in); + loadWidgetsStrings(); + } + catch(Exception ex) + { + ex.printStackTrace(); + } + }); + + + foreignItem.setToggleGroup(languageGroup); + foreignItem.setText(walletProperties.getProperty("foreign.language")); + foreignItem.setOnAction(e -> { + try + { + foreignItem.setSelected(true); + stringProperties = new Properties(); + BufferedReader in = new BufferedReader(new InputStreamReader(WalletController.class.getClassLoader().getResourceAsStream("resources/foreign.properties"), walletProperties.getProperty("encode"))); + stringProperties.load(in); + loadWidgetsStrings(); + } + catch(Exception ex) + { + ex.printStackTrace(); + } + }); + + languageMenu.getItems().add(englishItem); + languageMenu.getItems().add(foreignItem); + + SeparatorMenuItem separator = new SeparatorMenuItem(); + walletMenu.getItems().add(2, separator); + + + updateFromBlockchain(); + //loadAdresseses(); + + + stage.setOnShown(e -> { + animateLogo(); + }); + + stage.setOnCloseRequest(e -> { + try + { + // destroy the process running the node + nodeStarter.releaseResources(); + + timer.cancel(); + + Platform.exit(); + } + catch(Exception ex) { + ex.printStackTrace(); + }}); + + + // this task is runned every minute to look for new updates + updateTask = new TimerTask() { + + @Override + public void run() + { + Platform.runLater(new Runnable() { + + @Override + public void run() { + // The process of import addresses take some time. If this process is activated, then it won't try to look for new updates. + if(!isImportingAddresses) + { + updateFromBlockchain(); + } + } + + }); + } + + }; + timer = new Timer(); + timer.schedule(updateTask, 60000, 60000); + + if(!isTwoKindOfAddresses) + newZMenu.setVisible(false); + } + + + /* + * Set the new Strings after the language is changed. + */ + private void loadWidgetsStrings() + { + mainMenu.setText(stringProperties.getProperty("main.menu")); + walletMenu.setText(stringProperties.getProperty("wallet.menu")); + languageMenu.setText(stringProperties.getProperty("language.menu")); + //aboutMenu.setText(stringProperties.getProperty("about.menu")); + quitMenu.setText(stringProperties.getProperty("quit.menu")); + backupMenu.setText(stringProperties.getProperty("backup.menu")); + importMenu.setText(stringProperties.getProperty("import.menu")); + newTMenu.setText(stringProperties.getProperty("new.t.menu")); + newZMenu.setText(stringProperties.getProperty("new.z.menu")); + if(isTwoKindOfAddresses) + { + tB.setText(stringProperties.getProperty("t.balance")); + zB.setText(stringProperties.getProperty("z.balance")); + } + else + { + tB.setVisible(false); + zB.setVisible(false); + } + totalB.setText(stringProperties.getProperty("total.balance")); + dateColumn.setText(stringProperties.getProperty("date.column")); + amountColumn.setText(stringProperties.getProperty("amount.column")); + addressColumn.setText(stringProperties.getProperty("address.column")); + txidColumn.setText(stringProperties.getProperty("txid.column")); + myAddressesLabel.setText(stringProperties.getProperty("my.addresses")); + destinationAddressLabel.setText(stringProperties.getProperty("destination.address")); + amountToSendLabel.setText(stringProperties.getProperty("amount.to.send")); + codeLabel.setText(walletProperties.getProperty("coin.code")); + sendButton.setText(stringProperties.getProperty("send")); + donateButton.setText(stringProperties.getProperty("donate")); + myAddressColumn.setText(stringProperties.getProperty("my.address.column")); + myBalanceColumn.setText(stringProperties.getProperty("my.balance.column")); + + progressLabel.setText(stringProperties.getProperty("synchronized.node")); + if(!mainLogo.isVisible()) + progressLabel.setText(stringProperties.getProperty("synchronizing.node")); + } + + /* + * Get new updates from the blockchain. + */ + private void updateFromBlockchain() + { + // If some update is taking so much time, specially when it comes to track the last 100 transactions associated with addresses + // of the wallet, it could take more than 60 seconds (time by default to look for new updates), then it won't try to update again. + if(!isTryingToUpdate) + { + isTryingToUpdate = true; + + // initialize a service to look for the last 100 transactions associated with the addresses of the wallet, + // but it doesn't block the GUI. It will return the results in the future, when they are ready. + LastTransactionService lastTransactionService = new LastTransactionService(); + lastTransactionService.setOnSucceeded(e -> { + transactionList = FXCollections.observableList(lastTransactionService.getValue()); + tableView.setItems(transactionList); + + String percentage = processHandlerWrapper.getBlockchainPercentage(); + if(!percentage.equals("")) + { + if(Double.valueOf(percentage) < 100.0) + { + percentageLabel.setText(String.valueOf(percentage) + "%"); + progressLabel.setText(stringProperties.getProperty("synchronizing.node")); + mainLogo.setVisible(false); + } + else + { + progressLabel.setText(stringProperties.getProperty("synchronized.node")); + percentageLabel.setVisible(false); + mainLogo.setVisible(true); + } + } + + String[] balances = processHandlerWrapper.getBalances(); + if(isTwoKindOfAddresses) + { + tBalanceLabel.setText(" " + balances[0]); + zBalanceLabel.setText(" " + balances[1]); + totalBalanceLabel.setText(" " + balances[2]); + } + else + { + totalBalanceLabel.setText(" " + balances[0]); + tBalanceLabel.setVisible(false); + zBalanceLabel.setVisible(false); + } + + + loadAdresseses(); + + isTryingToUpdate = false; + }); + lastTransactionService.start(); + + } + + } + + + /* + * Updates the table with the addresses from the wallet. + */ + public void loadAdresseses() + { + List
tList = processHandlerWrapper.getTAddressesList(); + List
zList = null; + if(isTwoKindOfAddresses) + zList = processHandlerWrapper.getZAddressesList(); + + List
mergedList = new ArrayList
(); + mergedList.addAll(tList); + if(isTwoKindOfAddresses) + mergedList.addAll(zList); + + tAddressList = FXCollections.observableList(tList); + if(isTwoKindOfAddresses) + zAddressList = FXCollections.observableList(zList); + + addressTable.setItems(FXCollections.observableList(mergedList)); + + } + + /* + * Animate the arrow after click the donate button + */ + public void animateLogo() { + TranslateTransition tt = new TranslateTransition(Duration.seconds(1), arrow); + + tt.setFromX(100); + + tt.setToX(2); + tt.setCycleCount(Timeline.INDEFINITE); + tt.setAutoReverse(true); + tt.play(); + } + + + /* + * Listener of the menu item. + */ + @FXML + protected void createNewAddress(ActionEvent event) + { + try + { + boolean isT = true; + + Stage popup = new Stage(); + popup.initModality(Modality.WINDOW_MODAL); + popup.initOwner(stage); + + FXMLLoader fxmlLoader = null; + fxmlLoader = new FXMLLoader(getClass().getResource("messageWithCancelPopup.fxml")); + if(((MenuItem)event.getSource()).getText().contains("Z")) + isT = false; + + NewAddressesController addressController = new NewAddressesController(isT, popup, this, stringProperties); + + fxmlLoader.setController(addressController); + Pane mainPane = (Pane)fxmlLoader.load(); + mainPane.getStylesheets().add(WalletController.class.getClassLoader().getResource("resources/messagePopupStyle.css").toExternalForm()); + + Scene scene = new Scene(mainPane); + popup.setTitle(stringProperties.getProperty("popup.new.t.address.title")); + if(!isT) + popup.setTitle(stringProperties.getProperty("popup.new.z.address.title")); + popup.setScene(scene); + popup.show(); + } + catch(Exception e) + { + e.printStackTrace(); + } + } + + + /* + * Listener of the menu item. + * Save a file where every line has the following format: + * - + * could be a transparent or a private address. + */ + @FXML + protected void backupWallet(ActionEvent event) + { + DirectoryChooser directoryChooser = new DirectoryChooser(); + directoryChooser.setTitle(stringProperties.getProperty("backup.addresses.directory.chooser.title")); + File directory = directoryChooser.showDialog(stage); + + if(directory != null) + { + List pkTList = new ArrayList(); + for(Address address : tAddressList) + pkTList.add(processHandlerWrapper.getTPrivateKey(address.getAddressString())); + + List pkZList = new ArrayList(); + if(isTwoKindOfAddresses) + { + for(Address address : zAddressList) + pkZList.add(processHandlerWrapper.getZPrivateKey(address.getAddressString())); + } + + try + { + FileOutputStream outputStream = new FileOutputStream(directory + "/cryptoBackup.txt"); + OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream, "UTF-8"); + BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter); + + for(int i = 0; i < tAddressList.size(); i++) + { + bufferedWriter.write(tAddressList.get(i).getAddressString() + "-" + pkTList.get(i)); + bufferedWriter.newLine(); + } + + if(isTwoKindOfAddresses) + { + for(int i = 0; i < zAddressList.size(); i++) + { + bufferedWriter.write(zAddressList.get(i).getAddressString() + "-" + pkZList.get(i)); + bufferedWriter.newLine(); + } + } + + bufferedWriter.close(); + } + catch(Exception e) + { + e.printStackTrace(); + } + } + } + + + /* + * Listener of the menu item. + * The file to read the private keys from, should have the following format for each line: + * *- (1) + * z- (2) + * + * (1) If the wallet allows just transparent addresses and they start (for example) with a 3, the file should have the format *- too, + * where '*' means everything different to 'z'. + * (2) For private addresses like in BitcoinZ, ZCash, ZenCash, etc. + */ + @FXML + protected void importAddresses(ActionEvent event) + { + //boolean exit = false; + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle(stringProperties.getProperty("import.address.file.chooser.title").replace("${coin.code}", walletProperties.getProperty("coin.code"))); + File file = fileChooser.showOpenDialog(stage); + if(file != null) + { + isImportingAddresses = true; + + // initialize a service to load the addresses read from the file, + // but it doesn't block the GUI. It will return the results in the future, when they are ready. + ImportService importService = new ImportService(file); + importService.setOnSucceeded(e -> isImportingAddresses = false); + importService.start(); + + + MessagePopup mp = new MessagePopup(stage, stringProperties.getProperty("popup.wait.message.title"), stringProperties.getProperty("popup.wait.message.first.line"), stringProperties.getProperty("popup.wait.message.second.line")); + mp.display(); + } + } + + + /* + * Listener of the donate button. + */ + @FXML + protected void donate(MouseEvent event) + { + destinationAddress.setText(walletProperties.getProperty("donate.address")); + if(isCorrectSettingsBeforeSend()) + { + arrow.setVisible(true); + donateLabel.setVisible(true); + } + } + + /* + * Listener of the send button. + */ + @FXML + protected void sendMoney(ActionEvent event) + { + try + { + if(isCorrectSettingsBeforeSend()) + { + + Stage popup = new Stage(); + popup.initModality(Modality.WINDOW_MODAL); + popup.initOwner(stage); + + FXMLLoader fxmlLoader = null; + fxmlLoader = new FXMLLoader(getClass().getResource("messageWithCancelPopup.fxml")); + + SendMoneyController addressController = new SendMoneyController(popup, stringProperties, addressTable.getItems().get(addressTable.getSelectionModel().getSelectedIndex()).getAddressString(), destinationAddress.getText(), amountToSend.getText()); + + addressTable.getSelectionModel().clearSelection(); + destinationAddress.clear(); + amountToSend.clear(); + + + fxmlLoader.setController(addressController); + Pane mainPane = (Pane)fxmlLoader.load(); + mainPane.getStylesheets().add(WalletController.class.getClassLoader().getResource("resources/messagePopupStyle.css").toExternalForm()); + + Scene scene = new Scene(mainPane); + popup.setTitle(stringProperties.getProperty("popup.send.money.title")); + popup.setScene(scene); + popup.show(); + + } + } + catch(Exception e) + { + e.printStackTrace(); + } + } + + /* + * Create and display a popup window with useful information. + */ + private void createAndDisplayPopupMessage(String title, String firstLine, String secondLine) + { + MessagePopup mp = new MessagePopup(stage, title, firstLine, secondLine); + mp.display(); + } + + /* + * Verify the correct set up before to donate or to send money. + */ + private boolean isCorrectSettingsBeforeSend() + { + String regex = "[0-9]+(.[0-9]+)?"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(amountToSend.getText()); + + double addressBalance = 0.0; + if(addressTable.getSelectionModel().getSelectedIndex() >= 0) + { + String stringBalance = addressTable.getItems().get(addressTable.getSelectionModel().getSelectedIndex()).getBalance(); + addressBalance = Double.parseDouble(stringBalance); + } + + + if(addressTable.getSelectionModel().getSelectedIndex() < 0 || addressTable.getItems().get(addressTable.getSelectionModel().getSelectedIndex()).getBalance().equals("0.0")) + createAndDisplayPopupMessage(stringProperties.getProperty("popup.not.selected.address.title"), stringProperties.getProperty("popup.not.selected.address.first.line"), stringProperties.getProperty("popup.not.selected.address.second.line")); + else if(destinationAddress.getText().equals("")) + createAndDisplayPopupMessage(stringProperties.getProperty("popup.not.destination.address.title"), stringProperties.getProperty("popup.not.destination.address.first.line"), stringProperties.getProperty("popup.not.destination.address.second.line").replace("${coin.code}", stringProperties.getProperty("coin.code"))); + else if(amountToSend.getText().equals("")) + createAndDisplayPopupMessage(stringProperties.getProperty("popup.not.amount.to.send.title"), stringProperties.getProperty("popup.not.amount.to.send.first.line"), stringProperties.getProperty("popup.not.amount.to.send.second.line")); + else if(!matcher.matches()) + createAndDisplayPopupMessage(stringProperties.getProperty("popup.not.valid.amount.to.send.title"), stringProperties.getProperty("popup.not.valid.amount.to.send.first.line"), stringProperties.getProperty("popup.not.valid.amount.to.send.second.line")); + else if(addressBalance < Double.parseDouble(amountToSend.getText())) + createAndDisplayPopupMessage(stringProperties.getProperty("popup.not.enough.title"), stringProperties.getProperty("popup.not.enough.first.line"), stringProperties.getProperty("popup.not.enough.second.line").replace("${coin.code}", stringProperties.getProperty("coin.code"))); + else if(destinationAddress.getText().equals(addressTable.getItems().get(addressTable.getSelectionModel().getSelectedIndex()).getAddressString())) + createAndDisplayPopupMessage(stringProperties.getProperty("popup.same.address.title"), stringProperties.getProperty("popup.same.address.first.line"), stringProperties.getProperty("popup.same.address.second.line")); + else + return true; + + return false; + } + + + /* + * Release resources before to close the application. + */ + @FXML + private void exit(ActionEvent event) + { + nodeStarter.releaseResources(); + timer.cancel(); + Platform.exit(); + } +} diff --git a/src/main/java/com/donlaiq/controller/messagePopup.fxml b/src/main/java/com/donlaiq/controller/messagePopup.fxml new file mode 100644 index 0000000..1af4b88 --- /dev/null +++ b/src/main/java/com/donlaiq/controller/messagePopup.fxml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + +