Update wallet UI to use PaperclipChain

- Remove Ethereum/Web3 references from HTML
- Create PaperclipWallets class with Ed25519 key support
- Add PaperclipDatabase for wallet storage
- Update module loading order in index.html
- Convert button classes from btn-etho to btn-clips
This commit is contained in:
2025-06-15 18:39:48 -07:00
parent dab604463f
commit 1ceb56c7b8
4 changed files with 604 additions and 39 deletions

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>$ETHO Desktop Wallet</title> <title>PaperclipWallet</title>
<link href="https://fonts.googleapis.com/css2?family=Fira+Mono:wght@400;500;700&amp;display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Fira+Mono:wght@400;500;700&amp;display=swap" rel="stylesheet">
<link rel="stylesheet" href="./assets/styles/materialize.min.css"> <link rel="stylesheet" href="./assets/styles/materialize.min.css">
<link rel="stylesheet" href="./assets/styles/datatables.min.css"> <link rel="stylesheet" href="./assets/styles/datatables.min.css">
@@ -29,7 +29,6 @@
<span id="nodestorage" style="display:none;">Loading</span> <span id="nodestorage" style="display:none;">Loading</span>
<!-- normal script imports etc --> <!-- normal script imports etc -->
<script src="https://cdn.jsdelivr.net/gh/ethereum/web3.js@1.0.0-beta.36/dist/web3.min.js" integrity="sha256-nWBTbvxhJgjslRyuAKJHK+XcZPlCnmIAAMixz6EefVk=" crossorigin="anonymous"></script>
<script src="./assets/scripts/jquery.min.js"></script> <script src="./assets/scripts/jquery.min.js"></script>
<script src='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js'></script> <script src='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js'></script>
<script src="./assets/scripts/handlebars.js"></script> <script src="./assets/scripts/handlebars.js"></script>
@@ -74,10 +73,10 @@
}); });
var timeouts = [ var timeouts = [
{ text: "Starting Node...", delay: 1250 }, { text: "Starting PaperclipChain node...", delay: 1250 },
{ text: "Connecting to Network...", delay: 6400 }, { text: "Connecting to network...", delay: 6400 },
{ text: "Updating...", delay: 7320 }, { text: "Loading wallet...", delay: 7320 },
{ text: "Launching...", delay: 7400 } { text: "Ready", delay: 7400 }
]; ];
timeouts.forEach((item, index) => { timeouts.forEach((item, index) => {
@@ -86,14 +85,16 @@
}, item.delay + Math.random() * 50 * index); }, item.delay + Math.random() * 50 * index);
}); });
$(document).on("onGethReady", function () { $(document).on("onNodeReady", function () {
setTimeout(() => { setTimeout(() => {
loading_screen.finish(); loading_screen.finish();
}, 5500); }, 5500);
}); });
$(window).on("beforeunload", function () { $(window).on("beforeunload", function () {
EthoBlockchain.closeConnection(); if (window.PaperclipChain) {
PaperclipChain.stopConnection();
}
}); });
</script> </script>
@@ -117,7 +118,7 @@
</a> </a>
<a class="item" style="padding-top: 30px; padding-left: 13px" id="mainNavBtnSend" href="#" data-tippy="Send Funds" data-tippy-delay="100"> <a class="item" style="padding-top: 30px; padding-left: 13px" id="mainNavBtnSend" href="#" data-tippy="Send Funds" data-tippy-delay="100">
<i class="fas fa-comment-dollar fa-1x"></i> <i class="fas fa-comment-dollar fa-1x"></i>
<span class="sendEtho"> Send </span> <span class="sendClips"> Send </span>
</a> </a>
<a class="item" style="padding-top: 30px; padding-left: 13px" id="mainNavBtnTransactions" href="#" data-tippy="Transactions" data-tippy-delay="100"> <a class="item" style="padding-top: 30px; padding-left: 13px" id="mainNavBtnTransactions" href="#" data-tippy="Transactions" data-tippy-delay="100">
<i class="fas fa-exchange-alt fa-1x"></i> <i class="fas fa-exchange-alt fa-1x"></i>
@@ -142,17 +143,17 @@
<div id="mainContent"></div> <div id="mainContent"></div>
<div id="syncProgress"></div> <div id="syncProgress"></div>
<script> <script>
// You can also require other files to run in this process // PaperclipChain renderer modules
require('./renderer/about.js'); require('./renderer/paperclip-database.js');
require('./renderer/send.js'); require('./renderer/blockchain.js');
require('./renderer/paperclip-wallets.js');
require('./renderer/utils.js'); require('./renderer/utils.js');
require('./renderer/maingui.js'); require('./renderer/maingui.js');
require('./renderer/about.js');
require('./renderer/send.js');
require('./renderer/syncing.js'); require('./renderer/syncing.js');
require('./renderer/markets.js'); require('./renderer/markets.js');
require('./renderer/settings.js'); require('./renderer/settings.js');
require('./renderer/wallets.js');
require('./renderer/database.js');
require('./renderer/blockchain.js');
require('./renderer/addressBook.js'); require('./renderer/addressBook.js');
require('./renderer/transactions.js'); require('./renderer/transactions.js');
require('./renderer/tableTransactions.js'); require('./renderer/tableTransactions.js');
@@ -165,7 +166,7 @@
<div class="form-group"> <div class="form-group">
<span id="txtGeneralError"></span> <span id="txtGeneralError"></span>
</div> </div>
<button type="button" class="btn btn-etho btn-dialog-confirm" id="btnGeneralErrorOK">OK</button> <button type="button" class="btn btn-clips btn-dialog-confirm" id="btnGeneralErrorOK">OK</button>
</div> </div>
</div> </div>
@@ -175,27 +176,27 @@
<div class="form-group"> <div class="form-group">
<span id="txtGeneralConfirm"></span> <span id="txtGeneralConfirm"></span>
</div> </div>
<button type="button" class="btn btn-etho btn-dialog-cancel" id="btnGeneralConfirmNo">No</button> <button type="button" class="btn btn-clips btn-dialog-cancel" id="btnGeneralConfirmNo">No</button>
<button type="button" class="btn btn-etho btn-dialog-confirm" id="btnGeneralConfirmYes">Yes</button> <button type="button" class="btn btn-clips btn-dialog-confirm" id="btnGeneralConfirmYes">Yes</button>
</div> </div>
</div> </div>
<!-- The modal for about info --> <!-- The modal for about info -->
<div id="dlgAboutInfo" class="modalDialog" data-izimodal-title="About Etho Protocol Wallet" data-izimodal-icon="icon-home"> <div id="dlgAboutInfo" class="modalDialog" data-izimodal-title="About PaperclipWallet" data-izimodal-icon="icon-home">
<div class="modalBody"> <div class="modalBody">
<div class="aboutInfo"> <div class="aboutInfo">
<div class="infoText" id="aboutInfoWallet">Etho Protocol Wallet</div> <div class="infoText" id="aboutInfoWallet">PaperclipWallet</div>
<div class="infoText" id="aboutInfoGitHub">GitHub: <div class="infoText" id="aboutInfoGitHub">GitHub:
<a id="urlOpenGitHub" href="https://github.com/Ether1Project/Ether1DesktopWallet">https://github.com/Ether1Project/Ether1DesktopWallet</a> <a id="urlOpenGitHub" href="https://git.takoyaki.cool/matt/paperclip-wallet">https://git.takoyaki.cool/matt/paperclip-wallet</a>
</div> </div>
<div class="infoText" id="aboutInfoLicence">Made under <div class="infoText" id="aboutInfoLicence">Made under
<a id="urlOpenLicence" href="https://choosealicense.com/licenses/gpl-3.0">GPL v3.0</a> <a id="urlOpenLicence" href="https://choosealicense.com/licenses/cc0-1.0">CC0 1.0</a>
licence licence
</div> </div>
<div class="infoText" id="aboutInfoVersion">Version: <div class="infoText" id="aboutInfoVersion">Version:
<span id="versionNumber"></span></div> <span id="versionNumber"></span></div>
</div> </div>
<button type="button" class="btn btn-etho btn-dialog-confirm" id="btnAboutInfoClose">Close</button> <button type="button" class="btn btn-clips btn-dialog-confirm" id="btnAboutInfoClose">Close</button>
</div> </div>
</div> </div>
</body> </body>
@@ -204,7 +205,7 @@
<div id="dlgShowAddressQRCode" class="modalDialog" data-izimodal-title="Address QR-Code" data-izimodal-subtitle="Scan the QR-Code to get the address..." data-izimodal-icon="icon-home"> <div id="dlgShowAddressQRCode" class="modalDialog" data-izimodal-title="Address QR-Code" data-izimodal-subtitle="Scan the QR-Code to get the address..." data-izimodal-icon="icon-home">
<div class="modalBody"> <div class="modalBody">
<div id="addrQRCode"></div> <div id="addrQRCode"></div>
<button type="button" class="btn btn-etho btn-dialog-confirm" id="btnScanQRCodeClose">Close</button> <button type="button" class="btn btn-clips btn-dialog-confirm" id="btnScanQRCodeClose">Close</button>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,296 @@
const storage = require('electron-storage');
const path = require('path');
class PaperclipDatabase {
constructor() {
this.walletFile = 'paperclip-wallets.json';
this.settingsFile = 'paperclip-settings.json';
}
// Wallet operations
getWallets() {
try {
const data = storage.getSync(this.walletFile);
return data || { addresses: [], names: {}, keys: {} };
} catch (error) {
return { addresses: [], names: {}, keys: {} };
}
}
saveWallet(wallet) {
try {
const wallets = this.getWallets();
// Add address if not exists
if (!wallets.addresses.includes(wallet.address)) {
wallets.addresses.push(wallet.address);
}
// Save wallet data
wallets.names[wallet.address] = wallet.name;
wallets.keys[wallet.address] = {
publicKey: wallet.publicKey,
privateKey: wallet.privateKey, // TODO: Encrypt with password
created: wallet.created
};
storage.setSync(this.walletFile, wallets);
return true;
} catch (error) {
console.error('Failed to save wallet:', error);
return false;
}
}
getWallet(address) {
try {
const wallets = this.getWallets();
if (!wallets.addresses.includes(address)) {
return null;
}
return {
address: address,
name: wallets.names[address] || 'Account',
publicKey: wallets.keys[address]?.publicKey,
privateKey: wallets.keys[address]?.privateKey,
created: wallets.keys[address]?.created
};
} catch (error) {
console.error('Failed to get wallet:', error);
return null;
}
}
removeWallet(address) {
try {
const wallets = this.getWallets();
// Remove from addresses array
const index = wallets.addresses.indexOf(address);
if (index > -1) {
wallets.addresses.splice(index, 1);
}
// Remove wallet data
delete wallets.names[address];
delete wallets.keys[address];
storage.setSync(this.walletFile, wallets);
return true;
} catch (error) {
console.error('Failed to remove wallet:', error);
return false;
}
}
updateWalletName(address, newName) {
try {
const wallets = this.getWallets();
if (wallets.addresses.includes(address)) {
wallets.names[address] = newName;
storage.setSync(this.walletFile, wallets);
return true;
}
return false;
} catch (error) {
console.error('Failed to update wallet name:', error);
return false;
}
}
// Address book operations
getAddressBook() {
try {
const data = storage.getSync('address-book.json');
return data || [];
} catch (error) {
return [];
}
}
saveAddressBookEntry(entry) {
try {
const addressBook = this.getAddressBook();
// Check if address already exists
const existingIndex = addressBook.findIndex(item => item.address === entry.address);
if (existingIndex > -1) {
// Update existing entry
addressBook[existingIndex] = entry;
} else {
// Add new entry
addressBook.push(entry);
}
storage.setSync('address-book.json', addressBook);
return true;
} catch (error) {
console.error('Failed to save address book entry:', error);
return false;
}
}
removeAddressBookEntry(address) {
try {
const addressBook = this.getAddressBook();
const filtered = addressBook.filter(item => item.address !== address);
storage.setSync('address-book.json', filtered);
return true;
} catch (error) {
console.error('Failed to remove address book entry:', error);
return false;
}
}
// Settings operations
getSettings() {
try {
const data = storage.getSync(this.settingsFile);
return data || {
rpcUrl: 'http://localhost:26657',
autoConnect: true,
notifications: true
};
} catch (error) {
return {
rpcUrl: 'http://localhost:26657',
autoConnect: true,
notifications: true
};
}
}
saveSetting(key, value) {
try {
const settings = this.getSettings();
settings[key] = value;
storage.setSync(this.settingsFile, settings);
return true;
} catch (error) {
console.error('Failed to save setting:', error);
return false;
}
}
saveSettings(settingsObj) {
try {
storage.setSync(this.settingsFile, settingsObj);
return true;
} catch (error) {
console.error('Failed to save settings:', error);
return false;
}
}
// Transaction history (local cache)
getTransactionHistory(address) {
try {
const data = storage.getSync(`tx-history-${address}.json`);
return data || [];
} catch (error) {
return [];
}
}
saveTransaction(address, transaction) {
try {
const history = this.getTransactionHistory(address);
// Check if transaction already exists
const exists = history.some(tx => tx.hash === transaction.hash);
if (!exists) {
history.unshift(transaction); // Add to beginning
// Keep only last 100 transactions
if (history.length > 100) {
history.splice(100);
}
storage.setSync(`tx-history-${address}.json`, history);
}
return true;
} catch (error) {
console.error('Failed to save transaction:', error);
return false;
}
}
// Utility methods
clearAllData() {
try {
storage.removeSync(this.walletFile);
storage.removeSync(this.settingsFile);
storage.removeSync('address-book.json');
// Clear transaction histories
const wallets = this.getWallets();
wallets.addresses.forEach(address => {
try {
storage.removeSync(`tx-history-${address}.json`);
} catch (e) {
// Ignore errors for non-existent files
}
});
return true;
} catch (error) {
console.error('Failed to clear data:', error);
return false;
}
}
exportWallets() {
try {
const wallets = this.getWallets();
const settings = this.getSettings();
const addressBook = this.getAddressBook();
return {
wallets: wallets,
settings: settings,
addressBook: addressBook,
exportDate: new Date().toISOString()
};
} catch (error) {
console.error('Failed to export data:', error);
return null;
}
}
importWallets(data) {
try {
if (data.wallets) {
storage.setSync(this.walletFile, data.wallets);
}
if (data.settings) {
storage.setSync(this.settingsFile, data.settings);
}
if (data.addressBook) {
storage.setSync('address-book.json', data.addressBook);
}
return true;
} catch (error) {
console.error('Failed to import data:', error);
return false;
}
}
}
// Create global instance
const PaperclipDatabase = new PaperclipDatabase();
// Make it available globally
window.PaperclipDatabase = PaperclipDatabase;
module.exports = PaperclipDatabase;

View File

@@ -0,0 +1,271 @@
const {ipcRenderer} = require("electron");
class PaperclipWallets {
constructor() {
this.addressList = [];
this.price = 0;
}
_getPrice() {
return this.price;
}
_setPrice(price) {
this.price = price;
}
getAddressList() {
return this.addressList;
}
clearAddressList() {
this.addressList = [];
}
getAddressExists(address) {
if (address) {
return this.addressList.indexOf(address) > -1;
}
return false;
}
addAddressToList(address) {
if (address) {
this.addressList.push(address);
}
}
async createNewWallet(password) {
try {
const ClipsCrypto = require('../modules/clips-crypto.js');
const keyPair = ClipsCrypto.generateKeyPair();
// Store wallet in database
const wallet = {
address: keyPair.address,
publicKey: keyPair.publicKey,
privateKey: keyPair.privateKey, // In production, encrypt this with password
name: `Account ${this.addressList.length + 1}`,
created: Date.now()
};
PaperclipDatabase.saveWallet(wallet);
this.addAddressToList(keyPair.address);
return wallet;
} catch (error) {
throw new Error(`Failed to create wallet: ${error.message}`);
}
}
async importFromPrivateKey(privateKeyHex, password) {
try {
const ClipsCrypto = require('../modules/clips-crypto.js');
const keyPair = ClipsCrypto.importPrivateKey(privateKeyHex);
const wallet = {
address: keyPair.address,
publicKey: keyPair.publicKey,
privateKey: keyPair.privateKey,
name: `Imported Account`,
created: Date.now()
};
PaperclipDatabase.saveWallet(wallet);
this.addAddressToList(keyPair.address);
return wallet;
} catch (error) {
throw new Error(`Failed to import wallet: ${error.message}`);
}
}
async sendTransaction(fromAddress, toAddress, amount, password) {
try {
// Get wallet data
const wallet = PaperclipDatabase.getWallet(fromAddress);
if (!wallet) {
throw new Error("Wallet not found");
}
// Validate address format
const ClipsCrypto = require('../modules/clips-crypto.js');
if (!ClipsCrypto.isValidAddress(toAddress)) {
throw new Error("Invalid recipient address");
}
// Get current nonce
const nonce = await ipcRenderer.invoke("paperclip-get-nonce", fromAddress);
// Create and sign transaction
const transaction = ClipsCrypto.createTransaction({
sender: fromAddress,
receiver: toAddress,
amount: parseInt(amount),
nonce: nonce,
type: 'transfer',
data: '',
privateKey: wallet.privateKey
});
// Broadcast transaction
const result = await ipcRenderer.invoke("paperclip-broadcast-tx",
ClipsCrypto.transactionToHex(transaction));
if (result.success) {
return {
hash: result.hash,
height: result.height
};
} else {
throw new Error(result.error || "Transaction failed");
}
} catch (error) {
throw new Error(`Transaction failed: ${error.message}`);
}
}
async getWalletBalance(address) {
try {
const balance = await ipcRenderer.invoke("paperclip-get-balance", address);
return balance;
} catch (error) {
console.error(`Failed to get balance for ${address}:`, error);
return 0;
}
}
async refreshWalletData() {
try {
this.clearAddressList();
const wallets = PaperclipDatabase.getWallets();
if (!wallets || !wallets.addresses) {
return {
sumBalance: 0,
addressData: [],
sumBalanceFormatted: "0 CLIPS"
};
}
const data = {
sumBalance: 0,
addressData: []
};
// Get balance for each wallet
for (const address of wallets.addresses) {
const wallet = PaperclipDatabase.getWallet(address);
const balance = await this.getWalletBalance(address);
data.addressData.push({
address: address,
name: wallet ? wallet.name : `Account`,
balance: balance,
balanceFormatted: this.formatBalance(balance)
});
data.sumBalance += balance;
this.addAddressToList(address);
}
data.sumBalanceFormatted = this.formatBalance(data.sumBalance);
return data;
} catch (error) {
throw new Error(`Failed to refresh wallet data: ${error.message}`);
}
}
formatBalance(balance) {
if (balance === 0) {
return "0 CLIPS";
}
if (balance >= 1000000) {
return (balance / 1000000).toFixed(2) + "M CLIPS";
} else if (balance >= 1000) {
return (balance / 1000).toFixed(2) + "K CLIPS";
} else {
return balance.toLocaleString() + " CLIPS";
}
}
validateAddress(address) {
const ClipsCrypto = require('../modules/clips-crypto.js');
return ClipsCrypto.isValidAddress(address);
}
validateAmount(amount) {
const num = parseFloat(amount);
return !isNaN(num) && num > 0;
}
// UI Event Handlers
async handleCreateNewWallet() {
const password = $("#walletPasswordFirst").val();
if (!password) {
throw new Error("Password cannot be empty");
}
if (password !== $("#walletPasswordSecond").val()) {
throw new Error("Passwords do not match");
}
return await this.createNewWallet(password);
}
async handleImportPrivateKey() {
const privateKey = $("#inputPrivateKey").val();
const password = $("#keyPasswordFirst").val();
if (!privateKey) {
throw new Error("Private key cannot be empty");
}
if (!password) {
throw new Error("Password cannot be empty");
}
if (password !== $("#keyPasswordSecond").val()) {
throw new Error("Passwords do not match");
}
return await this.importFromPrivateKey(privateKey, password);
}
async handleSendTransaction() {
const fromAddress = $("#sendFromAddress").val();
const toAddress = $("#sendToAddress").val();
const amount = $("#sendAmount").val();
const password = $("#sendPassword").val();
if (!fromAddress) {
throw new Error("From address is required");
}
if (!toAddress) {
throw new Error("To address is required");
}
if (!this.validateAddress(toAddress)) {
throw new Error("Invalid recipient address format");
}
if (!this.validateAmount(amount)) {
throw new Error("Invalid amount");
}
if (!password) {
throw new Error("Password is required");
}
return await this.sendTransaction(fromAddress, toAddress, amount, password);
}
}
// Create global instance
const ClipsWallet = new PaperclipWallets();
// Make it available globally
window.ClipsWallet = ClipsWallet;

View File

@@ -1,12 +1,9 @@
const {ipcRenderer} = require("electron"); const {ipcRenderer} = require("electron");
class Wallets { class PaperclipWallets {
constructor() { constructor() {
this.addressList = []; this.addressList = [];
this.price = 0; // CLIPS price placeholder
$.getJSON("https://min-api.cryptocompare.com/data/price?fsym=ETHO&tsyms=USD", function (price) {
EthoWallets._setPrice(price.USD);
});
} }
_getPrice() { _getPrice() {
@@ -35,32 +32,32 @@ class Wallets {
addAddressToList(address) { addAddressToList(address) {
if (address) { if (address) {
this.addressList.push(address.toLowerCase()); this.addressList.push(address); // CLIPS addresses are case-sensitive
} }
} }
enableButtonTooltips() { enableButtonTooltips() {
EthoUtils.createToolTip("#btnNewAddress", "Create New Address"); PaperclipUtils.createToolTip("#btnNewAddress", "Create New Address");
EthoUtils.createToolTip("#btnRefreshAddress", "Refresh Address List"); PaperclipUtils.createToolTip("#btnRefreshAddress", "Refresh Address List");
EthoUtils.createToolTip("#btnExportAccounts", "Export Accounts"); PaperclipUtils.createToolTip("#btnExportAccounts", "Export Accounts");
EthoUtils.createToolTip("#btnImportAccounts", "Import Accounts"); PaperclipUtils.createToolTip("#btnImportAccounts", "Import Accounts");
EthoUtils.createToolTip("#btnImportFromPrivateKey", "Import From Private Key"); PaperclipUtils.createToolTip("#btnImportFromPrivateKey", "Import From Private Key");
} }
validateNewAccountForm() { validateNewAccountForm() {
if (EthoMainGUI.getAppState() == "account") { if (PaperclipMainGUI.getAppState() == "account") {
if (!$("#walletPasswordFirst").val()) { if (!$("#walletPasswordFirst").val()) {
EthoMainGUI.showGeneralError("Password cannot be empty!"); PaperclipMainGUI.showGeneralError("Password cannot be empty!");
return false; return false;
} }
if (!$("#walletPasswordSecond").val()) { if (!$("#walletPasswordSecond").val()) {
EthoMainGUI.showGeneralError("Password cannot be empty!"); PaperclipMainGUI.showGeneralError("Password cannot be empty!");
return false; return false;
} }
if ($("#walletPasswordFirst").val() !== $("#walletPasswordSecond").val()) { if ($("#walletPasswordFirst").val() !== $("#walletPasswordSecond").val()) {
EthoMainGUI.showGeneralError("Passwords do not match!"); PaperclipMainGUI.showGeneralError("Passwords do not match!");
return false; return false;
} }