From f3367b22dca282b4db8fa9e715387ca021833508 Mon Sep 17 00:00:00 2001 From: matt Date: Sun, 15 Jun 2025 16:32:44 -0700 Subject: [PATCH] feat: Convert Ether1 wallet to PaperclipWallet - Update package.json with PaperclipWallet branding - Replace Ethereum Web3 with Tendermint RPC client - Implement CLIPS address format (CLIP-{64-hex}) - Add Ed25519 key management with tweetnacl - Create PaperclipChain blockchain interface - Support gas system and transaction types - Add smart contract and multisig support --- main.js | 5 +- modules/clips-crypto.js | 236 ++++++++++++++++++ modules/paperclip-rpc.js | 254 +++++++++++++++++++ package.json | 29 ++- renderer/blockchain.js | 513 ++++++++++++++++++++------------------- 5 files changed, 772 insertions(+), 265 deletions(-) create mode 100644 modules/clips-crypto.js create mode 100644 modules/paperclip-rpc.js diff --git a/main.js b/main.js index c218aa1..5d11654 100755 --- a/main.js +++ b/main.js @@ -8,7 +8,8 @@ const { const singleInstance = require("single-instance"); const path = require("path"); const fs = require("fs"); -var locker = new singleInstance("Ether1DesktopWallet"); +const PaperclipNode = require("./modules/paperclip-rpc.js"); +var locker = new singleInstance("PaperclipDesktopWallet"); locker.lock().then(function() { // Keep a global reference of the window object, if you don't, the window will @@ -31,7 +32,7 @@ locker.lock().then(function() { // and load the index.html of the app. mainWindow.loadFile("index.html"); - EthoGeth.startGeth(); + PaperclipNode.startConnection(); // Open the DevTools. //mainWindow.webContents.openDevTools() diff --git a/modules/clips-crypto.js b/modules/clips-crypto.js new file mode 100644 index 0000000..9bd7272 --- /dev/null +++ b/modules/clips-crypto.js @@ -0,0 +1,236 @@ +const nacl = require('tweetnacl'); +const crypto = require('crypto'); + +class ClipsCrypto { + constructor() { + this.addressPrefix = 'CLIP-'; + } + + /** + * Generate a new Ed25519 key pair + * @returns {Object} Contains publicKey, privateKey, and address + */ + generateKeyPair() { + const keyPair = nacl.sign.keyPair(); + + return { + publicKey: Buffer.from(keyPair.publicKey).toString('hex'), + privateKey: Buffer.from(keyPair.secretKey).toString('hex'), + address: this.publicKeyToAddress(keyPair.publicKey) + }; + } + + /** + * Convert public key to CLIPS address format + * @param {Uint8Array|Buffer|string} publicKey - The public key + * @returns {string} CLIPS address + */ + publicKeyToAddress(publicKey) { + let pubKeyBuffer; + + if (typeof publicKey === 'string') { + pubKeyBuffer = Buffer.from(publicKey, 'hex'); + } else if (publicKey instanceof Uint8Array) { + pubKeyBuffer = Buffer.from(publicKey); + } else { + pubKeyBuffer = publicKey; + } + + return this.addressPrefix + pubKeyBuffer.toString('hex').toUpperCase(); + } + + /** + * Validate CLIPS address format + * @param {string} address - The address to validate + * @returns {boolean} True if valid + */ + isValidAddress(address) { + if (!address || typeof address !== 'string') { + return false; + } + + // Check prefix + if (!address.startsWith(this.addressPrefix)) { + return false; + } + + // Check hex part (should be 64 characters after prefix) + const hexPart = address.slice(this.addressPrefix.length); + if (hexPart.length !== 64) { + return false; + } + + // Check if hex is valid + return /^[0-9A-Fa-f]+$/.test(hexPart); + } + + /** + * Sign a transaction + * @param {string} privateKeyHex - The private key in hex format + * @param {Object} transaction - Transaction data + * @returns {string} Signature in hex format + */ + signTransaction(privateKeyHex, transaction) { + const privateKey = Buffer.from(privateKeyHex, 'hex'); + + // Create transaction message to sign + const txMessage = this.createTransactionMessage(transaction); + const messageBuffer = Buffer.from(txMessage, 'utf8'); + + // Sign the message + const signature = nacl.sign.detached(messageBuffer, privateKey); + + return Buffer.from(signature).toString('hex'); + } + + /** + * Create transaction message for signing + * @param {Object} tx - Transaction object + * @returns {string} Message to sign + */ + createTransactionMessage(tx) { + // Format: sender:receiver:amount:nonce:type:data + const parts = [ + tx.sender || '', + tx.receiver || '', + tx.amount || '0', + tx.nonce || '0', + tx.type || 'transfer', + tx.data || '' + ]; + + return parts.join(':'); + } + + /** + * Create a complete transaction object + * @param {Object} params - Transaction parameters + * @returns {Object} Complete transaction object + */ + createTransaction(params) { + const { + sender, + receiver, + amount, + nonce, + type = 'transfer', + data = '', + privateKey + } = params; + + const transaction = { + sender, + receiver, + amount: parseInt(amount), + nonce: parseInt(nonce), + type, + data, + timestamp: Date.now() + }; + + // Sign the transaction + if (privateKey) { + transaction.signature = this.signTransaction(privateKey, transaction); + } + + return transaction; + } + + /** + * Verify a transaction signature + * @param {Object} transaction - The transaction object + * @param {string} publicKeyHex - The public key to verify against + * @returns {boolean} True if signature is valid + */ + verifyTransaction(transaction, publicKeyHex) { + if (!transaction.signature) { + return false; + } + + try { + const publicKey = Buffer.from(publicKeyHex, 'hex'); + const signature = Buffer.from(transaction.signature, 'hex'); + + // Recreate the message that was signed + const txMessage = this.createTransactionMessage(transaction); + const messageBuffer = Buffer.from(txMessage, 'utf8'); + + return nacl.sign.detached.verify(messageBuffer, signature, publicKey); + } catch (error) { + console.error('Error verifying transaction:', error); + return false; + } + } + + /** + * Convert transaction to hex string for broadcasting + * @param {Object} transaction - The transaction object + * @returns {string} Transaction as hex string + */ + transactionToHex(transaction) { + const txString = JSON.stringify(transaction); + return Buffer.from(txString, 'utf8').toString('hex'); + } + + /** + * Convert hex string back to transaction object + * @param {string} txHex - Transaction in hex format + * @returns {Object} Transaction object + */ + hexToTransaction(txHex) { + const txString = Buffer.from(txHex, 'hex').toString('utf8'); + return JSON.parse(txString); + } + + /** + * Import private key and return key pair info + * @param {string} privateKeyHex - Private key in hex format + * @returns {Object} Key pair information + */ + importPrivateKey(privateKeyHex) { + try { + const privateKey = Buffer.from(privateKeyHex, 'hex'); + + // Generate public key from private key + const keyPair = nacl.sign.keyPair.fromSecretKey(privateKey); + + return { + publicKey: Buffer.from(keyPair.publicKey).toString('hex'), + privateKey: privateKeyHex, + address: this.publicKeyToAddress(keyPair.publicKey) + }; + } catch (error) { + throw new Error('Invalid private key format'); + } + } + + /** + * Generate a random seed phrase (12 words) + * @returns {string} Seed phrase + */ + generateSeedPhrase() { + // Simple word list for demo purposes + const wordList = [ + 'abandon', 'ability', 'able', 'about', 'above', 'absent', 'absorb', 'abstract', + 'absurd', 'abuse', 'access', 'accident', 'account', 'accuse', 'achieve', 'acid', + 'acoustic', 'acquire', 'across', 'act', 'action', 'actor', 'actress', 'actual', + 'adapt', 'add', 'addict', 'address', 'adjust', 'admit', 'adult', 'advance', + 'advice', 'aerobic', 'affair', 'afford', 'afraid', 'again', 'against', 'agent', + 'agree', 'ahead', 'aim', 'air', 'airport', 'aisle', 'alarm', 'album', + 'alcohol', 'alert', 'alien', 'all', 'alley', 'allow', 'almost', 'alone', + 'alpha', 'already', 'also', 'alter', 'always', 'amateur', 'amazing', 'among', + 'amount', 'amused', 'analyst', 'anchor', 'ancient', 'anger', 'angle', 'angry', + 'animal', 'ankle', 'announce', 'annual', 'another', 'answer', 'antenna', 'antique' + ]; + + const words = []; + for (let i = 0; i < 12; i++) { + const randomIndex = crypto.randomInt(0, wordList.length); + words.push(wordList[randomIndex]); + } + + return words.join(' '); + } +} + +module.exports = new ClipsCrypto(); \ No newline at end of file diff --git a/modules/paperclip-rpc.js b/modules/paperclip-rpc.js new file mode 100644 index 0000000..d2786b2 --- /dev/null +++ b/modules/paperclip-rpc.js @@ -0,0 +1,254 @@ +const {app, dialog, ipcMain} = require("electron"); +const path = require("path"); +const fs = require("fs"); +const fetch = require("node-fetch"); + +class PaperclipRPC { + constructor() { + this.isConnected = false; + this.rpcUrl = "http://localhost:26657"; + this.logEvents = false; + + // create the user data dir (needed for MacOS) + if (!fs.existsSync(app.getPath("userData"))) { + fs.mkdirSync(app.getPath("userData")); + } + + if (this.logEvents) { + this.logStream = fs.createWriteStream(path.join(app.getPath("userData"), "paperclip-rpc.log"), {flags: "a"}); + } + } + + _writeLog(text) { + if (this.logEvents) { + this.logStream.write(`${new Date().toISOString()}: ${text}\n`); + } + console.log("PaperclipRPC:", text); + } + + async _makeRPCCall(method, params = {}) { + try { + const url = `${this.rpcUrl}/${method}`; + const queryParams = new URLSearchParams(); + + Object.keys(params).forEach(key => { + queryParams.append(key, params[key]); + }); + + const fullUrl = queryParams.toString() ? `${url}?${queryParams}` : url; + + this._writeLog(`Making RPC call to: ${fullUrl}`); + + const response = await fetch(fullUrl); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + return data; + } catch (error) { + this._writeLog(`RPC call failed: ${error.message}`); + throw error; + } + } + + async getStatus() { + try { + const result = await this._makeRPCCall("status"); + this.isConnected = true; + return result; + } catch (error) { + this.isConnected = false; + throw error; + } + } + + async getBalance(address) { + try { + const result = await this._makeRPCCall("abci_query", { + path: `"balance:${address}"` + }); + + if (result.result && result.result.response && result.result.response.value) { + // Decode base64 response + const balance = Buffer.from(result.result.response.value, 'base64').toString(); + return parseInt(balance) || 0; + } + return 0; + } catch (error) { + this._writeLog(`Failed to get balance for ${address}: ${error.message}`); + return 0; + } + } + + async getNonce(address) { + try { + const result = await this._makeRPCCall("abci_query", { + path: `"nonce:${address}"` + }); + + if (result.result && result.result.response && result.result.response.value) { + const nonce = Buffer.from(result.result.response.value, 'base64').toString(); + return parseInt(nonce) || 0; + } + return 0; + } catch (error) { + this._writeLog(`Failed to get nonce for ${address}: ${error.message}`); + return 0; + } + } + + async broadcastTransaction(txHex) { + try { + const result = await this._makeRPCCall("broadcast_tx_commit", { + tx: txHex + }); + + if (result.result && result.result.check_tx && result.result.check_tx.code === 0) { + this._writeLog(`Transaction broadcast successful: ${result.result.hash}`); + return { + success: true, + hash: result.result.hash, + height: result.result.height + }; + } else { + const error = result.result?.check_tx?.log || "Transaction failed"; + throw new Error(error); + } + } catch (error) { + this._writeLog(`Failed to broadcast transaction: ${error.message}`); + throw error; + } + } + + async getTransaction(hash) { + try { + const result = await this._makeRPCCall("tx", { + hash: hash, + prove: "false" + }); + return result; + } catch (error) { + this._writeLog(`Failed to get transaction ${hash}: ${error.message}`); + throw error; + } + } + + async getBlock(height = null) { + try { + const params = height ? { height: height.toString() } : {}; + const result = await this._makeRPCCall("block", params); + return result; + } catch (error) { + this._writeLog(`Failed to get block: ${error.message}`); + throw error; + } + } + + async getContractInfo(contractAddress) { + try { + const result = await this._makeRPCCall("abci_query", { + path: `"contract:${contractAddress}"` + }); + + if (result.result && result.result.response && result.result.response.value) { + const contractInfo = Buffer.from(result.result.response.value, 'base64').toString(); + return JSON.parse(contractInfo); + } + return null; + } catch (error) { + this._writeLog(`Failed to get contract info for ${contractAddress}: ${error.message}`); + return null; + } + } + + async getMultisigInfo(multisigAddress) { + try { + const result = await this._makeRPCCall("abci_query", { + path: `"multisig:${multisigAddress}"` + }); + + if (result.result && result.result.response && result.result.response.value) { + const multisigInfo = Buffer.from(result.result.response.value, 'base64').toString(); + return JSON.parse(multisigInfo); + } + return null; + } catch (error) { + this._writeLog(`Failed to get multisig info for ${multisigAddress}: ${error.message}`); + return null; + } + } + + async getFeePool() { + try { + const result = await this._makeRPCCall("abci_query", { + path: `"fee_pool"` + }); + + if (result.result && result.result.response && result.result.response.value) { + const feePool = Buffer.from(result.result.response.value, 'base64').toString(); + return parseInt(feePool) || 0; + } + return 0; + } catch (error) { + this._writeLog(`Failed to get fee pool: ${error.message}`); + return 0; + } + } + + setRPCUrl(url) { + this.rpcUrl = url; + this._writeLog(`RPC URL updated to: ${url}`); + } + + startConnection() { + this._writeLog("Starting PaperclipChain RPC connection..."); + + // Test connection + this.getStatus() + .then(() => { + this._writeLog("Successfully connected to PaperclipChain node"); + }) + .catch((error) => { + this._writeLog(`Failed to connect to PaperclipChain node: ${error.message}`); + }); + } + + stopConnection() { + this._writeLog("Stopping PaperclipChain RPC connection..."); + this.isConnected = false; + + if (this.logStream) { + this.logStream.end(); + } + } +} + +const PaperclipNode = new PaperclipRPC(); + +// IPC handlers for renderer process +ipcMain.handle("paperclip-get-status", async () => { + return await PaperclipNode.getStatus(); +}); + +ipcMain.handle("paperclip-get-balance", async (event, address) => { + return await PaperclipNode.getBalance(address); +}); + +ipcMain.handle("paperclip-get-nonce", async (event, address) => { + return await PaperclipNode.getNonce(address); +}); + +ipcMain.handle("paperclip-broadcast-tx", async (event, txHex) => { + return await PaperclipNode.broadcastTransaction(txHex); +}); + +ipcMain.handle("paperclip-get-transaction", async (event, hash) => { + return await PaperclipNode.getTransaction(hash); +}); + +ipcMain.handle("paperclip-get-block", async (event, height) => { + return await PaperclipNode.getBlock(height); +}); + +module.exports = PaperclipNode; \ No newline at end of file diff --git a/package.json b/package.json index 6f5d0f9..97c94bd 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "EthoWallet", - "version": "3.0.0", - "description": "Desktop wallet for Etho Protocol ($ETHO)", + "name": "PaperclipWallet", + "version": "1.0.0", + "description": "Desktop wallet for PaperclipChain ($CLIPS)", "main": "main.js", "scripts": { "start": "electron .", @@ -12,7 +12,7 @@ "bundle": "browserify assets/dashboard/js/node.js > assets/dashboard/js/bundle.js" }, "build": { - "appId": "EthoDesktopWallet", + "appId": "PaperclipDesktopWallet", "files": [ "modules/*", "assets/**/*", @@ -46,34 +46,33 @@ ] } }, - "repository": "https://github.com/Ether1Project/Ether1DesktopWallet", + "repository": "https://git.takoyaki.cool/matt/paperclip-wallet", "keywords": [ - "Etho", + "PaperclipChain", + "CLIPS", "Desktop", "Wallet" ], - "author": "Etho ", - "url": "https://ethoprotocol.com", - "email": "admin@ethoprotocol.com", + "author": "Matt ", + "url": "https://takoyaki.cool", + "email": "matt@takoyaki.cool", "license": "CC0-1.0", "dependencies": { - "@ethereumjs/common": "3.0.0", "@unibeautify/beautifier-js-beautify": "^0.4.0", "adm-zip": "^0.5.10", "app-root-path": "^3.1.0", + "crypto": "^1.0.1", + "ed25519": "^0.0.4", "electron-storage": "^1.0.7", - "ethereum-private-key-to-address": "^0.0.7", - "ethereumjs-common": "1.5.2", "fs-extra": "^10.0.1", "handlebars": "^4.7.7", - "keythereum": "2.0.0", "moment": "^2.29.4", "nedb": "^1.8.0", "node-fetch": "^3.3.1", "pull-file-reader": "^1.0.2", "single-instance": "0.0.1", - "undici": "^5.21.0", - "wrtc": "^0.4.7" + "tweetnacl": "^1.0.3", + "undici": "^5.21.0" }, "devDependencies": { "browserify": "^17.0.0", diff --git a/renderer/blockchain.js b/renderer/blockchain.js index 9db4925..04bb9a4 100755 --- a/renderer/blockchain.js +++ b/renderer/blockchain.js @@ -3,284 +3,301 @@ const { ipcRenderer } = require("electron"); -class Blockchain { +class PaperclipBlockchain { constructor() { - this.txSubscribe = null; - this.bhSubscribe = null; + this.isConnected = false; + this.gasPrice = 1; // 1 CLIP per gas unit + this.gasLimits = { + 'transfer': 21000, + 'contract_call': 50000, + 'contract_deploy': 100000, + 'multisig_create': 75000, + 'multisig': 35000 + }; } - getBlock(blockToGet, includeData, clbError, clbSuccess) { - web3Local.eth.getBlock(blockToGet, includeData, function(error, block) { - if (error) { - clbError(error); - } else { - clbSuccess(block); - } - }); + async getStatus() { + try { + const result = await ipcRenderer.invoke("paperclip-get-status"); + this.isConnected = true; + return result; + } catch (error) { + this.isConnected = false; + throw error; + } } - getAccounts(clbError, clbSuccess) { - web3Local.eth.getAccounts(function(err, res) { - if (err) { - clbError(err); - } else { - clbSuccess(res); - } - }); + async getBlock(blockHeight, clbError, clbSuccess) { + try { + const block = await ipcRenderer.invoke("paperclip-get-block", blockHeight); + clbSuccess(block); + } catch (error) { + clbError(error); + } + } + + async getBalance(address, clbError, clbSuccess) { + try { + const balance = await ipcRenderer.invoke("paperclip-get-balance", address); + clbSuccess(balance); + } catch (error) { + clbError(error); + } + } + + async getNonce(address, clbError, clbSuccess) { + try { + const nonce = await ipcRenderer.invoke("paperclip-get-nonce", address); + clbSuccess(nonce); + } catch (error) { + clbError(error); + } } isAddress(address) { - return web3Local.utils.isAddress(address); + // CLIPS address validation: CLIP- followed by 64 hex characters + if (!address || typeof address !== 'string') { + return false; + } + return /^CLIP-[0-9A-Fa-f]{64}$/.test(address); } - getTransaction(thxid, clbError, clbSuccess) { - web3Local.eth.getTransaction(thxid, function(error, result) { - if (error) { - clbError(error); - } else { - clbSuccess(result); - } - }); - } - - getTranasctionFee(fromAddress, toAddress, value, clbError, clbSuccess) { - web3Local.eth.getTransactionCount(fromAddress, function(error, result) { - if (error) { - clbError(error); - } else { - var amountToSend = web3Local.utils.toWei(value, "ether"); //convert to wei value - var RawTransaction = { - from: fromAddress, - to: toAddress, - value: amountToSend, - nonce: result - }; - - web3Local.eth.estimateGas(RawTransaction, function(error, result) { - if (error) { - clbError(error); - } else { - var usedGas = result + 1; - web3Local.eth.getGasPrice(function(error, result) { - if (error) { - clbError(error); - } else { - clbSuccess(result * usedGas); - } - }); - } - }); - } - }); - } - - prepareTransaction(password, fromAddress, toAddress, value, clbError, clbSuccess) { - web3Local.eth.personal.unlockAccount(fromAddress, password, function(error, result) { - if (error) { - clbError("Wrong password for the selected address!"); - } else { - web3Local.eth.getTransactionCount(fromAddress, "pending", function(error, result) { - if (error) { - clbError(error); - } else { - var amountToSend = web3Local.utils.toWei(value, "ether"); //convert to wei value - var RawTransaction = { - from: fromAddress, - to: toAddress, - value: amountToSend, - nonce: result - }; - - web3Local.eth.estimateGas(RawTransaction, function(error, result) { - if (error) { - clbError(error); - } else { - RawTransaction.gas = result + 1; - web3Local.eth.getGasPrice(function(error, result) { - if (error) { - clbError(error); - } else { - RawTransaction.gasPrice = result; - web3Local.eth.signTransaction(RawTransaction, fromAddress, function(error, result) { - if (error) { - clbError(error); - } else { - clbSuccess(result); - } - }); - } - }); - } - }); - } - }); - } - }); - } - - sendTransaction(rawTransaction, clbError, clbSuccess) { - web3Local.eth.sendSignedTransaction(rawTransaction, function(error, result) { - if (error) { - clbError(error); - } else { - clbSuccess(result); - } - }); - } - - getAccountsData(clbError, clbSuccess) { - var rendererData = {}; - rendererData.sumBalance = 0; - rendererData.addressData = []; - - var wallets = EthoDatatabse.getWallets(); - var counter = 0; - - web3Local.eth.getAccounts(function(err, res) { - if (err) { - clbError(err); - } else { - for (var i = 0; i < res.length; i++) { - var walletName = vsprintf("Account %d", [i + 1]); - if (wallets) { - walletName = wallets.names[res[i]] || walletName; - } - - var addressInfo = {}; - addressInfo.balance = 0; - addressInfo.address = res[i]; - addressInfo.name = walletName; - rendererData.addressData.push(addressInfo); - } - - if (rendererData.addressData.length > 0) { - updateBalance(counter); - } else { - clbSuccess(rendererData); - } - } - }); - - function updateBalance(index) { - web3Local.eth.getBalance(rendererData.addressData[index].address, function(error, balance) { - rendererData.addressData[index].balance = parseFloat(web3Local.utils.fromWei(balance, "ether")).toFixed(2); - rendererData.sumBalance = rendererData.sumBalance + parseFloat(web3Local.utils.fromWei(balance, "ether")); - - if (counter < rendererData.addressData.length - 1) { - counter++; - updateBalance(counter); - } else { - rendererData.sumBalance = parseFloat(rendererData.sumBalance).toFixed(2); - clbSuccess(rendererData); - } - }); + async getTransaction(txHash, clbError, clbSuccess) { + try { + const transaction = await ipcRenderer.invoke("paperclip-get-transaction", txHash); + clbSuccess(transaction); + } catch (error) { + clbError(error); } } - getAddressListData(clbError, clbSuccess) { - var rendererData = {}; - rendererData.addressData = []; + async getTransactionFee(fromAddress, toAddress, amount, txType = 'transfer', clbError, clbSuccess) { + try { + // Calculate gas needed based on transaction type + const gasNeeded = this.gasLimits[txType] || this.gasLimits['transfer']; + const fee = gasNeeded * this.gasPrice; + + clbSuccess({ + gasNeeded: gasNeeded, + gasPrice: this.gasPrice, + totalFee: fee + }); + } catch (error) { + clbError(error); + } + } - var wallets = EthoDatatabse.getWallets(); - var counter = 0; + async prepareTransaction(privateKey, fromAddress, toAddress, amount, txType = 'transfer', data = '', clbError, clbSuccess) { + try { + // Get current nonce + const nonce = await ipcRenderer.invoke("paperclip-get-nonce", fromAddress); + + // Calculate gas + const gasNeeded = this.gasLimits[txType] || this.gasLimits['transfer']; + + // Create transaction object + const ClipsCrypto = require('../modules/clips-crypto.js'); + const transaction = ClipsCrypto.createTransaction({ + sender: fromAddress, + receiver: toAddress, + amount: parseInt(amount), + nonce: nonce, + type: txType, + data: data, + privateKey: privateKey + }); + + // Add gas information + transaction.gas = gasNeeded; + transaction.gasPrice = this.gasPrice; + transaction.fee = gasNeeded * this.gasPrice; + + clbSuccess(transaction); + } catch (error) { + clbError(error); + } + } - web3Local.eth.getAccounts(function(err, res) { - if (err) { - clbError(err); + async sendTransaction(transaction, clbError, clbSuccess) { + try { + // Convert transaction to hex for broadcasting + const ClipsCrypto = require('../modules/clips-crypto.js'); + const txHex = ClipsCrypto.transactionToHex(transaction); + + // Broadcast transaction + const result = await ipcRenderer.invoke("paperclip-broadcast-tx", txHex); + + if (result.success) { + clbSuccess({ + hash: result.hash, + height: result.height + }); } else { - for (var i = 0; i < res.length; i++) { - var walletName = vsprintf("Account %d", [i + 1]); - if (wallets) { - walletName = wallets.names[res[i]] || walletName; - } + clbError(result.error || "Transaction failed"); + } + } catch (error) { + clbError(error); + } + } - var addressInfo = {}; - addressInfo.address = res[i]; - addressInfo.name = walletName; - rendererData.addressData.push(addressInfo); - } + async getAccountsData(clbError, clbSuccess) { + try { + const rendererData = { + sumBalance: 0, + addressData: [] + }; + // Get stored wallets from database + const wallets = PaperclipDatabase.getWallets(); + + if (!wallets || !wallets.addresses || wallets.addresses.length === 0) { clbSuccess(rendererData); + return; } - }); - } - createNewAccount(password, clbError, clbSuccess) { - web3Local.eth.personal.newAccount(password, function(error, account) { - if (error) { - clbError(error); - } else { - clbSuccess(account); - } - }); - } + // Get balance for each address + let processedCount = 0; + const totalAddresses = wallets.addresses.length; - importFromPrivateKey(privateKey, keyPassword, clbError, clbSuccess) { - web3Local.eth.personal.importRawKey(privateKey, keyPassword, function(error, account) { - if (error) { - clbError(error); - } else { - clbSuccess(account); - } - }); - } + for (let i = 0; i < wallets.addresses.length; i++) { + const address = wallets.addresses[i]; + const walletName = wallets.names[address] || `Account ${i + 1}`; - subsribePendingTransactions(clbError, clbSuccess, clbData) { - this.txSubscribe = web3Local.eth.subscribe("pendingTransactions", function(error, result) { - if (error) { - clbError(error); - } else { - clbSuccess(result); - } - }).on("data", function(transaction) { - if (clbData) { - clbData(transaction); - } - }); - } - - unsubsribePendingTransactions(clbError, clbSuccess) { - if (this.txSubscribe) { - this.txSubscribe.unsubscribe(function(error, success) { - if (error) { - clbError(error); - } else { - clbSuccess(success); + try { + const balance = await ipcRenderer.invoke("paperclip-get-balance", address); + + rendererData.addressData.push({ + address: address, + name: walletName, + balance: balance, + balanceFormatted: this.formatBalance(balance) + }); + + rendererData.sumBalance += balance; + + } catch (error) { + console.error(`Failed to get balance for ${address}:`, error); + + rendererData.addressData.push({ + address: address, + name: walletName, + balance: 0, + balanceFormatted: "0 CLIPS", + error: true + }); } - }); + + processedCount++; + + // If all addresses processed, return data + if (processedCount === totalAddresses) { + rendererData.sumBalanceFormatted = this.formatBalance(rendererData.sumBalance); + clbSuccess(rendererData); + } + } + + } catch (error) { + clbError(error); } } - subsribeNewBlockHeaders(clbError, clbSuccess, clbData) { - this.bhSubscribe = web3Local.eth.subscribe("newBlockHeaders", function(error, result) { - if (error) { - clbError(error); - } else { - clbSuccess(result); - } - }).on("data", function(blockHeader) { - if (clbData) { - clbData(blockHeader); - } - }); - } - - unsubsribeNewBlockHeaders(clbError, clbSuccess) { - if (this.bhSubscribe) { - this.bhSubscribe.unsubscribe(function(error, success) { - if (error) { - clbError(error); - } else { - clbSuccess(success); - } - }); + formatBalance(balance) { + if (balance === 0) { + return "0 CLIPS"; + } + + // Format balance with appropriate decimal places + 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"; } } - closeConnection() { - web3Local.currentProvider.connection.close(); + async getNetworkInfo(clbError, clbSuccess) { + try { + const status = await this.getStatus(); + + const networkInfo = { + nodeInfo: status.result.node_info, + syncInfo: status.result.sync_info, + validatorInfo: status.result.validator_info, + chainId: status.result.node_info.network, + latestBlockHeight: status.result.sync_info.latest_block_height, + catching_up: status.result.sync_info.catching_up + }; + + clbSuccess(networkInfo); + } catch (error) { + clbError(error); + } + } + + async getContractInfo(contractAddress, clbError, clbSuccess) { + try { + const contractInfo = await ipcRenderer.invoke("paperclip-get-contract", contractAddress); + clbSuccess(contractInfo); + } catch (error) { + clbError(error); + } + } + + async getMultisigInfo(multisigAddress, clbError, clbSuccess) { + try { + const multisigInfo = await ipcRenderer.invoke("paperclip-get-multisig", multisigAddress); + clbSuccess(multisigInfo); + } catch (error) { + clbError(error); + } + } + + async getFeePool(clbError, clbSuccess) { + try { + const feePool = await ipcRenderer.invoke("paperclip-get-fee-pool"); + clbSuccess({ + totalFees: feePool, + formatted: this.formatBalance(feePool) + }); + } catch (error) { + clbError(error); + } + } + + // Utility method to validate transaction before sending + validateTransaction(transaction) { + const errors = []; + + if (!this.isAddress(transaction.sender)) { + errors.push("Invalid sender address"); + } + + if (!this.isAddress(transaction.receiver)) { + errors.push("Invalid receiver address"); + } + + if (!transaction.amount || transaction.amount <= 0) { + errors.push("Invalid amount"); + } + + if (transaction.nonce < 0) { + errors.push("Invalid nonce"); + } + + if (!transaction.signature) { + errors.push("Transaction not signed"); + } + + return { + isValid: errors.length === 0, + errors: errors + }; } } -// create new blockchain variable -EthoBlockchain = new Blockchain(); +// Create global instance +const PaperclipChain = new PaperclipBlockchain(); + +// Make it available globally (maintaining compatibility with existing code) +window.PaperclipChain = PaperclipChain; \ No newline at end of file