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:
5
main.js
5
main.js
@@ -8,7 +8,8 @@ const {
|
|||||||
const singleInstance = require("single-instance");
|
const singleInstance = require("single-instance");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const fs = require("fs");
|
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() {
|
locker.lock().then(function() {
|
||||||
// Keep a global reference of the window object, if you don't, the window will
|
// 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.
|
// and load the index.html of the app.
|
||||||
mainWindow.loadFile("index.html");
|
mainWindow.loadFile("index.html");
|
||||||
EthoGeth.startGeth();
|
PaperclipNode.startConnection();
|
||||||
|
|
||||||
// Open the DevTools.
|
// Open the DevTools.
|
||||||
//mainWindow.webContents.openDevTools()
|
//mainWindow.webContents.openDevTools()
|
||||||
|
|||||||
236
modules/clips-crypto.js
Normal file
236
modules/clips-crypto.js
Normal 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
254
modules/paperclip-rpc.js
Normal 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;
|
||||||
29
package.json
29
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "EthoWallet",
|
"name": "PaperclipWallet",
|
||||||
"version": "3.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Desktop wallet for Etho Protocol ($ETHO)",
|
"description": "Desktop wallet for PaperclipChain ($CLIPS)",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron .",
|
"start": "electron .",
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"bundle": "browserify assets/dashboard/js/node.js > assets/dashboard/js/bundle.js"
|
"bundle": "browserify assets/dashboard/js/node.js > assets/dashboard/js/bundle.js"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "EthoDesktopWallet",
|
"appId": "PaperclipDesktopWallet",
|
||||||
"files": [
|
"files": [
|
||||||
"modules/*",
|
"modules/*",
|
||||||
"assets/**/*",
|
"assets/**/*",
|
||||||
@@ -46,34 +46,33 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repository": "https://github.com/Ether1Project/Ether1DesktopWallet",
|
"repository": "https://git.takoyaki.cool/matt/paperclip-wallet",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"Etho",
|
"PaperclipChain",
|
||||||
|
"CLIPS",
|
||||||
"Desktop",
|
"Desktop",
|
||||||
"Wallet"
|
"Wallet"
|
||||||
],
|
],
|
||||||
"author": "Etho <admin@ethoprotocol.com>",
|
"author": "Matt <matt@takoyaki.cool>",
|
||||||
"url": "https://ethoprotocol.com",
|
"url": "https://takoyaki.cool",
|
||||||
"email": "admin@ethoprotocol.com",
|
"email": "matt@takoyaki.cool",
|
||||||
"license": "CC0-1.0",
|
"license": "CC0-1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ethereumjs/common": "3.0.0",
|
|
||||||
"@unibeautify/beautifier-js-beautify": "^0.4.0",
|
"@unibeautify/beautifier-js-beautify": "^0.4.0",
|
||||||
"adm-zip": "^0.5.10",
|
"adm-zip": "^0.5.10",
|
||||||
"app-root-path": "^3.1.0",
|
"app-root-path": "^3.1.0",
|
||||||
|
"crypto": "^1.0.1",
|
||||||
|
"ed25519": "^0.0.4",
|
||||||
"electron-storage": "^1.0.7",
|
"electron-storage": "^1.0.7",
|
||||||
"ethereum-private-key-to-address": "^0.0.7",
|
|
||||||
"ethereumjs-common": "1.5.2",
|
|
||||||
"fs-extra": "^10.0.1",
|
"fs-extra": "^10.0.1",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
"keythereum": "2.0.0",
|
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"nedb": "^1.8.0",
|
"nedb": "^1.8.0",
|
||||||
"node-fetch": "^3.3.1",
|
"node-fetch": "^3.3.1",
|
||||||
"pull-file-reader": "^1.0.2",
|
"pull-file-reader": "^1.0.2",
|
||||||
"single-instance": "0.0.1",
|
"single-instance": "0.0.1",
|
||||||
"undici": "^5.21.0",
|
"tweetnacl": "^1.0.3",
|
||||||
"wrtc": "^0.4.7"
|
"undici": "^5.21.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"browserify": "^17.0.0",
|
"browserify": "^17.0.0",
|
||||||
|
|||||||
@@ -3,284 +3,301 @@ const {
|
|||||||
ipcRenderer
|
ipcRenderer
|
||||||
} = require("electron");
|
} = require("electron");
|
||||||
|
|
||||||
class Blockchain {
|
class PaperclipBlockchain {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.txSubscribe = null;
|
this.isConnected = false;
|
||||||
this.bhSubscribe = null;
|
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) {
|
async getStatus() {
|
||||||
web3Local.eth.getBlock(blockToGet, includeData, function(error, block) {
|
try {
|
||||||
if (error) {
|
const result = await ipcRenderer.invoke("paperclip-get-status");
|
||||||
clbError(error);
|
this.isConnected = true;
|
||||||
} else {
|
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);
|
clbSuccess(block);
|
||||||
|
} catch (error) {
|
||||||
|
clbError(error);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getAccounts(clbError, clbSuccess) {
|
async getBalance(address, clbError, clbSuccess) {
|
||||||
web3Local.eth.getAccounts(function(err, res) {
|
try {
|
||||||
if (err) {
|
const balance = await ipcRenderer.invoke("paperclip-get-balance", address);
|
||||||
clbError(err);
|
clbSuccess(balance);
|
||||||
} else {
|
} catch (error) {
|
||||||
clbSuccess(res);
|
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) {
|
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) {
|
async getTransaction(txHash, clbError, clbSuccess) {
|
||||||
web3Local.eth.getTransaction(thxid, function(error, result) {
|
try {
|
||||||
if (error) {
|
const transaction = await ipcRenderer.invoke("paperclip-get-transaction", txHash);
|
||||||
|
clbSuccess(transaction);
|
||||||
|
} catch (error) {
|
||||||
clbError(error);
|
clbError(error);
|
||||||
} else {
|
|
||||||
clbSuccess(result);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getTranasctionFee(fromAddress, toAddress, value, clbError, clbSuccess) {
|
async prepareTransaction(privateKey, fromAddress, toAddress, amount, txType = 'transfer', data = '', clbError, clbSuccess) {
|
||||||
web3Local.eth.getTransactionCount(fromAddress, function(error, result) {
|
try {
|
||||||
if (error) {
|
// 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);
|
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 {
|
} else {
|
||||||
var amountToSend = web3Local.utils.toWei(value, "ether"); //convert to wei value
|
clbError(result.error || "Transaction failed");
|
||||||
var RawTransaction = {
|
}
|
||||||
from: fromAddress,
|
} catch (error) {
|
||||||
to: toAddress,
|
clbError(error);
|
||||||
value: amountToSend,
|
}
|
||||||
nonce: result
|
}
|
||||||
|
|
||||||
|
async getAccountsData(clbError, clbSuccess) {
|
||||||
|
try {
|
||||||
|
const rendererData = {
|
||||||
|
sumBalance: 0,
|
||||||
|
addressData: []
|
||||||
};
|
};
|
||||||
|
|
||||||
web3Local.eth.estimateGas(RawTransaction, function(error, result) {
|
// Get stored wallets from database
|
||||||
if (error) {
|
const wallets = PaperclipDatabase.getWallets();
|
||||||
clbError(error);
|
|
||||||
} else {
|
if (!wallets || !wallets.addresses || wallets.addresses.length === 0) {
|
||||||
var usedGas = result + 1;
|
clbSuccess(rendererData);
|
||||||
web3Local.eth.getGasPrice(function(error, result) {
|
return;
|
||||||
if (error) {
|
|
||||||
clbError(error);
|
|
||||||
} else {
|
|
||||||
clbSuccess(result * usedGas);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareTransaction(password, fromAddress, toAddress, value, clbError, clbSuccess) {
|
processedCount++;
|
||||||
web3Local.eth.personal.unlockAccount(fromAddress, password, function(error, result) {
|
|
||||||
if (error) {
|
// If all addresses processed, return data
|
||||||
clbError("Wrong password for the selected address!");
|
if (processedCount === totalAddresses) {
|
||||||
} else {
|
rendererData.sumBalanceFormatted = this.formatBalance(rendererData.sumBalance);
|
||||||
web3Local.eth.getTransactionCount(fromAddress, "pending", function(error, result) {
|
clbSuccess(rendererData);
|
||||||
if (error) {
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
clbError(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 {
|
} else {
|
||||||
var amountToSend = web3Local.utils.toWei(value, "ether"); //convert to wei value
|
return balance.toLocaleString() + " CLIPS";
|
||||||
var RawTransaction = {
|
}
|
||||||
from: fromAddress,
|
}
|
||||||
to: toAddress,
|
|
||||||
value: amountToSend,
|
async getNetworkInfo(clbError, clbSuccess) {
|
||||||
nonce: result
|
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
|
||||||
};
|
};
|
||||||
|
|
||||||
web3Local.eth.estimateGas(RawTransaction, function(error, result) {
|
clbSuccess(networkInfo);
|
||||||
if (error) {
|
} catch (error) {
|
||||||
clbError(error);
|
clbError(error);
|
||||||
} else {
|
}
|
||||||
RawTransaction.gas = result + 1;
|
}
|
||||||
web3Local.eth.getGasPrice(function(error, result) {
|
|
||||||
if (error) {
|
async getContractInfo(contractAddress, clbError, clbSuccess) {
|
||||||
|
try {
|
||||||
|
const contractInfo = await ipcRenderer.invoke("paperclip-get-contract", contractAddress);
|
||||||
|
clbSuccess(contractInfo);
|
||||||
|
} catch (error) {
|
||||||
clbError(error);
|
clbError(error);
|
||||||
} else {
|
}
|
||||||
RawTransaction.gasPrice = result;
|
}
|
||||||
web3Local.eth.signTransaction(RawTransaction, fromAddress, function(error, result) {
|
|
||||||
if (error) {
|
async getMultisigInfo(multisigAddress, clbError, clbSuccess) {
|
||||||
|
try {
|
||||||
|
const multisigInfo = await ipcRenderer.invoke("paperclip-get-multisig", multisigAddress);
|
||||||
|
clbSuccess(multisigInfo);
|
||||||
|
} catch (error) {
|
||||||
clbError(error);
|
clbError(error);
|
||||||
} else {
|
|
||||||
clbSuccess(result);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sendTransaction(rawTransaction, clbError, clbSuccess) {
|
async getFeePool(clbError, clbSuccess) {
|
||||||
web3Local.eth.sendSignedTransaction(rawTransaction, function(error, result) {
|
try {
|
||||||
if (error) {
|
const feePool = await ipcRenderer.invoke("paperclip-get-fee-pool");
|
||||||
|
clbSuccess({
|
||||||
|
totalFees: feePool,
|
||||||
|
formatted: this.formatBalance(feePool)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
clbError(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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getAddressListData(clbError, clbSuccess) {
|
// Utility method to validate transaction before sending
|
||||||
var rendererData = {};
|
validateTransaction(transaction) {
|
||||||
rendererData.addressData = [];
|
const errors = [];
|
||||||
|
|
||||||
var wallets = EthoDatatabse.getWallets();
|
if (!this.isAddress(transaction.sender)) {
|
||||||
var counter = 0;
|
errors.push("Invalid sender address");
|
||||||
|
|
||||||
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 = {};
|
if (!this.isAddress(transaction.receiver)) {
|
||||||
addressInfo.address = res[i];
|
errors.push("Invalid receiver address");
|
||||||
addressInfo.name = walletName;
|
|
||||||
rendererData.addressData.push(addressInfo);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clbSuccess(rendererData);
|
if (!transaction.amount || transaction.amount <= 0) {
|
||||||
}
|
errors.push("Invalid amount");
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createNewAccount(password, clbError, clbSuccess) {
|
if (transaction.nonce < 0) {
|
||||||
web3Local.eth.personal.newAccount(password, function(error, account) {
|
errors.push("Invalid nonce");
|
||||||
if (error) {
|
|
||||||
clbError(error);
|
|
||||||
} else {
|
|
||||||
clbSuccess(account);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
importFromPrivateKey(privateKey, keyPassword, clbError, clbSuccess) {
|
if (!transaction.signature) {
|
||||||
web3Local.eth.personal.importRawKey(privateKey, keyPassword, function(error, account) {
|
errors.push("Transaction not signed");
|
||||||
if (error) {
|
|
||||||
clbError(error);
|
|
||||||
} else {
|
|
||||||
clbSuccess(account);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
subsribePendingTransactions(clbError, clbSuccess, clbData) {
|
return {
|
||||||
this.txSubscribe = web3Local.eth.subscribe("pendingTransactions", function(error, result) {
|
isValid: errors.length === 0,
|
||||||
if (error) {
|
errors: errors
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
subsribeNewBlockHeaders(clbError, clbSuccess, clbData) {
|
// Create global instance
|
||||||
this.bhSubscribe = web3Local.eth.subscribe("newBlockHeaders", function(error, result) {
|
const PaperclipChain = new PaperclipBlockchain();
|
||||||
if (error) {
|
|
||||||
clbError(error);
|
|
||||||
} else {
|
|
||||||
clbSuccess(result);
|
|
||||||
}
|
|
||||||
}).on("data", function(blockHeader) {
|
|
||||||
if (clbData) {
|
|
||||||
clbData(blockHeader);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
unsubsribeNewBlockHeaders(clbError, clbSuccess) {
|
// Make it available globally (maintaining compatibility with existing code)
|
||||||
if (this.bhSubscribe) {
|
window.PaperclipChain = PaperclipChain;
|
||||||
this.bhSubscribe.unsubscribe(function(error, success) {
|
|
||||||
if (error) {
|
|
||||||
clbError(error);
|
|
||||||
} else {
|
|
||||||
clbSuccess(success);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
closeConnection() {
|
|
||||||
web3Local.currentProvider.connection.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// create new blockchain variable
|
|
||||||
EthoBlockchain = new Blockchain();
|
|
||||||
Reference in New Issue
Block a user