Files
paperclip-wallet/renderer/blockchain.js
matt f3367b22dc 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
2025-06-15 16:32:44 -07:00

303 lines
8.0 KiB
JavaScript
Executable File

// In renderer process (web page).
const {
ipcRenderer
} = require("electron");
class PaperclipBlockchain {
constructor() {
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
};
}
async getStatus() {
try {
const result = await ipcRenderer.invoke("paperclip-get-status");
this.isConnected = true;
return result;
} catch (error) {
this.isConnected = false;
throw error;
}
}
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) {
// 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);
}
async getTransaction(txHash, clbError, clbSuccess) {
try {
const transaction = await ipcRenderer.invoke("paperclip-get-transaction", txHash);
clbSuccess(transaction);
} catch (error) {
clbError(error);
}
}
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);
}
}
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);
}
}
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 {
clbError(result.error || "Transaction failed");
}
} catch (error) {
clbError(error);
}
}
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;
}
// Get balance for each address
let processedCount = 0;
const totalAddresses = wallets.addresses.length;
for (let i = 0; i < wallets.addresses.length; i++) {
const address = wallets.addresses[i];
const walletName = wallets.names[address] || `Account ${i + 1}`;
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);
}
}
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";
}
}
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 global instance
const PaperclipChain = new PaperclipBlockchain();
// Make it available globally (maintaining compatibility with existing code)
window.PaperclipChain = PaperclipChain;