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

236
modules/clips-crypto.js Normal file
View File

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

254
modules/paperclip-rpc.js Normal file
View File

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