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
This commit is contained in:
2025-06-15 16:32:44 -07:00
parent d1c1c835f2
commit f3367b22dc
5 changed files with 772 additions and 265 deletions

View File

@@ -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;