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:
@@ -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;
|
||||
Reference in New Issue
Block a user