6 changed files with 540 additions and 72 deletions
@ -0,0 +1,270 @@ |
|||||
|
package com.sparrowwallet.sparrow.terminal.wallet; |
||||
|
|
||||
|
import com.googlecode.lanterna.TerminalPosition; |
||||
|
import com.googlecode.lanterna.TerminalSize; |
||||
|
import com.googlecode.lanterna.gui2.*; |
||||
|
import com.googlecode.lanterna.gui2.dialogs.DialogWindow; |
||||
|
import com.sparrowwallet.drongo.policy.Policy; |
||||
|
import com.sparrowwallet.drongo.policy.PolicyType; |
||||
|
import com.sparrowwallet.drongo.protocol.ScriptType; |
||||
|
import com.sparrowwallet.drongo.wallet.DeterministicSeed; |
||||
|
import com.sparrowwallet.drongo.wallet.Keystore; |
||||
|
import com.sparrowwallet.drongo.wallet.Wallet; |
||||
|
import com.sparrowwallet.sparrow.io.Bip39; |
||||
|
import com.sparrowwallet.sparrow.io.ImportException; |
||||
|
import com.sparrowwallet.sparrow.terminal.SparrowTerminal; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
import java.security.NoSuchAlgorithmException; |
||||
|
import java.security.SecureRandom; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.Arrays; |
||||
|
import java.util.List; |
||||
|
|
||||
|
public class Bip39Dialog extends NewWalletDialog { |
||||
|
private static final Logger log = LoggerFactory.getLogger(Bip39Dialog.class); |
||||
|
public static final int MAX_COLUMNS = 40; |
||||
|
|
||||
|
private final Bip39 importer = new Bip39(); |
||||
|
|
||||
|
private Wallet wallet; |
||||
|
|
||||
|
private final String walletName; |
||||
|
private final ComboBox<DisplayScriptType> scriptType; |
||||
|
private final TextBox seedWords; |
||||
|
private final TextBox passphrase; |
||||
|
private final Button createWallet; |
||||
|
|
||||
|
public Bip39Dialog(String walletName) { |
||||
|
super("Create BIP39 Wallet - " + walletName); |
||||
|
|
||||
|
setHints(List.of(Hint.CENTERED)); |
||||
|
|
||||
|
this.walletName = walletName; |
||||
|
|
||||
|
Panel mainPanel = new Panel(); |
||||
|
mainPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(5).setVerticalSpacing(1)); |
||||
|
|
||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE)); |
||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE)); |
||||
|
|
||||
|
mainPanel.addComponent(new Label("Script type")); |
||||
|
scriptType = new ComboBox<>(); |
||||
|
mainPanel.addComponent(scriptType); |
||||
|
|
||||
|
mainPanel.addComponent(new Label("Seed words")); |
||||
|
seedWords = new TextBox(new TerminalSize(MAX_COLUMNS, 5)); |
||||
|
mainPanel.addComponent(seedWords); |
||||
|
|
||||
|
mainPanel.addComponent(new Label("Passphrase")); |
||||
|
passphrase = new TextBox(new TerminalSize(25, 1)); |
||||
|
mainPanel.addComponent(passphrase); |
||||
|
|
||||
|
Panel buttonPanel = new Panel(); |
||||
|
buttonPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(1)); |
||||
|
buttonPanel.addComponent(new Button("Cancel", this::onCancel)); |
||||
|
createWallet = new Button("Create Wallet", this::createWallet).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.CENTER, GridLayout.Alignment.CENTER, true, false)); |
||||
|
createWallet.setEnabled(false); |
||||
|
buttonPanel.addComponent(createWallet); |
||||
|
|
||||
|
mainPanel.addComponent(new Button("Generate New", () -> generateNew())); |
||||
|
|
||||
|
buttonPanel.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER,false,false)).addTo(mainPanel); |
||||
|
setComponent(mainPanel); |
||||
|
|
||||
|
ScriptType.getAddressableScriptTypes(PolicyType.SINGLE).stream().map(DisplayScriptType::new).forEach(scriptType::addItem); |
||||
|
scriptType.setSelectedItem(new DisplayScriptType(ScriptType.P2WPKH)); |
||||
|
|
||||
|
seedWords.setTextChangeListener((newText, changedByUserInteraction) -> { |
||||
|
try { |
||||
|
String[] words = newText.split("[ \n]"); |
||||
|
importer.getKeystore(scriptType.getSelectedItem().scriptType.getDefaultDerivation(), Arrays.asList(words), passphrase.getText()); |
||||
|
createWallet.setEnabled(true); |
||||
|
} catch(ImportException e) { |
||||
|
createWallet.setEnabled(false); |
||||
|
} |
||||
|
|
||||
|
if(changedByUserInteraction) { |
||||
|
List<String> lines = splitString(newText, MAX_COLUMNS); |
||||
|
String splitText = lines.stream().reduce((s1, s2) -> s1 + "\n" + s2).get(); |
||||
|
if(!newText.equals(splitText)) { |
||||
|
seedWords.setText(splitText); |
||||
|
|
||||
|
TerminalPosition pos = seedWords.getCaretPosition(); |
||||
|
if(pos.getRow() == lines.size() - 2 && pos.getColumn() == lines.get(lines.size() - 2).length()) { |
||||
|
seedWords.setCaretPosition(lines.size() - 1, lines.get(lines.size() - 1).length()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private void generateNew() { |
||||
|
WordNumberDialog wordNumberDialog = new WordNumberDialog(); |
||||
|
Integer numberOfWords = wordNumberDialog.showDialog(SparrowTerminal.get().getGui()); |
||||
|
|
||||
|
if(numberOfWords != null) { |
||||
|
int mnemonicSeedLength = numberOfWords * 11; |
||||
|
int entropyLength = mnemonicSeedLength - (mnemonicSeedLength/33); |
||||
|
|
||||
|
SecureRandom secureRandom; |
||||
|
try { |
||||
|
secureRandom = SecureRandom.getInstanceStrong(); |
||||
|
} catch(NoSuchAlgorithmException e) { |
||||
|
secureRandom = new SecureRandom(); |
||||
|
} |
||||
|
|
||||
|
DeterministicSeed deterministicSeed = new DeterministicSeed(secureRandom, entropyLength, ""); |
||||
|
List<String> words = deterministicSeed.getMnemonicCode(); |
||||
|
setWords(words); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private List<String> getWords() { |
||||
|
return List.of(seedWords.getText().split("[ \n]")); |
||||
|
} |
||||
|
|
||||
|
private void setWords(List<String> words) { |
||||
|
String text = words.stream().reduce((s1, s2) -> s1 + " " + s2).get(); |
||||
|
List<String> splitText = splitString(text, MAX_COLUMNS); |
||||
|
seedWords.setText(splitText.stream().reduce((s1, s2) -> s1 + "\n" + s2).get()); |
||||
|
} |
||||
|
|
||||
|
public static List<String> splitString(String stringToSplit, int maxLength) { |
||||
|
int splitLength = maxLength - 1; |
||||
|
String text = stringToSplit; |
||||
|
List<String> lines = new ArrayList<>(); |
||||
|
while (text.length() > splitLength) { |
||||
|
int spaceAt = splitLength - 1; |
||||
|
// the text is too long.
|
||||
|
// find the last space before the maxLength
|
||||
|
for (int i = splitLength - 1; i > 0; i--) { |
||||
|
if (Character.isWhitespace(text.charAt(i))) { |
||||
|
spaceAt = i; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
lines.add(text.substring(0, spaceAt)); |
||||
|
text = text.substring(spaceAt + 1); |
||||
|
} |
||||
|
lines.add(text); |
||||
|
return lines; |
||||
|
} |
||||
|
|
||||
|
private void createWallet() { |
||||
|
close(); |
||||
|
|
||||
|
try { |
||||
|
wallet = getWallet(); |
||||
|
discoverAndSaveWallet(wallet); |
||||
|
} catch(ImportException e) { |
||||
|
log.error("Cannot import wallet", e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private Wallet getWallet() throws ImportException { |
||||
|
Wallet wallet = new Wallet(walletName); |
||||
|
wallet.setPolicyType(PolicyType.SINGLE); |
||||
|
wallet.setScriptType(scriptType.getSelectedItem().scriptType); |
||||
|
Keystore keystore = importer.getKeystore(wallet.getScriptType().getDefaultDerivation(), getWords(), passphrase.getText()); |
||||
|
wallet.getKeystores().add(keystore); |
||||
|
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, wallet.getScriptType(), wallet.getKeystores(), 1)); |
||||
|
return wallet; |
||||
|
} |
||||
|
|
||||
|
private void onCancel() { |
||||
|
close(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Wallet showDialog(WindowBasedTextGUI textGUI) { |
||||
|
super.showDialog(textGUI); |
||||
|
return wallet; |
||||
|
} |
||||
|
|
||||
|
private static final class DisplayScriptType { |
||||
|
private final ScriptType scriptType; |
||||
|
|
||||
|
public DisplayScriptType(ScriptType scriptType) { |
||||
|
this.scriptType = scriptType; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String toString() { |
||||
|
return scriptType.getDescription(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean equals(Object o) { |
||||
|
if(this == o) { |
||||
|
return true; |
||||
|
} |
||||
|
if(o == null || getClass() != o.getClass()) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
DisplayScriptType that = (DisplayScriptType) o; |
||||
|
|
||||
|
return scriptType == that.scriptType; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public int hashCode() { |
||||
|
return scriptType.hashCode(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static final class WordNumberDialog extends DialogWindow { |
||||
|
ComboBox<Integer> wordCount; |
||||
|
private Integer numberOfWords; |
||||
|
|
||||
|
public WordNumberDialog() { |
||||
|
super("Generate Seed Words"); |
||||
|
|
||||
|
setHints(List.of(Hint.CENTERED)); |
||||
|
|
||||
|
Panel mainPanel = new Panel(); |
||||
|
mainPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(5).setVerticalSpacing(1)); |
||||
|
|
||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE)); |
||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE)); |
||||
|
|
||||
|
mainPanel.addComponent(new Label("Number of words")); |
||||
|
wordCount = new ComboBox<>(); |
||||
|
mainPanel.addComponent(wordCount); |
||||
|
|
||||
|
wordCount.addItem(24); |
||||
|
wordCount.addItem(21); |
||||
|
wordCount.addItem(18); |
||||
|
wordCount.addItem(15); |
||||
|
wordCount.addItem(12); |
||||
|
|
||||
|
Panel buttonPanel = new Panel(); |
||||
|
buttonPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(1)); |
||||
|
buttonPanel.addComponent(new Button("Cancel", this::onCancel)); |
||||
|
Button okButton = new Button("Ok", this::onOk).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.CENTER, GridLayout.Alignment.CENTER, true, false)); |
||||
|
buttonPanel.addComponent(okButton); |
||||
|
|
||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE)); |
||||
|
|
||||
|
buttonPanel.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER,false,false)).addTo(mainPanel); |
||||
|
setComponent(mainPanel); |
||||
|
} |
||||
|
|
||||
|
private void onOk() { |
||||
|
numberOfWords = wordCount.getSelectedItem(); |
||||
|
close(); |
||||
|
} |
||||
|
|
||||
|
private void onCancel() { |
||||
|
close(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Integer showDialog(WindowBasedTextGUI textGUI) { |
||||
|
super.showDialog(textGUI); |
||||
|
return numberOfWords; |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,163 @@ |
|||||
|
package com.sparrowwallet.sparrow.terminal.wallet; |
||||
|
|
||||
|
import com.googlecode.lanterna.TerminalSize; |
||||
|
import com.googlecode.lanterna.gui2.EmptySpace; |
||||
|
import com.googlecode.lanterna.gui2.Label; |
||||
|
import com.googlecode.lanterna.gui2.LinearLayout; |
||||
|
import com.googlecode.lanterna.gui2.Panel; |
||||
|
import com.googlecode.lanterna.gui2.dialogs.DialogWindow; |
||||
|
import com.googlecode.lanterna.gui2.dialogs.TextInputDialogBuilder; |
||||
|
import com.sparrowwallet.drongo.SecureString; |
||||
|
import com.sparrowwallet.drongo.crypto.ECKey; |
||||
|
import com.sparrowwallet.drongo.crypto.EncryptionType; |
||||
|
import com.sparrowwallet.drongo.crypto.Key; |
||||
|
import com.sparrowwallet.drongo.wallet.MnemonicException; |
||||
|
import com.sparrowwallet.drongo.wallet.Wallet; |
||||
|
import com.sparrowwallet.sparrow.AppServices; |
||||
|
import com.sparrowwallet.sparrow.EventManager; |
||||
|
import com.sparrowwallet.sparrow.event.StorageEvent; |
||||
|
import com.sparrowwallet.sparrow.event.TimedEvent; |
||||
|
import com.sparrowwallet.sparrow.io.Storage; |
||||
|
import com.sparrowwallet.sparrow.io.StorageException; |
||||
|
import com.sparrowwallet.sparrow.net.ElectrumServer; |
||||
|
import com.sparrowwallet.sparrow.terminal.SparrowTerminal; |
||||
|
import javafx.application.Platform; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
|
||||
|
import java.io.IOException; |
||||
|
import java.util.List; |
||||
|
|
||||
|
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog; |
||||
|
|
||||
|
public class NewWalletDialog extends DialogWindow { |
||||
|
private static final Logger log = LoggerFactory.getLogger(NewWalletDialog.class); |
||||
|
|
||||
|
public NewWalletDialog(String title) { |
||||
|
super(title); |
||||
|
} |
||||
|
|
||||
|
protected void discoverAndSaveWallet(Wallet wallet) { |
||||
|
if(AppServices.onlineProperty().get()) { |
||||
|
discoverAccounts(wallet); |
||||
|
} else { |
||||
|
saveWallet(wallet); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void discoverAccounts(Wallet wallet) { |
||||
|
DiscoveringDialog discoveringDialog = new DiscoveringDialog(wallet); |
||||
|
SparrowTerminal.get().getGui().addWindow(discoveringDialog); |
||||
|
|
||||
|
Platform.runLater(() -> { |
||||
|
ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(List.of(wallet)); |
||||
|
walletDiscoveryService.setOnSucceeded(successEvent -> { |
||||
|
SparrowTerminal.get().getGuiThread().invokeLater(() -> { |
||||
|
SparrowTerminal.get().getGui().removeWindow(discoveringDialog); |
||||
|
saveWallet(wallet); |
||||
|
}); |
||||
|
}); |
||||
|
walletDiscoveryService.setOnFailed(failedEvent -> { |
||||
|
SparrowTerminal.get().getGuiThread().invokeLater(() -> { |
||||
|
SparrowTerminal.get().getGui().removeWindow(discoveringDialog); |
||||
|
saveWallet(wallet); |
||||
|
}); |
||||
|
log.error("Failed to discover accounts", failedEvent.getSource().getException()); |
||||
|
}); |
||||
|
walletDiscoveryService.start(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private void saveWallet(Wallet wallet) { |
||||
|
Storage storage = new Storage(Storage.getWalletFile(wallet.getName())); |
||||
|
|
||||
|
TextInputDialogBuilder builder = new TextInputDialogBuilder().setTitle("Wallet Password"); |
||||
|
builder.setDescription(SettingsDialog.PasswordRequirement.UPDATE_NEW.getDescription()); |
||||
|
builder.setPasswordInput(true); |
||||
|
|
||||
|
String password = builder.build().showDialog(SparrowTerminal.get().getGui()); |
||||
|
if(password != null) { |
||||
|
Platform.runLater(() -> { |
||||
|
if(password.length() == 0) { |
||||
|
try { |
||||
|
storage.setEncryptionPubKey(Storage.NO_PASSWORD_KEY); |
||||
|
storage.saveWallet(wallet); |
||||
|
storage.restorePublicKeysFromSeed(wallet, null); |
||||
|
SparrowTerminal.addWallet(storage, wallet); |
||||
|
|
||||
|
for(Wallet childWallet : wallet.getChildWallets()) { |
||||
|
storage.saveWallet(childWallet); |
||||
|
storage.restorePublicKeysFromSeed(childWallet, null); |
||||
|
SparrowTerminal.addWallet(storage, childWallet); |
||||
|
} |
||||
|
|
||||
|
SparrowTerminal.get().getGuiThread().invokeLater(() -> LoadWallet.getOpeningDialog(storage, wallet).showDialog(SparrowTerminal.get().getGui())); |
||||
|
} catch(IOException | StorageException | MnemonicException e) { |
||||
|
log.error("Error saving imported wallet", e); |
||||
|
} |
||||
|
} else { |
||||
|
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, new SecureString(password)); |
||||
|
keyDerivationService.setOnSucceeded(workerStateEvent -> { |
||||
|
EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()).getAbsolutePath(), TimedEvent.Action.END, "Done")); |
||||
|
ECKey encryptionFullKey = keyDerivationService.getValue(); |
||||
|
Key key = null; |
||||
|
|
||||
|
try { |
||||
|
ECKey encryptionPubKey = ECKey.fromPublicOnly(encryptionFullKey); |
||||
|
key = new Key(encryptionFullKey.getPrivKeyBytes(), storage.getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2); |
||||
|
wallet.encrypt(key); |
||||
|
storage.setEncryptionPubKey(encryptionPubKey); |
||||
|
storage.saveWallet(wallet); |
||||
|
storage.restorePublicKeysFromSeed(wallet, key); |
||||
|
SparrowTerminal.addWallet(storage, wallet); |
||||
|
|
||||
|
for(Wallet childWallet : wallet.getChildWallets()) { |
||||
|
if(!childWallet.isNested()) { |
||||
|
childWallet.encrypt(key); |
||||
|
} |
||||
|
storage.saveWallet(childWallet); |
||||
|
storage.restorePublicKeysFromSeed(childWallet, key); |
||||
|
SparrowTerminal.addWallet(storage, childWallet); |
||||
|
} |
||||
|
|
||||
|
SparrowTerminal.get().getGuiThread().invokeLater(() -> LoadWallet.getOpeningDialog(storage, wallet).showDialog(SparrowTerminal.get().getGui())); |
||||
|
} catch(IOException | StorageException | MnemonicException e) { |
||||
|
log.error("Error saving imported wallet", e); |
||||
|
} finally { |
||||
|
encryptionFullKey.clear(); |
||||
|
if(key != null) { |
||||
|
key.clear(); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
keyDerivationService.setOnFailed(workerStateEvent -> { |
||||
|
EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()).getAbsolutePath(), TimedEvent.Action.END, "Failed")); |
||||
|
showErrorDialog("Error encrypting wallet", keyDerivationService.getException().getMessage()); |
||||
|
}); |
||||
|
EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()).getAbsolutePath(), TimedEvent.Action.START, "Encrypting wallet...")); |
||||
|
keyDerivationService.start(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static final class DiscoveringDialog extends DialogWindow { |
||||
|
public DiscoveringDialog(Wallet wallet) { |
||||
|
super(wallet.getName()); |
||||
|
|
||||
|
setHints(List.of(Hint.CENTERED)); |
||||
|
setFixedSize(new TerminalSize(30, 5)); |
||||
|
|
||||
|
Panel mainPanel = new Panel(); |
||||
|
mainPanel.setLayoutManager(new LinearLayout()); |
||||
|
mainPanel.addComponent(new EmptySpace(), LinearLayout.createLayoutData(LinearLayout.Alignment.Beginning, LinearLayout.GrowPolicy.CanGrow)); |
||||
|
|
||||
|
Label label = new Label("Discovering Accounts..."); |
||||
|
mainPanel.addComponent(label, LinearLayout.createLayoutData(LinearLayout.Alignment.Center)); |
||||
|
|
||||
|
mainPanel.addComponent(new EmptySpace(), LinearLayout.createLayoutData(LinearLayout.Alignment.Beginning, LinearLayout.GrowPolicy.CanGrow)); |
||||
|
|
||||
|
setComponent(mainPanel); |
||||
|
} |
||||
|
} |
||||
|
} |
Loading…
Reference in new issue