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