Compare commits
10 Commits
2ba29503f6
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b597ebc07 | |||
| a4db7394b0 | |||
| 97a11985e7 | |||
| 2c3ad62bc4 | |||
| 8414593d47 | |||
| 1ceb56c7b8 | |||
| dab604463f | |||
| f3367b22dc | |||
|
|
d1c1c835f2 | ||
|
|
6db10719a7 |
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
.DS_Store
|
.DS_Store
|
||||||
dist
|
dist
|
||||||
|
bin/
|
||||||
|
|||||||
81
CHANGELOG.md
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to PaperclipWallet will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.0.0] - 2024-06-17
|
||||||
|
|
||||||
|
### Added - Major DeFi Staking Release
|
||||||
|
|
||||||
|
#### Comprehensive Staking System
|
||||||
|
- Validator delegation - Stake CLIPS tokens to active validators and earn rewards
|
||||||
|
- Real-time staking dashboard - Live overview of staking positions, rewards, and network statistics
|
||||||
|
- Multi-validator support - Delegate to multiple validators simultaneously
|
||||||
|
- Reward management - One-click claiming of accumulated staking rewards
|
||||||
|
- Validator creation - Full interface for becoming a validator (10,000 CLIPS minimum)
|
||||||
|
|
||||||
|
#### Advanced Staking Features
|
||||||
|
- APY calculations - Real-time Annual Percentage Yield based on validator performance
|
||||||
|
- Commission tracking - View validator commission rates and adjusted returns
|
||||||
|
- Staking statistics - Network-wide staking data and participation rates
|
||||||
|
- Auto-refresh data - Automatic updates every 30 seconds for live information
|
||||||
|
- Transaction validation - Comprehensive validation for all staking operations
|
||||||
|
|
||||||
|
#### Professional UI/UX
|
||||||
|
- Staking navigation tab - Dedicated staking section in main navigation
|
||||||
|
- Interactive validator list - Sortable table with delegation actions
|
||||||
|
- Modal dialog system - Professional forms for delegation, undelegation, and validator creation
|
||||||
|
- Responsive design - Mobile-friendly interface that works on all screen sizes
|
||||||
|
- Custom staking themes - Beautiful gradient designs and professional styling
|
||||||
|
|
||||||
|
#### Backend Integration
|
||||||
|
- Enhanced RPC client - New staking-specific RPC methods for blockchain communication
|
||||||
|
- Validator queries - Fetch active validators, staking info, and reward data
|
||||||
|
- Transaction support - Handle delegate, undelegate, claim, and validator creation transactions
|
||||||
|
- IPC handlers - Secure communication between frontend and blockchain node
|
||||||
|
|
||||||
|
#### Security & Validation
|
||||||
|
- Input validation - Comprehensive validation for all staking parameters
|
||||||
|
- Minimum requirements - Enforce minimum staking amounts and validator requirements
|
||||||
|
- Error handling - Detailed error messages and user feedback
|
||||||
|
- Transaction signing - Secure transaction signing with wallet private keys
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Updated branding - Complete rebrand from Etho Protocol to PaperclipChain
|
||||||
|
- Enhanced navigation - Added staking tab to main navigation menu
|
||||||
|
- Improved RPC client - Extended paperclip-rpc.js with staking functionality
|
||||||
|
- Updated documentation - Comprehensive README with staking guide and setup instructions
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
- New Files Added:
|
||||||
|
- `renderer/staking.js` - Complete staking interface logic
|
||||||
|
- `assets/templates/staking.html` - Professional staking UI template
|
||||||
|
- `assets/styles/staking.css` - Custom staking interface styling
|
||||||
|
- Enhanced Files:
|
||||||
|
- `modules/paperclip-rpc.js` - Added 5 new staking RPC methods
|
||||||
|
- `index.html` - Integrated staking navigation and CSS
|
||||||
|
- `renderer/maingui.js` - Added staking navigation handler
|
||||||
|
- Package Updates:
|
||||||
|
- Updated description to include DeFi staking capabilities
|
||||||
|
- Added staking-related keywords for discoverability
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
- All existing dependencies maintained
|
||||||
|
- No new external dependencies required
|
||||||
|
- Compatible with Node.js 14+ and Electron 11.5.0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Previous Versions
|
||||||
|
|
||||||
|
### [0.9.x] - Pre-Staking Releases
|
||||||
|
- Basic wallet functionality
|
||||||
|
- Transaction management
|
||||||
|
- Address book features
|
||||||
|
- Initial PaperclipChain integration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Note: This changelog focuses on the major staking release. For detailed technical changes, see the git commit history.
|
||||||
190
README.md
@@ -1,35 +1,189 @@
|
|||||||
# Etho Protocol Desktop Wallet
|
# PaperclipWallet - Desktop Wallet for PaperclipChain
|
||||||
|
|
||||||
**Clone and run to see it in action.**
|
A modern, feature-rich desktop wallet for PaperclipChain with comprehensive DeFi staking capabilities.
|
||||||
|
|
||||||
This is a desktop wallet for the [Etho Protocol](https://ethoprotocol.com/) project.
|
PaperclipWallet is the official desktop wallet for PaperclipChain, providing secure wallet management, transaction handling, and advanced staking features for earning rewards through validator delegation.
|
||||||
|
|
||||||
## To Use
|
## Features
|
||||||
|
|
||||||
To clone and run this repository you'll need [Git](https://git-scm.com) and [Node.js](https://nodejs.org/en/download/) (which comes with [npm](http://npmjs.com)) installed on your computer. From your command line:
|
### Core Wallet Functions
|
||||||
|
- Secure wallet management - Create, import, and manage multiple CLIPS wallets
|
||||||
|
- Transaction history - View detailed transaction history and status
|
||||||
|
- Address book - Manage frequently used addresses
|
||||||
|
- Balance tracking - Real-time CLIPS balance updates
|
||||||
|
- Send/Receive - Simple and secure CLIPS transfers
|
||||||
|
|
||||||
|
### Advanced DeFi Staking
|
||||||
|
- Validator delegation - Stake CLIPS to active validators and earn rewards
|
||||||
|
- Reward claiming - One-click claiming of accumulated staking rewards
|
||||||
|
- Validator creation - Become a validator with minimum 10,000 CLIPS stake
|
||||||
|
- Real-time statistics - Live network staking data and APY calculations
|
||||||
|
- Multi-validator support - Delegate to multiple validators simultaneously
|
||||||
|
|
||||||
|
### Professional Interface
|
||||||
|
- Modern dashboard - Clean, intuitive interface with real-time data
|
||||||
|
- Responsive design - Works seamlessly on all screen sizes
|
||||||
|
- Theme support - Professional dark and light themes
|
||||||
|
- Data visualization - Charts and graphs for staking performance
|
||||||
|
|
||||||
|
## Installation & Setup
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- [Git](https://git-scm.com)
|
||||||
|
- [Node.js](https://nodejs.org/en/download/) (v14 or higher)
|
||||||
|
- [Yarn](https://yarnpkg.com/) or npm
|
||||||
|
- Running PaperclipChain node (for blockchain connectivity)
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone this repository
|
# Clone this repository
|
||||||
git clone https://github.com/Ether1Project/Ether1DesktopWallet.git
|
git clone https://git.takoyaki.cool/matt/paperclip-wallet.git
|
||||||
# Go into the repository
|
cd paperclip-wallet
|
||||||
cd Ether1DesktopWallet
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
yarn install
|
yarn install
|
||||||
# Run the app
|
|
||||||
yarn run start
|
# Start the wallet
|
||||||
|
yarn start
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: If you're using Linux Bash for Windows, [see this guide](https://www.howtogeek.com/261575/how-to-run-graphical-linux-desktop-applications-from-windows-10s-bash-shell/) or use `node` from the command prompt.
|
### Building for Distribution
|
||||||
|
|
||||||
## Resources for Learning Electron
|
```bash
|
||||||
|
# Build for your platform
|
||||||
|
yarn run dist-linux # Linux
|
||||||
|
yarn run dist-win # Windows
|
||||||
|
yarn run dist-osx # macOS
|
||||||
|
|
||||||
- [electronjs.org/docs](https://electronjs.org/docs) - all of Electron's documentation
|
# Build for all platforms
|
||||||
- [electronjs.org/community#boilerplates](https://electronjs.org/community#boilerplates) - sample starter apps created by the community
|
yarn run pack-win && yarn run pack-linux && yarn run pack-osx
|
||||||
- [electron/electron-quick-start](https://github.com/electron/electron-quick-start) - a very basic starter Electron app
|
```
|
||||||
- [electron/simple-samples](https://github.com/electron/simple-samples) - small applications with ideas for taking them further
|
|
||||||
- [electron/electron-api-demos](https://github.com/electron/electron-api-demos) - an Electron app that teaches you how to use Electron
|
## Configuration
|
||||||
- [hokein/electron-sample-apps](https://github.com/hokein/electron-sample-apps) - small demo apps for the various Electron APIs
|
|
||||||
|
### Node Connection
|
||||||
|
By default, the wallet connects to `localhost:26657` (PaperclipChain RPC). To connect to a different node:
|
||||||
|
|
||||||
|
1. Open wallet settings
|
||||||
|
2. Update the RPC URL to your PaperclipChain node
|
||||||
|
3. Restart the wallet
|
||||||
|
|
||||||
|
### Network Requirements
|
||||||
|
- **Mainnet**: Connect to a synced PaperclipChain mainnet node
|
||||||
|
- **Testnet**: Use testnet node for development and testing
|
||||||
|
- **Local**: Run a local PaperclipChain node for development
|
||||||
|
|
||||||
|
## Staking Guide
|
||||||
|
|
||||||
|
### How to Stake CLIPS
|
||||||
|
|
||||||
|
1. **Select Validators**
|
||||||
|
- Navigate to the "Staking" tab
|
||||||
|
- Browse active validators
|
||||||
|
- Compare commission rates and APY
|
||||||
|
|
||||||
|
2. **Delegate Tokens**
|
||||||
|
- Click "Delegate" on your chosen validator
|
||||||
|
- Enter the amount to stake (minimum 100 CLIPS)
|
||||||
|
- Confirm the transaction
|
||||||
|
|
||||||
|
3. **Earn Rewards**
|
||||||
|
- Rewards accumulate automatically
|
||||||
|
- Claim rewards anytime via "Claim Rewards" button
|
||||||
|
- Rewards are distributed based on validator performance
|
||||||
|
|
||||||
|
### Becoming a Validator
|
||||||
|
|
||||||
|
1. **Minimum Requirements**
|
||||||
|
- 10,000 CLIPS minimum stake
|
||||||
|
- Running PaperclipChain validator node
|
||||||
|
- Stable internet connection
|
||||||
|
|
||||||
|
2. **Setup Process**
|
||||||
|
- Click "Become Validator" in staking tab
|
||||||
|
- Set your commission rate (0-50%)
|
||||||
|
- Provide initial stake
|
||||||
|
- Submit validator creation transaction
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
### Best Practices
|
||||||
|
- Backup wallets - Always backup your wallet files and private keys
|
||||||
|
- Secure storage - Store backups in multiple secure locations
|
||||||
|
- Regular updates - Keep the wallet updated to the latest version
|
||||||
|
- Network security - Only connect to trusted PaperclipChain nodes
|
||||||
|
|
||||||
|
### Private Key Management
|
||||||
|
- Private keys are stored locally and encrypted
|
||||||
|
- Never share your private keys or wallet files
|
||||||
|
- Use strong passwords for wallet encryption
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
```
|
||||||
|
paperclip-wallet/
|
||||||
|
├── main.js # Electron main process
|
||||||
|
├── index.html # Main application window
|
||||||
|
├── renderer/ # Frontend logic
|
||||||
|
│ ├── staking.js # Staking interface
|
||||||
|
│ ├── wallets.js # Wallet management
|
||||||
|
│ └── ...
|
||||||
|
├── modules/ # Backend modules
|
||||||
|
│ ├── paperclip-rpc.js # Blockchain RPC client
|
||||||
|
│ ├── clips-crypto.js # Cryptographic functions
|
||||||
|
│ └── ...
|
||||||
|
└── assets/ # UI assets and templates
|
||||||
|
├── templates/ # HTML templates
|
||||||
|
├── styles/ # CSS styling
|
||||||
|
└── images/ # Icons and graphics
|
||||||
|
```
|
||||||
|
|
||||||
|
### Technology Stack
|
||||||
|
- Frontend: Electron, jQuery, Handlebars
|
||||||
|
- Backend: Node.js, Ed25519 cryptography
|
||||||
|
- Blockchain: PaperclipChain RPC integration
|
||||||
|
- Styling: Custom CSS with Material Design elements
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**Wallet won't connect to node**
|
||||||
|
- Verify PaperclipChain node is running and synced
|
||||||
|
- Check RPC URL in settings (default: `localhost:26657`)
|
||||||
|
- Ensure firewall allows connections
|
||||||
|
|
||||||
|
**Staking transactions fail**
|
||||||
|
- Verify sufficient CLIPS balance for transaction + gas fees
|
||||||
|
- Check validator is active and accepting delegations
|
||||||
|
- Ensure wallet is unlocked and private key is accessible
|
||||||
|
|
||||||
|
**Balance not updating**
|
||||||
|
- Refresh the wallet interface
|
||||||
|
- Verify node is fully synced
|
||||||
|
- Check transaction was properly broadcast
|
||||||
|
|
||||||
|
### Support
|
||||||
|
For technical support and bug reports, please visit:
|
||||||
|
- Repository: https://git.takoyaki.cool/matt/paperclip-wallet
|
||||||
|
- Issues: Create a new issue with detailed information
|
||||||
|
- Documentation: Check PaperclipChain main repository
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
We welcome contributions! Please:
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Make your changes
|
||||||
|
4. Test thoroughly
|
||||||
|
5. Submit a pull request
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[CC0 1.0 (Public Domain)](LICENSE.md)
|
[CC0 1.0 (Public Domain)](LICENSE.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
PaperclipWallet - Secure, Modern, and Feature-Rich Desktop Wallet for PaperclipChain
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 3.3 MiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 16 MiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 68 KiB |
@@ -1,12 +1,12 @@
|
|||||||
.btn-etho {
|
.btn-etho {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
background-color: #840032;
|
background-color: #25D4DC;
|
||||||
border-color: #103024;
|
border-color: #103024;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-etho:hover {
|
.btn-etho:hover {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: 0px 2px 10px 5px #840032;
|
box-shadow: 0px 2px 10px 5px #25D4DC;
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,12 +41,12 @@ fieldset[disabled] .btn-etho:active,
|
|||||||
.btn-etho.disabled.active,
|
.btn-etho.disabled.active,
|
||||||
.btn-etho[disabled].active,
|
.btn-etho[disabled].active,
|
||||||
fieldset[disabled] .btn-etho.active {
|
fieldset[disabled] .btn-etho.active {
|
||||||
background-color: #840032;
|
background-color: #25D4DC;
|
||||||
border-color: #450118;
|
border-color: #450118;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-etho .badge {
|
.btn-etho .badge {
|
||||||
color: #840032;
|
color: #25D4DC;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ fieldset[disabled] .btn-etho.active {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: #840032;
|
background-color: #25D4DC;
|
||||||
border: none;
|
border: none;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
@@ -71,13 +71,13 @@ fieldset[disabled] .btn-etho.active {
|
|||||||
|
|
||||||
.button2:hover {
|
.button2:hover {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: 0px 2px 10px 5px #840032;
|
box-shadow: 0px 2px 10px 5px #25D4DC;
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button2:after {
|
.button2:after {
|
||||||
content: "";
|
content: "";
|
||||||
background: #840032;
|
background: #25D4DC;
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
padding-top: 300%;
|
padding-top: 300%;
|
||||||
@@ -106,7 +106,7 @@ fieldset[disabled] .btn-etho.active {
|
|||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: #840032;
|
background-color: #25D4DC;
|
||||||
border: none;
|
border: none;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
width: 110px;
|
width: 110px;
|
||||||
@@ -119,13 +119,13 @@ fieldset[disabled] .btn-etho.active {
|
|||||||
|
|
||||||
.button3:hover {
|
.button3:hover {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: 0px 2px 10px 5px #840032;
|
box-shadow: 0px 2px 10px 5px #25D4DC;
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button3:after {
|
.button3:after {
|
||||||
content: "";
|
content: "";
|
||||||
background: #840032;
|
background: #25D4DC;
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
padding-top: 300%;
|
padding-top: 300%;
|
||||||
@@ -154,7 +154,7 @@ fieldset[disabled] .btn-etho.active {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: #840032;
|
background-color: #25D4DC;
|
||||||
border: none;
|
border: none;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
@@ -167,13 +167,13 @@ fieldset[disabled] .btn-etho.active {
|
|||||||
|
|
||||||
.button4:hover {
|
.button4:hover {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: 0px 2px 10px 5px #840032;
|
box-shadow: 0px 2px 10px 5px #25D4DC;
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button4:after {
|
.button4:after {
|
||||||
content: "";
|
content: "";
|
||||||
background: #840032;
|
background: #25D4DC;
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
padding-top: 300%;
|
padding-top: 300%;
|
||||||
@@ -194,3 +194,4 @@ fieldset[disabled] .btn-etho.active {
|
|||||||
.button4:focus {
|
.button4:focus {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-content li>a, .dropdown-content li>span {
|
.dropdown-content li>a, .dropdown-content li>span {
|
||||||
color: rgb(36, 13, 21);
|
color: rgb(13, 36, 33);
|
||||||
}
|
}
|
||||||
|
|
||||||
#sendFeeRange {
|
#sendFeeRange {
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#tableTransactionsForAll .fa-sign-out-alt {
|
#tableTransactionsForAll .fa-sign-out-alt {
|
||||||
color: red;
|
color: rgb(0, 238, 255);
|
||||||
}
|
}
|
||||||
|
|
||||||
#tableTransactionsForAll .fa-arrow-left {
|
#tableTransactionsForAll .fa-arrow-left {
|
||||||
@@ -45,3 +45,4 @@
|
|||||||
#tableTransactionsForAll .fa-check {
|
#tableTransactionsForAll .fa-check {
|
||||||
color: #228B22;
|
color: #228B22;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
108
assets/styles/materialize.min.css
vendored
@@ -796,11 +796,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.teal.lighten-1 {
|
.teal.lighten-1 {
|
||||||
background-color: #840032 !important
|
background-color: #24F9DD !important
|
||||||
}
|
}
|
||||||
|
|
||||||
.teal-text.text-lighten-1 {
|
.teal-text.text-lighten-1 {
|
||||||
color: #840032 !important
|
color: #24F9DD !important
|
||||||
}
|
}
|
||||||
|
|
||||||
.teal.darken-1 {
|
.teal.darken-1 {
|
||||||
@@ -2446,7 +2446,7 @@ td, th {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.collection .collection-item.active {
|
.collection .collection-item.active {
|
||||||
background-color: #840032;
|
background-color: #24F9DD;
|
||||||
color: #eafaf9
|
color: #eafaf9
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2458,7 +2458,7 @@ td, th {
|
|||||||
display: block;
|
display: block;
|
||||||
-webkit-transition: .25s;
|
-webkit-transition: .25s;
|
||||||
transition: .25s;
|
transition: .25s;
|
||||||
color: #840032
|
color: #24F9DD
|
||||||
}
|
}
|
||||||
|
|
||||||
.collection a.collection-item:not(.active):hover {
|
.collection a.collection-item:not(.active):hover {
|
||||||
@@ -2481,7 +2481,7 @@ td, th {
|
|||||||
|
|
||||||
.secondary-content {
|
.secondary-content {
|
||||||
float: right;
|
float: right;
|
||||||
color: #840032
|
color: #24F9DD
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapsible .collection {
|
.collapsible .collection {
|
||||||
@@ -2520,13 +2520,13 @@ td, th {
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background-color: #840032;
|
background-color: #24F9DD;
|
||||||
-webkit-transition: width .3s linear;
|
-webkit-transition: width .3s linear;
|
||||||
transition: width .3s linear
|
transition: width .3s linear
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress .indeterminate {
|
.progress .indeterminate {
|
||||||
background-color: #840032
|
background-color: #24F9DD
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress .indeterminate:before {
|
.progress .indeterminate:before {
|
||||||
@@ -2693,7 +2693,7 @@ span.badge.new {
|
|||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #840032;
|
background-color: #24F9DD;
|
||||||
border-radius: 2px
|
border-radius: 2px
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4207,7 +4207,7 @@ small {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card .card-title {
|
.card .card-title {
|
||||||
font-size: 24px;
|
font-size: 19px;
|
||||||
font-weight: 300
|
font-weight: 300
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4355,6 +4355,11 @@ small {
|
|||||||
line-height: 32px
|
line-height: 32px
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cardValue {
|
||||||
|
font-size:33px;
|
||||||
|
margin-bottom:8px;
|
||||||
|
}
|
||||||
|
|
||||||
.card .card-action {
|
.card .card-action {
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
border-top: 1px solid rgba(160, 160, 160, 0.2);
|
border-top: 1px solid rgba(160, 160, 160, 0.2);
|
||||||
@@ -4657,7 +4662,7 @@ small {
|
|||||||
.btn, .btn-large, .btn-small {
|
.btn, .btn-large, .btn-small {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #840032;
|
background-color: #24F9DD;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
letter-spacing: .5px;
|
letter-spacing: .5px;
|
||||||
-webkit-transition: background-color .2s ease-out;
|
-webkit-transition: background-color .2s ease-out;
|
||||||
@@ -4679,7 +4684,7 @@ small {
|
|||||||
height: 40px;
|
height: 40px;
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background-color: #840032;
|
background-color: #24F9DD;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
-webkit-transition: background-color .3s;
|
-webkit-transition: background-color .3s;
|
||||||
transition: background-color .3s;
|
transition: background-color .3s;
|
||||||
@@ -4688,7 +4693,7 @@ small {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-floating:hover {
|
.btn-floating:hover {
|
||||||
background-color: #840032
|
background-color: #24F9DD
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-floating:before {
|
.btn-floating:before {
|
||||||
@@ -4890,7 +4895,7 @@ button.btn-floating {
|
|||||||
z-index: -1;
|
z-index: -1;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
background-color: #840032;
|
background-color: #24F9DD;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
-webkit-transform: scale(0);
|
-webkit-transform: scale(0);
|
||||||
transform: scale(0)
|
transform: scale(0)
|
||||||
@@ -4990,7 +4995,7 @@ button.btn-floating {
|
|||||||
|
|
||||||
.dropdown-content li>a, .dropdown-content li>span {
|
.dropdown-content li>a, .dropdown-content li>span {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: #840032;
|
color: #24F9DD;
|
||||||
display: block;
|
display: block;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
padding: 14px 16px
|
padding: 14px 16px
|
||||||
@@ -5357,7 +5362,7 @@ body.keyboard-focused .dropdown-content li:focus {
|
|||||||
|
|
||||||
.chip:focus {
|
.chip:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
background-color: #840032;
|
background-color: #24F9DD;
|
||||||
color: #fff
|
color: #fff
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5390,9 +5395,9 @@ body.keyboard-focused .dropdown-content li:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chips.focus {
|
.chips.focus {
|
||||||
border-bottom: 1px solid #840032;
|
border-bottom: 1px solid #24F9DD;
|
||||||
-webkit-box-shadow: 0 1px 0 0 #840032;
|
-webkit-box-shadow: 0 1px 0 0 #24F9DD;
|
||||||
box-shadow: 0 1px 0 0 #840032
|
box-shadow: 0 1px 0 0 #24F9DD
|
||||||
}
|
}
|
||||||
|
|
||||||
.chips:hover {
|
.chips:hover {
|
||||||
@@ -5547,9 +5552,9 @@ input:not([type]):disabled+label, input:not([type])[readonly="readonly"]+label,
|
|||||||
}
|
}
|
||||||
|
|
||||||
input:not([type]):focus:not([readonly]), input[type=text]:not(.browser-default):focus:not([readonly]), input[type=password]:not(.browser-default):focus:not([readonly]), input[type=email]:not(.browser-default):focus:not([readonly]), input[type=url]:not(.browser-default):focus:not([readonly]), input[type=time]:not(.browser-default):focus:not([readonly]), input[type=date]:not(.browser-default):focus:not([readonly]), input[type=datetime]:not(.browser-default):focus:not([readonly]), input[type=datetime-local]:not(.browser-default):focus:not([readonly]), input[type=tel]:not(.browser-default):focus:not([readonly]), input[type=number]:not(.browser-default):focus:not([readonly]), input[type=search]:not(.browser-default):focus:not([readonly]), textarea.materialize-textarea:focus:not([readonly]) {
|
input:not([type]):focus:not([readonly]), input[type=text]:not(.browser-default):focus:not([readonly]), input[type=password]:not(.browser-default):focus:not([readonly]), input[type=email]:not(.browser-default):focus:not([readonly]), input[type=url]:not(.browser-default):focus:not([readonly]), input[type=time]:not(.browser-default):focus:not([readonly]), input[type=date]:not(.browser-default):focus:not([readonly]), input[type=datetime]:not(.browser-default):focus:not([readonly]), input[type=datetime-local]:not(.browser-default):focus:not([readonly]), input[type=tel]:not(.browser-default):focus:not([readonly]), input[type=number]:not(.browser-default):focus:not([readonly]), input[type=search]:not(.browser-default):focus:not([readonly]), textarea.materialize-textarea:focus:not([readonly]) {
|
||||||
border-bottom: 1px solid #840032;
|
border-bottom: 1px solid #24F9DD;
|
||||||
-webkit-box-shadow: 0 1px 0 0 #840032;
|
-webkit-box-shadow: 0 1px 0 0 #24F9DD;
|
||||||
box-shadow: 0 1px 0 0 #840032
|
box-shadow: 0 1px 0 0 #24F9DD
|
||||||
}
|
}
|
||||||
|
|
||||||
input:not([type]):focus:not([readonly])+label, input[type=text]:not(.browser-default):focus:not([readonly])+label, input[type=password]:not(.browser-default):focus:not([readonly])+label, input[type=email]:not(.browser-default):focus:not([readonly])+label, input[type=url]:not(.browser-default):focus:not([readonly])+label, input[type=time]:not(.browser-default):focus:not([readonly])+label, input[type=date]:not(.browser-default):focus:not([readonly])+label, input[type=datetime]:not(.browser-default):focus:not([readonly])+label, input[type=datetime-local]:not(.browser-default):focus:not([readonly])+label, input[type=tel]:not(.browser-default):focus:not([readonly])+label, input[type=number]:not(.browser-default):focus:not([readonly])+label, input[type=search]:not(.browser-default):focus:not([readonly])+label, textarea.materialize-textarea:focus:not([readonly])+label {
|
input:not([type]):focus:not([readonly])+label, input[type=text]:not(.browser-default):focus:not([readonly])+label, input[type=password]:not(.browser-default):focus:not([readonly])+label, input[type=email]:not(.browser-default):focus:not([readonly])+label, input[type=url]:not(.browser-default):focus:not([readonly])+label, input[type=time]:not(.browser-default):focus:not([readonly])+label, input[type=date]:not(.browser-default):focus:not([readonly])+label, input[type=datetime]:not(.browser-default):focus:not([readonly])+label, input[type=datetime-local]:not(.browser-default):focus:not([readonly])+label, input[type=tel]:not(.browser-default):focus:not([readonly])+label, input[type=number]:not(.browser-default):focus:not([readonly])+label, input[type=search]:not(.browser-default):focus:not([readonly])+label, textarea.materialize-textarea:focus:not([readonly])+label {
|
||||||
@@ -5691,7 +5696,7 @@ input:not([type])+label:after, input[type=text]:not(.browser-default)+label:afte
|
|||||||
}
|
}
|
||||||
|
|
||||||
.input-field .prefix.active {
|
.input-field .prefix.active {
|
||||||
color: #840032
|
color: #24F9DD
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-field .prefix~input, .input-field .prefix~textarea, .input-field .prefix~label, .input-field .prefix~.validate~label, .input-field .prefix~.helper-text, .input-field .prefix~.autocomplete-content {
|
.input-field .prefix~input, .input-field .prefix~textarea, .input-field .prefix~label, .input-field .prefix~.validate~label, .input-field .prefix~.helper-text, .input-field .prefix~.autocomplete-content {
|
||||||
@@ -5857,11 +5862,11 @@ textarea.materialize-textarea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
[type="radio"]:checked+span:after, [type="radio"].with-gap:checked+span:before, [type="radio"].with-gap:checked+span:after {
|
[type="radio"]:checked+span:after, [type="radio"].with-gap:checked+span:before, [type="radio"].with-gap:checked+span:after {
|
||||||
border: 2px solid #840032
|
border: 2px solid #24F9DD
|
||||||
}
|
}
|
||||||
|
|
||||||
[type="radio"]:checked+span:after, [type="radio"].with-gap:checked+span:after {
|
[type="radio"]:checked+span:after, [type="radio"].with-gap:checked+span:after {
|
||||||
background-color: #840032
|
background-color: #24F9DD
|
||||||
}
|
}
|
||||||
|
|
||||||
[type="radio"]:checked+span:after {
|
[type="radio"]:checked+span:after {
|
||||||
@@ -5969,8 +5974,8 @@ textarea.materialize-textarea {
|
|||||||
height: 22px;
|
height: 22px;
|
||||||
border-top: 2px solid transparent;
|
border-top: 2px solid transparent;
|
||||||
border-left: 2px solid transparent;
|
border-left: 2px solid transparent;
|
||||||
border-right: 2px solid #840032;
|
border-right: 2px solid #24F9DD;
|
||||||
border-bottom: 2px solid #840032;
|
border-bottom: 2px solid #24F9DD;
|
||||||
-webkit-transform: rotate(40deg);
|
-webkit-transform: rotate(40deg);
|
||||||
transform: rotate(40deg);
|
transform: rotate(40deg);
|
||||||
-webkit-backface-visibility: hidden;
|
-webkit-backface-visibility: hidden;
|
||||||
@@ -5991,7 +5996,7 @@ textarea.materialize-textarea {
|
|||||||
height: 22px;
|
height: 22px;
|
||||||
border-top: none;
|
border-top: none;
|
||||||
border-left: none;
|
border-left: none;
|
||||||
border-right: 2px solid #840032;
|
border-right: 2px solid #24F9DD;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
-webkit-transform: rotate(90deg);
|
-webkit-transform: rotate(90deg);
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
@@ -6059,8 +6064,8 @@ textarea.materialize-textarea {
|
|||||||
top: 0;
|
top: 0;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
border: 2px solid #840032;
|
border: 2px solid #24F9DD;
|
||||||
background-color: #840032;
|
background-color: #24F9DD;
|
||||||
z-index: 0
|
z-index: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6072,8 +6077,8 @@ textarea.materialize-textarea {
|
|||||||
|
|
||||||
[type="checkbox"].filled-in.tabbed:checked:focus+span:not(.lever):after {
|
[type="checkbox"].filled-in.tabbed:checked:focus+span:not(.lever):after {
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background-color: #840032;
|
background-color: #24F9DD;
|
||||||
border-color: #840032
|
border-color: #24F9DD
|
||||||
}
|
}
|
||||||
|
|
||||||
[type="checkbox"].filled-in:disabled:not(:checked)+span:not(.lever):before {
|
[type="checkbox"].filled-in:disabled:not(:checked)+span:not(.lever):before {
|
||||||
@@ -6122,7 +6127,7 @@ textarea.materialize-textarea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.switch label input[type=checkbox]:checked+.lever:after {
|
.switch label input[type=checkbox]:checked+.lever:after {
|
||||||
background-color: #840032
|
background-color: #24F9DD
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch label .lever {
|
.switch label .lever {
|
||||||
@@ -6194,7 +6199,7 @@ select {
|
|||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
color: #000;
|
color: #000;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 3px solid #840032;
|
border: 2px ridge #6dbb9b;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
@@ -6234,7 +6239,7 @@ select {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.select-wrapper input.select-dropdown:focus {
|
.select-wrapper input.select-dropdown:focus {
|
||||||
border-bottom: 1px solid #840032
|
border-bottom: 1px solid #24F9DD
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-wrapper .caret {
|
.select-wrapper .caret {
|
||||||
@@ -6402,7 +6407,7 @@ input[type=range]+.thumb {
|
|||||||
height: 0;
|
height: 0;
|
||||||
width: 0;
|
width: 0;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: #840032;
|
background-color: #24F9DD;
|
||||||
margin-left: 7px;
|
margin-left: 7px;
|
||||||
-webkit-transform-origin: 50% 50%;
|
-webkit-transform-origin: 50% 50%;
|
||||||
transform-origin: 50% 50%;
|
transform-origin: 50% 50%;
|
||||||
@@ -6414,7 +6419,7 @@ input[type=range]+.thumb .value {
|
|||||||
display: block;
|
display: block;
|
||||||
width: 30px;
|
width: 30px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #840032;
|
color: #24F9DD;
|
||||||
font-size: 0;
|
font-size: 0;
|
||||||
-webkit-transform: rotate(45deg);
|
-webkit-transform: rotate(45deg);
|
||||||
transform: rotate(45deg)
|
transform: rotate(45deg)
|
||||||
@@ -6446,13 +6451,13 @@ input[type=range]::-webkit-slider-thumb {
|
|||||||
height: 14px;
|
height: 14px;
|
||||||
width: 14px;
|
width: 14px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #840032;
|
background: #24F9DD;
|
||||||
-webkit-transition: -webkit-box-shadow .3s;
|
-webkit-transition: -webkit-box-shadow .3s;
|
||||||
transition: -webkit-box-shadow .3s;
|
transition: -webkit-box-shadow .3s;
|
||||||
transition: box-shadow .3s;
|
transition: box-shadow .3s;
|
||||||
transition: box-shadow .3s, -webkit-box-shadow .3s;
|
transition: box-shadow .3s, -webkit-box-shadow .3s;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
background-color: #840032;
|
background-color: #24F9DD;
|
||||||
-webkit-transform-origin: 50% 50%;
|
-webkit-transform-origin: 50% 50%;
|
||||||
transform-origin: 50% 50%;
|
transform-origin: 50% 50%;
|
||||||
margin: -5px 0 0 0
|
margin: -5px 0 0 0
|
||||||
@@ -6482,7 +6487,7 @@ input[type=range]::-moz-range-thumb {
|
|||||||
height: 14px;
|
height: 14px;
|
||||||
width: 14px;
|
width: 14px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #840032;
|
background: #24F9DD;
|
||||||
-webkit-transition: -webkit-box-shadow .3s;
|
-webkit-transition: -webkit-box-shadow .3s;
|
||||||
transition: -webkit-box-shadow .3s;
|
transition: -webkit-box-shadow .3s;
|
||||||
transition: box-shadow .3s;
|
transition: box-shadow .3s;
|
||||||
@@ -6520,7 +6525,7 @@ input[type=range]::-ms-thumb {
|
|||||||
height: 14px;
|
height: 14px;
|
||||||
width: 14px;
|
width: 14px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #840032;
|
background: #24F9DD;
|
||||||
-webkit-transition: -webkit-box-shadow .3s;
|
-webkit-transition: -webkit-box-shadow .3s;
|
||||||
transition: -webkit-box-shadow .3s;
|
transition: -webkit-box-shadow .3s;
|
||||||
transition: box-shadow .3s;
|
transition: box-shadow .3s;
|
||||||
@@ -6637,7 +6642,7 @@ input[type=range]::-ms-thumb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidenav li>a.btn-floating:hover {
|
.sidenav li>a.btn-floating:hover {
|
||||||
background-color: #840032
|
background-color: #24F9DD
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidenav li>a>i, .sidenav li>a>[class^="mdi-"], .sidenav li>a li>a>[class*="mdi-"], .sidenav li>a>i.material-icons {
|
.sidenav li>a>i, .sidenav li>a>[class^="mdi-"], .sidenav li>a li>a>[class*="mdi-"], .sidenav li>a>i.material-icons {
|
||||||
@@ -6823,7 +6828,7 @@ input[type=range]::-ms-thumb {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
border-color: #840032
|
border-color: #24F9DD
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner-blue, .spinner-blue-only {
|
.spinner-blue, .spinner-blue-only {
|
||||||
@@ -7736,7 +7741,7 @@ input[type=range]::-ms-thumb {
|
|||||||
-webkit-flex: 1 auto;
|
-webkit-flex: 1 auto;
|
||||||
-ms-flex: 1 auto;
|
-ms-flex: 1 auto;
|
||||||
flex: 1 auto;
|
flex: 1 auto;
|
||||||
background-color: #840032;
|
background-color: #24F9DD;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding: 20px 22px;
|
padding: 20px 22px;
|
||||||
font-weight: 500
|
font-weight: 500
|
||||||
@@ -7793,11 +7798,11 @@ input[type=range]::-ms-thumb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.datepicker-table td.is-today {
|
.datepicker-table td.is-today {
|
||||||
color: #840032
|
color: #24F9DD
|
||||||
}
|
}
|
||||||
|
|
||||||
.datepicker-table td.is-selected {
|
.datepicker-table td.is-selected {
|
||||||
background-color: #840032;
|
background-color: #24F9DD;
|
||||||
color: #fff
|
color: #fff
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7837,7 +7842,7 @@ input[type=range]::-ms-thumb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.datepicker-cancel, .datepicker-clear, .datepicker-today, .datepicker-done {
|
.datepicker-cancel, .datepicker-clear, .datepicker-today, .datepicker-done {
|
||||||
color: #840032;
|
color: #24F9DD;
|
||||||
padding: 0 1rem
|
padding: 0 1rem
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7901,7 +7906,7 @@ input[type=range]::-ms-thumb {
|
|||||||
-webkit-flex: 1 auto;
|
-webkit-flex: 1 auto;
|
||||||
-ms-flex: 1 auto;
|
-ms-flex: 1 auto;
|
||||||
flex: 1 auto;
|
flex: 1 auto;
|
||||||
background-color: #840032;
|
background-color: #24F9DD;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
font-weight: 300
|
font-weight: 300
|
||||||
}
|
}
|
||||||
@@ -8017,7 +8022,7 @@ input[type=range]::-ms-thumb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.timepicker-canvas line {
|
.timepicker-canvas line {
|
||||||
stroke: #840032;
|
stroke: #24F9DD;
|
||||||
stroke-width: 4;
|
stroke-width: 4;
|
||||||
stroke-linecap: round
|
stroke-linecap: round
|
||||||
}
|
}
|
||||||
@@ -8028,12 +8033,12 @@ input[type=range]::-ms-thumb {
|
|||||||
|
|
||||||
.timepicker-canvas-bearing {
|
.timepicker-canvas-bearing {
|
||||||
stroke: none;
|
stroke: none;
|
||||||
fill: #840032
|
fill: #24F9DD
|
||||||
}
|
}
|
||||||
|
|
||||||
.timepicker-canvas-bg {
|
.timepicker-canvas-bg {
|
||||||
stroke: none;
|
stroke: none;
|
||||||
fill: #840032
|
fill: #24F9DD
|
||||||
}
|
}
|
||||||
|
|
||||||
.timepicker-footer {
|
.timepicker-footer {
|
||||||
@@ -8054,7 +8059,7 @@ input[type=range]::-ms-thumb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.timepicker-close {
|
.timepicker-close {
|
||||||
color: #840032
|
color: #24F9DD
|
||||||
}
|
}
|
||||||
|
|
||||||
.timepicker-clear, .timepicker-close {
|
.timepicker-clear, .timepicker-close {
|
||||||
@@ -8086,3 +8091,4 @@ input[type=range]::-ms-thumb {
|
|||||||
margin-top: 1.2rem
|
margin-top: 1.2rem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
386
assets/styles/staking.css
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
/* PaperclipChain Staking Interface Styles */
|
||||||
|
|
||||||
|
.staking-container {
|
||||||
|
padding: 20px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.staking-overview {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.staking-card {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.staking-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.staking-card h3 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.9;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.staking-card .amount {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.staking-card .subtitle {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.8;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.staking-card.success {
|
||||||
|
background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.staking-card.warning {
|
||||||
|
background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.staking-card.info {
|
||||||
|
background: linear-gradient(135deg, #2196F3 0%, #1976D2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #333;
|
||||||
|
border-bottom: 2px solid #eee;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-staking {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-staking:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-staking.warning {
|
||||||
|
background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-staking.warning:hover {
|
||||||
|
box-shadow: 0 5px 15px rgba(255, 152, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-staking.success {
|
||||||
|
background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-staking.success:hover {
|
||||||
|
box-shadow: 0 5px 15px rgba(76, 175, 80, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-staking.info {
|
||||||
|
background: linear-gradient(135deg, #2196F3 0%, #1976D2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-staking.info:hover {
|
||||||
|
box-shadow: 0 5px 15px rgba(33, 150, 243, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.validators-section {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.validators-section h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #333;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 2px solid #eee;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn:hover {
|
||||||
|
background: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validators-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validators-table th,
|
||||||
|
.validators-table td {
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validators-table th {
|
||||||
|
background: #f8f9fa;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validators-table tbody tr:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validator-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validator-info strong {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validator-info small {
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.active {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.inactive {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-validator-action {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 5px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-validator-action.delegate {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-validator-action.delegate:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-validator-action.undelegate {
|
||||||
|
background: #ffc107;
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-validator-action.undelegate:hover {
|
||||||
|
background: #e0a800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-validator-action:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal Styles */
|
||||||
|
.staking-modal .modal-body {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.staking-modal .form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.staking-modal label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.staking-modal input,
|
||||||
|
.staking-modal select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.staking-modal input:focus,
|
||||||
|
.staking-modal select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.staking-modal .help-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6c757d;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.staking-modal .alert {
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.staking-modal .alert.warning {
|
||||||
|
background: #fff3cd;
|
||||||
|
border: 1px solid #ffeaa7;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.staking-modal .alert.info {
|
||||||
|
background: #d1ecf1;
|
||||||
|
border: 1px solid #bee5eb;
|
||||||
|
color: #0c5460;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-buttons .btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-buttons .btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-buttons .btn-primary {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-buttons .btn:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.staking-overview {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validators-table {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validators-table th,
|
||||||
|
.validators-table td {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading States */
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading i {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Success/Error Messages */
|
||||||
|
.message {
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.success {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.error {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
html {
|
html {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: 'Fira Mono', monospace;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ body.pg-loaded>.inner {
|
|||||||
/* Style the sidebar - fixed full height */
|
/* Style the sidebar - fixed full height */
|
||||||
.sidebar {
|
.sidebar {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 160px;
|
width: 200px;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -34,8 +34,8 @@ body.pg-loaded>.inner {
|
|||||||
/* Style sidebar links */
|
/* Style sidebar links */
|
||||||
.sidebar a {
|
.sidebar a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 20px;
|
font-size: 18px;
|
||||||
color: #840032;
|
color: #25D4DC;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,11 +67,11 @@ body.pg-loaded>.inner {
|
|||||||
#mainContent {
|
#mainContent {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
left: 160px;
|
left: 184px;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
background-color: #212529;
|
background-color: #293838;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,12 +188,12 @@ body.pg-loaded>.inner {
|
|||||||
|
|
||||||
.modalDialog {
|
.modalDialog {
|
||||||
display: none;
|
display: none;
|
||||||
border-bottom: 3px solid #7A1336 !important;
|
border-bottom: 3px solid #28709F !important;
|
||||||
border-radius: 0px !important;
|
border-radius: 0px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.iziModal-header {
|
.iziModal-header {
|
||||||
background: #7A1336 !important;
|
background: #28709F !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#walletsToolbar {
|
#walletsToolbar {
|
||||||
@@ -223,7 +223,7 @@ div.loadingText {
|
|||||||
}
|
}
|
||||||
|
|
||||||
div.sidebar svg {
|
div.sidebar svg {
|
||||||
color: #7A1336;
|
color: #28709F;
|
||||||
}
|
}
|
||||||
|
|
||||||
#tableTransactionsForAll_filter {
|
#tableTransactionsForAll_filter {
|
||||||
@@ -252,7 +252,7 @@ div.sidebar svg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dataTables_scrollBody .transactionsBlockNum {
|
.dataTables_scrollBody .transactionsBlockNum {
|
||||||
color: #f92472;
|
color: #24f9dd;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,11 +286,12 @@ div.sidebar svg {
|
|||||||
|
|
||||||
.sendTXInfo {
|
.sendTXInfo {
|
||||||
height: 30px;
|
height: 30px;
|
||||||
|
color:#b1aa58;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sendTXInfo label {
|
.sendTXInfo label {
|
||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
font-size: 1.1em;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.txInfo label {
|
.txInfo label {
|
||||||
@@ -300,7 +301,7 @@ div.sidebar svg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.txInfo label+label {
|
.txInfo label+label {
|
||||||
color: #7A1336;
|
color: #28709F;
|
||||||
margin-left: 0px;
|
margin-left: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,13 +314,13 @@ div.sidebar svg {
|
|||||||
#toAddressInfo,
|
#toAddressInfo,
|
||||||
#valueToSendInfo,
|
#valueToSendInfo,
|
||||||
#feeToPayInfo {
|
#feeToPayInfo {
|
||||||
color: #ee6e73;
|
color: #24f9dd;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.accountName {
|
.accountName {
|
||||||
color: #7A1336;
|
color: #28709F;
|
||||||
}
|
}
|
||||||
|
|
||||||
.accountAddr {
|
.accountAddr {
|
||||||
@@ -430,7 +431,7 @@ div.noAddressWrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.etho-red {
|
.etho-red {
|
||||||
background-color: #7A1336 !important;
|
background-color: #28709F !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chartMarketPrice {
|
#chartMarketPrice {
|
||||||
@@ -440,3 +441,4 @@ div.noAddressWrapper {
|
|||||||
#addrQRCode {
|
#addrQRCode {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
.dt-button {
|
.dt-button {
|
||||||
margin-left: 10px !important;
|
margin-left: 10px !important;
|
||||||
color: #ffffff !important;
|
color: #ffffff !important;
|
||||||
background-color: #7A1336 !important;
|
background-color: #13737a !important;
|
||||||
border: 1px solid #450118 !important;
|
border: 1px solid #012c45 !important;
|
||||||
background-image: none !important;
|
background-image: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,8 +15,8 @@
|
|||||||
.dt-button.active,
|
.dt-button.active,
|
||||||
.open .dropdown-toggle.dt-button {
|
.open .dropdown-toggle.dt-button {
|
||||||
color: #ffffff !important;
|
color: #ffffff !important;
|
||||||
background-color: #B01549 !important;
|
background-color: #1594b0 !important;
|
||||||
border: 1px solid #450118 !important;
|
border: 1px solid #012445 !important;
|
||||||
background-image: none !important;
|
background-image: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,12 +40,12 @@ fieldset[disabled] .dt-button:active,
|
|||||||
.dt-button.disabled.active,
|
.dt-button.disabled.active,
|
||||||
.dt-button[disabled].active,
|
.dt-button[disabled].active,
|
||||||
fieldset[disabled] .dt-button.active {
|
fieldset[disabled] .dt-button.active {
|
||||||
background-color: #7A1336 !important;
|
background-color: #28709F !important;
|
||||||
border-color: #450118 !important;
|
border-color: #013045 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dt-button .badge {
|
.dt-button .badge {
|
||||||
color: #7A1336 !important;
|
color: #28709F !important;
|
||||||
background-color: #ffffff !important;
|
background-color: #ffffff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,3 +410,4 @@ table.dataTable td {
|
|||||||
text-align: center; }
|
text-align: center; }
|
||||||
.dataTables_wrapper .dataTables_filter {
|
.dataTables_wrapper .dataTables_filter {
|
||||||
margin-top: 0.5em; } }
|
margin-top: 0.5em; } }
|
||||||
|
|
||||||
|
|||||||
@@ -28,31 +28,105 @@
|
|||||||
<br>
|
<br>
|
||||||
|
|
||||||
<center>
|
<center>
|
||||||
<h2 style="color:white; margin-top: 12px">Buy ETHO</h2>
|
<h2 style="color:white; margin-top: 12px">Markets</h2>
|
||||||
</center>
|
</center>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
<div class="ui grid container">
|
<div class="ui grid container">
|
||||||
<div class="four column row">
|
<div class="four column row">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<button class="aboutbutton ui violet inverted button" type="button" onclick="require('electron').shell.openExternal('https://www.probit.com/r/41540915')">ProBit</button>
|
<button class="aboutbutton ui violet inverted button" type="button" onclick="require('electron').shell.openExternal('https://www.probit.com/r/26069458')">
|
||||||
|
<img src="https://s2.coinmarketcap.com/static/img/exchanges/64x64/477.png" alt="ProBit Logo" style="width: 24px; height: 24px; vertical-align: middle;">
|
||||||
|
<span style="vertical-align: middle;">ProBit</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<button class="aboutbutton ui violet inverted button" type="button" onclick="require('electron').shell.openExternal('https://www.kucoin.com/ucenter/signup?rcode=rJUCF6W')">KuCoin</button>
|
<button class="aboutbutton ui violet inverted button" type="button" onclick="require('electron').shell.openExternal('https://xeggex.com?ref=662d0099f852ffbc4da4f8bf')">
|
||||||
|
<img src="https://s2.coinmarketcap.com/static/img/exchanges/64x64/7500.png" alt="Xeggex Logo" style="width: 24px; height: 24px; vertical-align: middle;">
|
||||||
|
<span style="vertical-align: middle;">Xeggex</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<button class="aboutbutton ui violet inverted button" type="button" onclick="require('electron').shell.openExternal('https://mercatox.com/?referrer=467736')">Mercatox</button>
|
<button class="aboutbutton ui violet inverted button" type="button" onclick="require('electron').shell.openExternal('https://mercatox.com/?referrer=765491')">
|
||||||
|
<img src="https://s2.coinmarketcap.com/static/img/exchanges/64x64/213.png" alt="Mercatox Logo" style="width: 24px; height: 24px; vertical-align: middle;">
|
||||||
|
<span style="vertical-align: middle;">Mercatox</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<button class="aboutbutton ui violet inverted button" type="button" onclick="require('electron').shell.openExternal('https://graviex.net')">Graviex</button>
|
<button class="aboutbutton ui violet inverted button" type="button" onclick="require('electron').shell.openExternal('https://graviex.net')">
|
||||||
|
<img src="https://s2.coinmarketcap.com/static/img/exchanges/64x64/419.png" alt="Graviex Logo" style="width: 24px; height: 24px; vertical-align: middle;">
|
||||||
|
<span style="vertical-align: middle;">Graviex</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="ui grid container">
|
<div class="ui grid container">
|
||||||
<div class="one column row">
|
<div class="four column row">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<button class="aboutbutton ui violet inverted button" type="button" onclick="require('electron').shell.openExternal('https://app.stex.com?ref=26491159')">Stex</button>
|
<button class="aboutbutton ui violet inverted button" type="button" onclick="require('electron').shell.openExternal('https://www.dextools.io/app/en/bnb/pair-explorer/0xb77cf2bd2571c3e58c79107cbc155a7f9ca28b75')">
|
||||||
|
<img src="https://s2.coinmarketcap.com/static/img/exchanges/64x64/6706.png" alt="Pancake Logo" style="width: 24px; height: 24px; display: inline-block; vertical-align: middle;">
|
||||||
|
<span style="vertical-align: middle;">PancakeSwap v2 ETHO / USDT</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column">
|
||||||
|
<button class="aboutbutton ui violet inverted button" type="button" onclick="require('electron').shell.openExternal('https://www.dextools.io/app/en/bnb/pair-explorer/0xef2b9739de830bdc651ae2aa146762c499cbf7c1')">
|
||||||
|
<img src="https://s2.coinmarketcap.com/static/img/exchanges/64x64/6706.png" alt="Pancake Logo" style="width: 24px; height: 24px; display: inline-block; vertical-align: middle;">
|
||||||
|
<span style="vertical-align: middle;">PancakeSwap v3 ETHO / USDT</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column">
|
||||||
|
<button class="aboutbutton ui violet inverted button" type="button" onclick="require('electron').shell.openExternal('https://www.dextools.io/app/en/ether/pair-explorer/0xa6dc0957e7c1935e80f5ec746be85eb4f0753dae')">
|
||||||
|
<img src="https://s2.coinmarketcap.com/static/img/exchanges/64x64/1348.png" alt="Uniswap Logo" style="width: 24px; height: 24px; display: inline-block; vertical-align: middle;">
|
||||||
|
<span style="vertical-align: middle;">Uniswap ETHO / WETH</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column">
|
||||||
|
<button class="aboutbutton ui violet inverted button" type="button" onclick="require('electron').shell.openExternal('https://www.dextools.io/app/en/ether/pair-explorer/0xb77cf2bd2571c3e58c79107cbc155a7f9ca28b75')">
|
||||||
|
<img src="https://s2.coinmarketcap.com/static/img/exchanges/64x64/1348.png" alt="Uniswap Logo" style="width: 24px; height: 24px; display: inline-block; vertical-align: middle;">
|
||||||
|
<span style="vertical-align: middle;">Uniswap ETHO / USDT</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<center>
|
||||||
|
<h2 style="color:white; margin-top: 15px">Links 🌐</h2>
|
||||||
|
</center>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="ui grid container">
|
||||||
|
<div class="four column row">
|
||||||
|
<div class="column">
|
||||||
|
<button class="aboutbutton ui inverted green button" type="button" onclick="require('electron').shell.openExternal('https://ethoprotocol.com')">
|
||||||
|
<i class="fas fa-globe"></i> Etho Website
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<button class="aboutbutton ui inverted green button" type="button" onclick="require('electron').shell.openExternal('https://uploads.ethoprotocol.com')">
|
||||||
|
<i class="fas fa-server"></i> Etho Hosting
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<button class="aboutbutton ui inverted green button" type="button" onclick="require('electron').shell.openExternal('https://github.com/Ether1Project')">
|
||||||
|
<i class="fab fa-github"></i> Etho Github
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<button class="aboutbutton ui inverted green button" type="button" onclick="require('electron').shell.openExternal('https://github.com/Ether1Project/Ethoprotocol')">
|
||||||
|
<i class="fab fa-github"></i> Etho Geth
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,32 +134,7 @@
|
|||||||
<br>
|
<br>
|
||||||
|
|
||||||
<center>
|
<center>
|
||||||
<h2 style="color:white; margin-top: 15px">Etho Protocol Links</h2>
|
<h2 style="color:white; margin-top: 15px">Social Media 📱</h2>
|
||||||
</center>
|
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<div class="ui grid container">
|
|
||||||
<div class="four column row">
|
|
||||||
<div class="column">
|
|
||||||
<button class="aboutbutton ui inverted green button" type="button" onclick="require('electron').shell.openExternal('https://ethoprotocol.com')">Etho Protocol Website</button>
|
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
<button class="aboutbutton ui inverted green button" type="button" onclick="require('electron').shell.openExternal('https://uploads.ethoprotocol.com/')">ethoFS Website</button>
|
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
<button class="aboutbutton ui inverted green button" type="button" onclick="require('electron').shell.openExternal('https://github.com/Ether1Project')">Etho Protocol Github</button>
|
|
||||||
</div>
|
|
||||||
<div class="column">
|
|
||||||
<button class="aboutbutton ui inverted green button" type="button" onclick="require('electron').shell.openExternal('https://github.com/Ether1Project/Ether1')">Etho Protocol Geth</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<center>
|
|
||||||
<h2 style="color:white; margin-top: 15px">Etho Protocol Social Media</h2>
|
|
||||||
</center>
|
</center>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
@@ -94,26 +143,39 @@
|
|||||||
<div class="doubling six column row">
|
<div class="doubling six column row">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<!-- Etho Protocol -->
|
<!-- Etho Protocol -->
|
||||||
<button class="aboutbutton ui inverted blue button" type="button" onclick="require('electron').shell.openExternal('https://discord.gg/Ap5FmXc')">Discord</button>
|
<button class="aboutbutton ui inverted blue button" type="button" onclick="require('electron').shell.openExternal('https://discord.com/invite/VanQ23KMZv')">
|
||||||
|
<i class="fab fa-discord"></i> Discord
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<button class="aboutbutton ui inverted blue button" type="button" onclick="require('electron').shell.openExternal('https://twitter.com/EthoProtocol/')">Twitter</button>
|
<button class="aboutbutton ui inverted blue button" type="button" onclick="require('electron').shell.openExternal('https://twitter.com/EthoProtocol/')">
|
||||||
|
<i class="fab fa-twitter"></i> Twitter
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<button class="aboutbutton ui inverted blue button" type="button" onclick="require('electron').shell.openExternal('https://ethoprotocol.medium.com')">Medium</button>
|
<button class="aboutbutton ui inverted blue button" type="button" onclick="require('electron').shell.openExternal('https://ethoprotocol.medium.com')">
|
||||||
|
<i class="fab fa-medium"></i> Medium
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<button class="aboutbutton ui inverted blue button" type="button" onclick="require('electron').shell.openExternal('https://t.me/Ether_1')">Telegram</button>
|
<button class="aboutbutton ui inverted blue button" type="button" onclick="require('electron').shell.openExternal('https://t.me/Ether_1')">
|
||||||
|
<i class="fab fa-telegram"></i> Telegram
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<button class="aboutbutton ui inverted blue button" type="button" onclick="require('electron').shell.openExternal('https://ethoprotocol.com/blog/')">News Blog</button>
|
<button class="aboutbutton ui inverted blue button" type="button" onclick="require('electron').shell.openExternal('https://ethoprotocol.com/blog/')">
|
||||||
|
<i class="far fa-newspaper"></i> Blog
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<button class="aboutbutton ui inverted blue button" type="button" onclick="require('electron').shell.openExternal('mailto:admin@ethoprotocol.com')">Email</button>
|
<button class="aboutbutton ui inverted blue button" type="button" onclick="require('electron').shell.openExternal('mailto:admin@ethoprotocol.com')">
|
||||||
|
<i class="far fa-envelope"></i> Email
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
$('.message .close')
|
$('.message .close')
|
||||||
.on('click', function() {
|
.on('click', function() {
|
||||||
@@ -122,3 +184,4 @@
|
|||||||
.transition('fade');
|
.transition('fade');
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -3,44 +3,44 @@
|
|||||||
<div class="col s12 m3">
|
<div class="col s12 m3">
|
||||||
<div class="card etho-red">
|
<div class="card etho-red">
|
||||||
<div class="card-content white-text">
|
<div class="card-content white-text">
|
||||||
<span class="card-title">ETHO to USD</span>
|
<span class="card-title">ETHO to USD 💵</span>
|
||||||
<p id="ETHOToUSD">N/A</p>
|
<p id="ETHOToUSD">N/A</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-action">
|
<div class="card-action">
|
||||||
<p id="changeUSD">This is a link</p>
|
7 days change: <p id="changeUSD" class="cardValue">N/A 🔗</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div><!-- col-3 -->
|
</div><!-- col-3 -->
|
||||||
<div class="col s12 m3">
|
<div class="col s12 m3">
|
||||||
<div class="card etho-red">
|
<div class="card etho-red">
|
||||||
<div class="card-content white-text">
|
<div class="card-content white-text">
|
||||||
<span class="card-title">ETHO to BTC</span>
|
<span class="card-title">ETHO to BTC ₿</span>
|
||||||
<p id="ETHOToBTC">N/A</p>
|
<p id="ETHOToBTC">N/A</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-action">
|
<div class="card-action">
|
||||||
<p id="changeBTC">This is a link</p>
|
7 days change:<p id="changeBTC" class="cardValue">N/A 🔗</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div><!-- col-3 -->
|
</div><!-- col-3 -->
|
||||||
<div class="col s12 m3">
|
<div class="col s12 m3">
|
||||||
<div class="card etho-red">
|
<div class="card etho-red">
|
||||||
<div class="card-content white-text">
|
<div class="card-content white-text">
|
||||||
<span class="card-title">Marketcap</span>
|
<span class="card-title">Marketcap 💰</span>
|
||||||
<p id="marketcap">N/A</p>
|
<p id="marketcap">N/A</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-action">
|
<div class="card-action">
|
||||||
<p id="changeMarketcap">This is a link</p>
|
high 24h: <p id="changeMarketcap" class="cardValue">N/A 🔗</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div><!-- col-3 -->
|
</div><!-- col-3 -->
|
||||||
<div class="col s12 m3">
|
<div class="col s12 m3">
|
||||||
<div class="card etho-red">
|
<div class="card etho-red">
|
||||||
<div class="card-content white-text">
|
<div class="card-content white-text">
|
||||||
<span class="card-title">Daily Volume</span>
|
<span class="card-title">Daily Volume 📊</span>
|
||||||
<p id="dailyVolume">N/A</p>
|
<p id="dailyVolume">N/A</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-action">
|
<div class="card-action">
|
||||||
<p id="changeVolume">This is a link</p>
|
all time high:<p id="changeVolume" class="cardValue">N/A 🔗</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div><!-- col-3 -->
|
</div><!-- col-3 -->
|
||||||
@@ -51,3 +51,4 @@
|
|||||||
<a class="chart-style"></a>
|
<a class="chart-style"></a>
|
||||||
</div>
|
</div>
|
||||||
<span id="nodestorage" style="display:none;">Loading</span>
|
<span id="nodestorage" style="display:none;">Loading</span>
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,16 @@
|
|||||||
<form class="col s12">
|
<form class="col s12">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="input-field col s6">
|
<div class="input-field col s6">
|
||||||
|
<label for="sendToAddress" class="active">Sender:</label>
|
||||||
<select id="sendFromAddress">
|
<select id="sendFromAddress">
|
||||||
{{#addressData}}
|
{{#addressData}}
|
||||||
<option value="{{address}}">{{name}} - {{address}}</option>
|
<option value="{{address}}">{{balance}} | {{name}} | {{address}}</option>
|
||||||
{{/addressData}}
|
{{/addressData}}
|
||||||
</select>
|
</select>
|
||||||
<small id="sendFromAddressName" class="form-text text-muted"></small>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="input-field col s6">
|
<div class="input-field col s6">
|
||||||
<label for="sendToAddress" class="active">To address:</label>
|
<label for="sendToAddress" class="active">Recipient:</label>
|
||||||
<div class="addressInputWrapper">
|
<div class="addressInputWrapper">
|
||||||
<input id="sendToAddress" placeholder="recipient address" type="text">
|
<input id="sendToAddress" placeholder="recipient address" type="text">
|
||||||
<button type="button" class="btn button3 btnSendToolButton" id="btnAddToAddressBook">
|
<button type="button" class="btn button3 btnSendToolButton" id="btnAddToAddressBook">
|
||||||
@@ -26,12 +27,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="input-field col s6">
|
<div class="input-field col s6">
|
||||||
<input id="sendAmmount" placeholder="0" type="number">
|
<label for="sendAmmount" style="display:flex">
|
||||||
|
<img height="22px" width="22px" style="margin: 11px 6px 0 2px;" src="https://raw.githubusercontent.com/Ether1Project/ETHO-Protocol-Branding/master/NewLogo2024/newethologo_32.png" alt="ETHO" />
|
||||||
|
<input id="sendAmmount" placeholder="0" type="number">
|
||||||
|
</label>
|
||||||
|
|
||||||
<label for="sendAmmount" class="active">Amount:</label>
|
<label for="sendAmmount" class="active">Amount:</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-field col s6">
|
<div class="input-field col s6">
|
||||||
<div class="input-field col s12">
|
<div class="input-field col s12">
|
||||||
<span id="sendMaxAmmount">0</span><span>ETHO</span>
|
<span id="sendMaxAmmount">0</span><img height="22px" width="22px" style="margin-bottom: -6px;" src="https://raw.githubusercontent.com/Ether1Project/ETHO-Protocol-Branding/master/NewLogo2024/newethologo_32.png" alt="ETHO" />Total
|
||||||
<button type="button" class="button4" id="btnSendAll">ALL</button>
|
<button type="button" class="button4" id="btnSendAll">ALL</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
214
assets/templates/staking.html
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
<!-- PaperclipChain Staking Interface -->
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>Staking & Delegation
|
||||||
|
<small>Earn rewards by staking your CLIPS</small>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Staking Overview -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="info-box bg-clips">
|
||||||
|
<div class="info-box-icon">
|
||||||
|
<i class="fas fa-coins"></i>
|
||||||
|
</div>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">Total Staked</span>
|
||||||
|
<span class="info-box-number" id="totalStakedAmount">0 CLIPS</span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar" style="width: 0%" id="totalStakedProgress"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="info-box bg-success">
|
||||||
|
<div class="info-box-icon">
|
||||||
|
<i class="fas fa-trophy"></i>
|
||||||
|
</div>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">Pending Rewards</span>
|
||||||
|
<span class="info-box-number" id="pendingRewards">0 CLIPS</span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar bg-success" style="width: 0%" id="rewardsProgress"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="info-box bg-warning">
|
||||||
|
<div class="info-box-icon">
|
||||||
|
<i class="fas fa-users"></i>
|
||||||
|
</div>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">Active Validators</span>
|
||||||
|
<span class="info-box-number" id="activeValidators">0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="info-box bg-info">
|
||||||
|
<div class="info-box-icon">
|
||||||
|
<i class="fas fa-percentage"></i>
|
||||||
|
</div>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">Network APY</span>
|
||||||
|
<span class="info-box-number" id="networkAPY">~8%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Quick Actions</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button type="button" class="btn btn-clips" id="btnDelegate">
|
||||||
|
<i class="fas fa-arrow-up"></i> Delegate
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-warning" id="btnUndelegate">
|
||||||
|
<i class="fas fa-arrow-down"></i> Undelegate
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-success" id="btnClaimRewards">
|
||||||
|
<i class="fas fa-gift"></i> Claim Rewards
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-info" id="btnCreateValidator">
|
||||||
|
<i class="fas fa-plus"></i> Become Validator
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Validators List -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Validators</h3>
|
||||||
|
<div class="card-tools">
|
||||||
|
<button type="button" class="btn btn-tool" id="btnRefreshValidators" data-toggle="tooltip" title="Refresh">
|
||||||
|
<i class="fas fa-sync"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-bordered" id="validatorsTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Validator</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Self Stake</th>
|
||||||
|
<th>Delegated</th>
|
||||||
|
<th>Commission</th>
|
||||||
|
<th>APY</th>
|
||||||
|
<th>Your Stake</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="validatorsTableBody">
|
||||||
|
<!-- Validators will be populated here -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delegate Modal -->
|
||||||
|
<div id="dlgDelegate" class="modalDialog" data-izimodal-title="Delegate to Validator" data-izimodal-subtitle="Stake your CLIPS to earn rewards" data-izimodal-icon="icon-home">
|
||||||
|
<div class="modalBody">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="delegateValidator">Select Validator:</label>
|
||||||
|
<select class="form-control" id="delegateValidator">
|
||||||
|
<option value="">Select a validator...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="delegateAmount">Amount to Delegate (CLIPS):</label>
|
||||||
|
<input type="number" class="form-control" id="delegateAmount" placeholder="Minimum 100 CLIPS" min="100" step="0.000001">
|
||||||
|
<small class="form-text text-muted">Available balance: <span id="availableBalance">0 CLIPS</span></small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="delegateWallet">From Wallet:</label>
|
||||||
|
<select class="form-control" id="delegateWallet">
|
||||||
|
<!-- Wallets will be populated here -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-secondary btn-dialog-cancel" id="btnDelegateCancel">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-clips btn-dialog-confirm" id="btnDelegateConfirm">Delegate</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Undelegate Modal -->
|
||||||
|
<div id="dlgUndelegate" class="modalDialog" data-izimodal-title="Undelegate from Validator" data-izimodal-subtitle="Withdraw your staked CLIPS" data-izimodal-icon="icon-home">
|
||||||
|
<div class="modalBody">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="undelegateValidator">Validator:</label>
|
||||||
|
<select class="form-control" id="undelegateValidator">
|
||||||
|
<option value="">Select a validator...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="undelegateAmount">Amount to Undelegate (CLIPS):</label>
|
||||||
|
<input type="number" class="form-control" id="undelegateAmount" placeholder="Enter amount" min="0.000001" step="0.000001">
|
||||||
|
<small class="form-text text-muted">Your stake: <span id="yourStakeAmount">0 CLIPS</span></small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="undelegateWallet">To Wallet:</label>
|
||||||
|
<select class="form-control" id="undelegateWallet">
|
||||||
|
<!-- Wallets will be populated here -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
<strong>Note:</strong> Undelegated funds may have a cooldown period before they become available.
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-secondary btn-dialog-cancel" id="btnUndelegateCancel">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-warning btn-dialog-confirm" id="btnUndelegateConfirm">Undelegate</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Create Validator Modal -->
|
||||||
|
<div id="dlgCreateValidator" class="modalDialog" data-izimodal-title="Become a Validator" data-izimodal-subtitle="Create a new validator node" data-izimodal-icon="icon-home">
|
||||||
|
<div class="modalBody">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
<strong>Requirements:</strong> Minimum 10,000 CLIPS stake required to create a validator.
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="validatorStake">Initial Stake (CLIPS):</label>
|
||||||
|
<input type="number" class="form-control" id="validatorStake" placeholder="Minimum 10,000 CLIPS" min="10000" step="0.000001">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="validatorCommission">Commission Rate (%):</label>
|
||||||
|
<input type="number" class="form-control" id="validatorCommission" placeholder="0-50%" min="0" max="50" step="0.01" value="5">
|
||||||
|
<small class="form-text text-muted">Percentage of rewards you'll keep as commission</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="validatorWallet">Validator Wallet:</label>
|
||||||
|
<select class="form-control" id="validatorWallet">
|
||||||
|
<!-- Wallets will be populated here -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-secondary btn-dialog-cancel" id="btnCreateValidatorCancel">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-info btn-dialog-confirm" id="btnCreateValidatorConfirm">Create Validator</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -16,8 +16,10 @@
|
|||||||
</button>
|
</button>
|
||||||
<div id="sumBalance">
|
<div id="sumBalance">
|
||||||
<span class="sumBalance" id="labelSumBalance">{{sumBalance}}</span>
|
<span class="sumBalance" id="labelSumBalance">{{sumBalance}}</span>
|
||||||
<span class="sumCurrency" id="labelSumCurrency">ETHO</span>
|
<span class="sumCurrency" id="labelSumCurrency"><img height="22px" width="22px" style="margin-bottom: -6px;margin-left:-7px;margin-right:-2px;" src="https://raw.githubusercontent.com/Ether1Project/ETHO-Protocol-Branding/master/NewLogo2024/newethologo_32.png" alt="ETHO" />
|
||||||
<span class="sumDollars" id="labelSumDollars">/ 0.00 $ / 0.00 $ per ETHO</span></div>
|
</span>
|
||||||
|
<span class="sumDollars" id="labelSumDollars">= 0.00 💵 | 0.00 💵 per
|
||||||
|
</span><img height="22px" width="22px" style="margin-bottom: -6px;margin-left:8px;margin-right:2px;" src="https://raw.githubusercontent.com/Ether1Project/ETHO-Protocol-Branding/master/NewLogo2024/newethologo_32.png" alt="ETHO" />ETHO</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="addressList" class="{{#if addressData.length}}walletsWrapper{{else}}noWalletsWrapper{{/if}}">
|
<div id="addressList" class="{{#if addressData.length}}walletsWrapper{{else}}noWalletsWrapper{{/if}}">
|
||||||
{{#if addressData.length}}
|
{{#if addressData.length}}
|
||||||
@@ -51,7 +53,7 @@
|
|||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td>{{balance}}</td>
|
<td>{{balance}} <img height="22px" width="22px" style="margin-bottom: -6px;margin-left:-5px;" src="https://raw.githubusercontent.com/Ether1Project/ETHO-Protocol-Branding/master/NewLogo2024/newethologo_32.png" alt="ETHO" /></td>
|
||||||
</tr>
|
</tr>
|
||||||
{{/addressData}}
|
{{/addressData}}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
BIN
bin/linux/geth
BIN
bin/macos/geth
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:5a0d13c1c0aea93d72c457d318f4fa3bfb6fcf9b792b25055ea51df2b1047f11
|
|
||||||
size 23514025
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:ca2e254777323d66dcc483e967d30babeb126f082dddc540b947e1f55be7602c
|
|
||||||
size 111578699
|
|
||||||
BIN
build/icon.icns
BIN
build/icon.png
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 21 KiB |
88
index.html
@@ -3,8 +3,8 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>$ETHO Desktop Wallet</title>
|
<title>PaperclipWallet</title>
|
||||||
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Fira+Mono:wght@400;500;700&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="./assets/styles/materialize.min.css">
|
<link rel="stylesheet" href="./assets/styles/materialize.min.css">
|
||||||
<link rel="stylesheet" href="./assets/styles/datatables.min.css">
|
<link rel="stylesheet" href="./assets/styles/datatables.min.css">
|
||||||
<link rel="stylesheet" href="./assets/styles/iziModal.min.css">
|
<link rel="stylesheet" href="./assets/styles/iziModal.min.css">
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
<link rel="stylesheet" href="./assets/styles/style.css">
|
<link rel="stylesheet" href="./assets/styles/style.css">
|
||||||
<link rel="stylesheet" href="./assets/styles/forms.css">
|
<link rel="stylesheet" href="./assets/styles/forms.css">
|
||||||
<link rel="stylesheet" href="./assets/styles/about.css">
|
<link rel="stylesheet" href="./assets/styles/about.css">
|
||||||
|
<link rel="stylesheet" href="./assets/styles/staking.css">
|
||||||
<!-- Insert this line above script imports -->
|
<!-- Insert this line above script imports -->
|
||||||
<script>
|
<script>
|
||||||
if (typeof module === 'object') {
|
if (typeof module === 'object') {
|
||||||
@@ -29,7 +30,6 @@
|
|||||||
<span id="nodestorage" style="display:none;">Loading</span>
|
<span id="nodestorage" style="display:none;">Loading</span>
|
||||||
|
|
||||||
<!-- normal script imports etc -->
|
<!-- normal script imports etc -->
|
||||||
<script src="https://cdn.jsdelivr.net/gh/ethereum/web3.js@1.0.0-beta.36/dist/web3.min.js" integrity="sha256-nWBTbvxhJgjslRyuAKJHK+XcZPlCnmIAAMixz6EefVk=" crossorigin="anonymous"></script>
|
|
||||||
<script src="./assets/scripts/jquery.min.js"></script>
|
<script src="./assets/scripts/jquery.min.js"></script>
|
||||||
<script src='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js'></script>
|
<script src='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js'></script>
|
||||||
<script src="./assets/scripts/handlebars.js"></script>
|
<script src="./assets/scripts/handlebars.js"></script>
|
||||||
@@ -65,24 +65,45 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
var loadingHtml = "<div class='spinner'><div class='bounce bounce1'></div><div class='bounce bounce2'></div><div class='bounce bounce3'></div></div><div class='loadingText' id='startextEL'>Loading Resources...</div>";
|
||||||
var loading_screen = pleaseWait({
|
var loading_screen = pleaseWait({
|
||||||
logo: "assets/images/logo-glitch.gif",
|
logo: "assets/images/logo-glitch.gif",
|
||||||
backgroundColor: '#000000',
|
backgroundColor: '#000000',
|
||||||
loadingHtml: "<div class='spinner'><div class='bounce bounce1'></div><div class='bounce bounce2'></div><div class='bounce bounce3'></div></div><div class='loadingText'>Starting the node and loading app, please wait...</div>"
|
loadingHtml: loadingHtml
|
||||||
});
|
});
|
||||||
|
|
||||||
$(document).on("onGethReady", function() {
|
var timeouts = [
|
||||||
setTimeout(() => {
|
{ text: "Starting PaperclipChain node...", delay: 1250 },
|
||||||
loading_screen.finish();
|
{ text: "Connecting to network...", delay: 6400 },
|
||||||
}, 4000);
|
{ text: "Loading wallet...", delay: 7320 },
|
||||||
|
{ text: "Ready", delay: 7400 }
|
||||||
|
];
|
||||||
|
|
||||||
|
timeouts.forEach((item, index) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById('startextEL').innerHTML = item.text;
|
||||||
|
}, item.delay + Math.random() * 50 * index);
|
||||||
});
|
});
|
||||||
|
|
||||||
$(window).on("beforeunload", function() {
|
$(document).on("onNodeReady", function () {
|
||||||
EthoBlockchain.closeConnection();
|
setTimeout(() => {
|
||||||
})
|
loading_screen.finish();
|
||||||
</script>
|
}, 5500);
|
||||||
|
});
|
||||||
|
|
||||||
|
$(window).on("beforeunload", function () {
|
||||||
|
if (window.PaperclipChain) {
|
||||||
|
PaperclipChain.stopConnection();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
<!-- The sidebar -->
|
<!-- The sidebar -->
|
||||||
<div class="ui left demo vertical inverted sidebar labeled icon menu">
|
<div class="ui left demo vertical inverted sidebar labeled icon menu">
|
||||||
@@ -98,12 +119,16 @@
|
|||||||
</a>
|
</a>
|
||||||
<a class="item" style="padding-top: 30px; padding-left: 13px" id="mainNavBtnSend" href="#" data-tippy="Send Funds" data-tippy-delay="100">
|
<a class="item" style="padding-top: 30px; padding-left: 13px" id="mainNavBtnSend" href="#" data-tippy="Send Funds" data-tippy-delay="100">
|
||||||
<i class="fas fa-comment-dollar fa-1x"></i>
|
<i class="fas fa-comment-dollar fa-1x"></i>
|
||||||
<span class="sendEtho"> Send </span>
|
<span class="sendClips"> Send </span>
|
||||||
</a>
|
</a>
|
||||||
<a class="item" style="padding-top: 30px; padding-left: 13px" id="mainNavBtnTransactions" href="#" data-tippy="Transactions" data-tippy-delay="100">
|
<a class="item" style="padding-top: 30px; padding-left: 13px" id="mainNavBtnTransactions" href="#" data-tippy="Transactions" data-tippy-delay="100">
|
||||||
<i class="fas fa-exchange-alt fa-1x"></i>
|
<i class="fas fa-exchange-alt fa-1x"></i>
|
||||||
<span class="txlist"> Transactions </span>
|
<span class="txlist"> Transactions </span>
|
||||||
</a>
|
</a>
|
||||||
|
<a class="item" style="padding-top: 30px; padding-left: 13px" id="mainNavBtnStaking" href="#" data-tippy="Staking & Delegation" data-tippy-delay="100">
|
||||||
|
<i class="fas fa-coins fa-1x"></i>
|
||||||
|
<span class="staking"> Staking </span>
|
||||||
|
</a>
|
||||||
<a class="item" style="padding-top: 30px; padding-left: 13px" id="mainNavBtnMarkets" href="#" data-tippy="Markets" data-tippy-delay="100">
|
<a class="item" style="padding-top: 30px; padding-left: 13px" id="mainNavBtnMarkets" href="#" data-tippy="Markets" data-tippy-delay="100">
|
||||||
<i class="fas fa-poll fa-1x"></i>
|
<i class="fas fa-poll fa-1x"></i>
|
||||||
<span class="Markets"> Market </span>
|
<span class="Markets"> Market </span>
|
||||||
@@ -123,20 +148,21 @@
|
|||||||
<div id="mainContent"></div>
|
<div id="mainContent"></div>
|
||||||
<div id="syncProgress"></div>
|
<div id="syncProgress"></div>
|
||||||
<script>
|
<script>
|
||||||
// You can also require other files to run in this process
|
// PaperclipChain renderer modules
|
||||||
require('./renderer/about.js');
|
require('./renderer/paperclip-database.js');
|
||||||
require('./renderer/send.js');
|
require('./renderer/blockchain.js');
|
||||||
|
require('./renderer/paperclip-wallets.js');
|
||||||
require('./renderer/utils.js');
|
require('./renderer/utils.js');
|
||||||
require('./renderer/maingui.js');
|
require('./renderer/maingui.js');
|
||||||
|
require('./renderer/about.js');
|
||||||
|
require('./renderer/send.js');
|
||||||
require('./renderer/syncing.js');
|
require('./renderer/syncing.js');
|
||||||
require('./renderer/markets.js');
|
require('./renderer/markets.js');
|
||||||
require('./renderer/settings.js');
|
require('./renderer/settings.js');
|
||||||
require('./renderer/wallets.js');
|
|
||||||
require('./renderer/database.js');
|
|
||||||
require('./renderer/blockchain.js');
|
|
||||||
require('./renderer/addressBook.js');
|
require('./renderer/addressBook.js');
|
||||||
require('./renderer/transactions.js');
|
require('./renderer/transactions.js');
|
||||||
require('./renderer/tableTransactions.js');
|
require('./renderer/tableTransactions.js');
|
||||||
|
require('./renderer/staking.js');
|
||||||
</script>
|
</script>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -146,7 +172,7 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<span id="txtGeneralError"></span>
|
<span id="txtGeneralError"></span>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-etho btn-dialog-confirm" id="btnGeneralErrorOK">OK</button>
|
<button type="button" class="btn btn-clips btn-dialog-confirm" id="btnGeneralErrorOK">OK</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -156,27 +182,27 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<span id="txtGeneralConfirm"></span>
|
<span id="txtGeneralConfirm"></span>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-etho btn-dialog-cancel" id="btnGeneralConfirmNo">No</button>
|
<button type="button" class="btn btn-clips btn-dialog-cancel" id="btnGeneralConfirmNo">No</button>
|
||||||
<button type="button" class="btn btn-etho btn-dialog-confirm" id="btnGeneralConfirmYes">Yes</button>
|
<button type="button" class="btn btn-clips btn-dialog-confirm" id="btnGeneralConfirmYes">Yes</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- The modal for about info -->
|
<!-- The modal for about info -->
|
||||||
<div id="dlgAboutInfo" class="modalDialog" data-izimodal-title="About Etho Protocol Wallet" data-izimodal-icon="icon-home">
|
<div id="dlgAboutInfo" class="modalDialog" data-izimodal-title="About PaperclipWallet" data-izimodal-icon="icon-home">
|
||||||
<div class="modalBody">
|
<div class="modalBody">
|
||||||
<div class="aboutInfo">
|
<div class="aboutInfo">
|
||||||
<div class="infoText" id="aboutInfoWallet">Etho Protocol Wallet</div>
|
<div class="infoText" id="aboutInfoWallet">PaperclipWallet</div>
|
||||||
<div class="infoText" id="aboutInfoGitHub">GitHub:
|
<div class="infoText" id="aboutInfoGitHub">GitHub:
|
||||||
<a id="urlOpenGitHub" href="https://github.com/Ether1Project/Ether1DesktopWallet">https://github.com/Ether1Project/Ether1DesktopWallet</a>
|
<a id="urlOpenGitHub" href="https://git.takoyaki.cool/matt/paperclip-wallet">https://git.takoyaki.cool/matt/paperclip-wallet</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="infoText" id="aboutInfoLicence">Made under
|
<div class="infoText" id="aboutInfoLicence">Made under
|
||||||
<a id="urlOpenLicence" href="https://choosealicense.com/licenses/gpl-3.0">GPL v3.0</a>
|
<a id="urlOpenLicence" href="https://choosealicense.com/licenses/cc0-1.0">CC0 1.0</a>
|
||||||
licence
|
licence
|
||||||
</div>
|
</div>
|
||||||
<div class="infoText" id="aboutInfoVersion">Version:
|
<div class="infoText" id="aboutInfoVersion">Version:
|
||||||
<span id="versionNumber"></span></div>
|
<span id="versionNumber"></span></div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-etho btn-dialog-confirm" id="btnAboutInfoClose">Close</button>
|
<button type="button" class="btn btn-clips btn-dialog-confirm" id="btnAboutInfoClose">Close</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
@@ -185,7 +211,7 @@
|
|||||||
<div id="dlgShowAddressQRCode" class="modalDialog" data-izimodal-title="Address QR-Code" data-izimodal-subtitle="Scan the QR-Code to get the address..." data-izimodal-icon="icon-home">
|
<div id="dlgShowAddressQRCode" class="modalDialog" data-izimodal-title="Address QR-Code" data-izimodal-subtitle="Scan the QR-Code to get the address..." data-izimodal-icon="icon-home">
|
||||||
<div class="modalBody">
|
<div class="modalBody">
|
||||||
<div id="addrQRCode"></div>
|
<div id="addrQRCode"></div>
|
||||||
<button type="button" class="btn btn-etho btn-dialog-confirm" id="btnScanQRCodeClose">Close</button>
|
<button type="button" class="btn btn-clips btn-dialog-confirm" id="btnScanQRCodeClose">Close</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
6
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()
|
||||||
@@ -73,6 +74,7 @@ locker.lock().then(function() {
|
|||||||
// In this file you can include the rest of your app's specific main process
|
// In this file you can include the rest of your app's specific main process
|
||||||
// code. You can also put them in separate files and require them here.
|
// code. You can also put them in separate files and require them here.
|
||||||
// listen for request to get template
|
// listen for request to get template
|
||||||
|
|
||||||
|
|
||||||
// get the template content from file
|
// get the template content from file
|
||||||
ipcMain.on("getTemplateContent", (event, arg) => {
|
ipcMain.on("getTemplateContent", (event, arg) => {
|
||||||
|
|||||||
@@ -1,135 +1,194 @@
|
|||||||
const {app, dialog, ipcMain} = require("electron");
|
const { app, dialog, ipcMain } = require("electron");
|
||||||
const admZip = require("adm-zip");
|
const admZip = require("adm-zip");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const fs = require("fs-extra");
|
const fs = require("fs-extra");
|
||||||
const os = require("os");
|
const os = require("os");
|
||||||
|
|
||||||
class Accounts {
|
class Accounts {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
|
getKeyStoreLocation() {
|
||||||
|
const platform = os.type();
|
||||||
|
let keystorePath;
|
||||||
|
|
||||||
getKeyStoreLocation() {
|
switch (platform) {
|
||||||
switch (os.type()) {
|
case "Darwin":
|
||||||
case "Darwin":
|
keystorePath = path.join(os.homedir(), "Library", "Ether1", "keystore");
|
||||||
return path.join(os.homedir(), "Library", "Ether1", "keystore");
|
break;
|
||||||
break;
|
case "Windows_NT":
|
||||||
default:
|
if (process.env.APPDATA) {
|
||||||
return path.join(process.env.APPDATA.replace('Roaming', 'Local'), "Ether1", "keystore");
|
keystorePath = path.join(process.env.APPDATA.replace('Roaming', 'Local'), "Ether1", "keystore");
|
||||||
}
|
} else {
|
||||||
}
|
keystorePath = path.join(app.getPath("userData"), "Ether1", "keystore");
|
||||||
|
}
|
||||||
exportAccounts() {
|
break;
|
||||||
var savePath = dialog.showSaveDialog({
|
case "Linux":
|
||||||
defaultPath: path.join(app.getPath("documents"), "accounts.zip")
|
keystorePath = path.join(app.getPath("home"), ".ether1", "keystore");
|
||||||
});
|
break;
|
||||||
|
default:
|
||||||
if (savePath) {
|
keystorePath = path.join(app.getPath("userData"), "Ether1", "keystore");
|
||||||
const accPath = EthoAccounts.getKeyStoreLocation();
|
break;
|
||||||
|
|
||||||
fs.readdir(accPath, function (err, files) {
|
|
||||||
var zip = new admZip();
|
|
||||||
|
|
||||||
for (let filePath of files) {
|
|
||||||
zip.addFile(filePath, fs.readFileSync(path.join(accPath, filePath)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// store zip to path
|
return Promise.resolve(keystorePath);
|
||||||
zip.writeZip(savePath);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
importAccounts(accountsFile) {
|
|
||||||
var extName = path.extname(accountsFile).toUpperCase();
|
|
||||||
const accPath = EthoAccounts.getKeyStoreLocation();
|
|
||||||
|
|
||||||
if (extName == ".ZIP") {
|
|
||||||
var zip = new admZip(accountsFile);
|
|
||||||
zip.extractAllTo(accPath, true);
|
|
||||||
return {success: true, text: "Accounts ware successfully imported."};
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
fs.copySync(accountsFile, path.join(accPath, path.basename(accountsFile)));
|
|
||||||
return {success: true, text: "Account was successfully imported."};
|
|
||||||
} catch (err) {
|
|
||||||
return {success: false, text: err};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
saveAccount(account) {
|
exportAccounts() {
|
||||||
fs.writeFile(path.join(tEthoAccountshis.getKeyStoreLocation(), "0x" + account.address), JSON.stringify(account), "utf8", function () {
|
dialog.showSaveDialog({
|
||||||
// file was written
|
defaultPath: path.join(app.getPath("documents"), "accounts.zip")
|
||||||
});
|
}).then(result => {
|
||||||
}
|
if (!result.canceled) {
|
||||||
|
const savePath = result.filePath;
|
||||||
deteteAccount(address) {
|
const accPathPromise = this.getKeyStoreLocation();
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const accPath = EthoAccounts.getKeyStoreLocation();
|
|
||||||
|
|
||||||
fs.readdir(accPath, function (err, files) {
|
accPathPromise.then(accPath => {
|
||||||
let deleteFilePath = null;
|
fs.readdir(accPath, function (err, files) {
|
||||||
if (err) reject(err);
|
if (err) {
|
||||||
else {
|
console.error("Error reading directory:", err);
|
||||||
const searchStr = String(address).substring(2, String(address).length).toLowerCase();
|
// Handle the error, e.g., show an error dialog to the user
|
||||||
for (let filePath of files) {
|
return;
|
||||||
if (String(filePath).toLowerCase().indexOf(searchStr) > -1) {
|
}
|
||||||
deleteFilePath = filePath;
|
|
||||||
break;
|
var zip = new admZip();
|
||||||
}
|
|
||||||
}
|
for (let filePath of files) {
|
||||||
if (deleteFilePath) {
|
zip.addFile(filePath, fs.readFileSync(path.join(accPath, filePath)));
|
||||||
fs.unlink(path.join(accPath, deleteFilePath), function(error) {
|
}
|
||||||
if (error) reject(error);
|
|
||||||
else resolve(true);
|
// store zip to path
|
||||||
|
zip.writeZip(savePath);
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
console.error("Error getting keystore location:", err);
|
||||||
|
// Handle the error, e.g., show an error dialog to the user
|
||||||
});
|
});
|
||||||
} else resolve(true)
|
|
||||||
}
|
}
|
||||||
});
|
}).catch(err => {
|
||||||
});
|
console.error("Error showing save dialog:", err);
|
||||||
}
|
// Handle the error, e.g., show an error dialog to the user
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async importAccounts() {
|
||||||
|
try {
|
||||||
|
const openPath = await dialog.showOpenDialog({
|
||||||
|
defaultPath: app.getPath("documents"),
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: "archive",
|
||||||
|
extensions: ["zip"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "json",
|
||||||
|
extensions: ["json"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "All",
|
||||||
|
extensions: ["*"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!openPath.canceled && openPath.filePaths.length > 0) {
|
||||||
|
const accPath = await this.getKeyStoreLocation();
|
||||||
|
const extName = path.extname(openPath.filePaths[0]).toUpperCase();
|
||||||
|
|
||||||
|
if (extName === ".ZIP") {
|
||||||
|
const zip = new admZip(openPath.filePaths[0]);
|
||||||
|
zip.extractAllTo(accPath, true);
|
||||||
|
return { success: true, text: "Accounts were successfully imported." };
|
||||||
|
} else {
|
||||||
|
await fs.copy(openPath.filePaths[0], path.join(accPath, path.basename(openPath.filePaths[0])));
|
||||||
|
return { success: true, text: "Account was successfully imported." };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return { success: false, text: "No file selected for import." };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, text: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
saveAccount(account) {
|
||||||
|
fs.writeFile(path.join(this.getKeyStoreLocation(), "0x" + account.address), JSON.stringify(account), "utf8", function () {
|
||||||
|
// file was written
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deteteAccount(address) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const accPathPromise = this.getKeyStoreLocation();
|
||||||
|
|
||||||
|
accPathPromise.then(accPath => {
|
||||||
|
fs.readdir(accPath, function (err, files) {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let deleteFilePath = null;
|
||||||
|
const searchStr = String(address).substring(2, String(address).length).toLowerCase();
|
||||||
|
|
||||||
|
for (let filePath of files) {
|
||||||
|
if (String(filePath).toLowerCase().indexOf(searchStr) > -1) {
|
||||||
|
deleteFilePath = filePath;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deleteFilePath) {
|
||||||
|
fs.unlink(path.join(accPath, deleteFilePath), function (error) {
|
||||||
|
if (error) reject(error);
|
||||||
|
else resolve(true);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcMain.on("exportAccounts", (event, arg) => {
|
ipcMain.on("exportAccounts", (event, arg) => {
|
||||||
EthoAccounts.exportAccounts();
|
const ethoAccounts = new Accounts();
|
||||||
|
ethoAccounts.exportAccounts();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on("importAccounts", (event, arg) => {
|
|
||||||
var openPath = dialog.showOpenDialog({
|
|
||||||
defaultPath: app.getPath("documents"),
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
name: "archive",
|
|
||||||
extensions: ["zip"]
|
|
||||||
}, {
|
|
||||||
name: "json",
|
|
||||||
extensions: ["json"]
|
|
||||||
}, {
|
|
||||||
name: "All",
|
|
||||||
extensions: ["*"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
if (openPath) {
|
ipcMain.on("importAccounts", async (event, arg) => {
|
||||||
event.returnValue = EthoAccounts.importAccounts(openPath[0]);
|
const ethoAccounts = new Accounts();
|
||||||
} else {
|
try {
|
||||||
event.returnValue = {};
|
const importResult = await ethoAccounts.importAccounts();
|
||||||
}
|
event.reply("importAccountsReply", importResult);
|
||||||
|
} catch (error) {
|
||||||
|
event.reply("importAccountsReply", { success: false, text: error.message });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
ipcMain.on("saveAccount", (event, arg) => {
|
ipcMain.on("saveAccount", (event, arg) => {
|
||||||
EthoAccounts.saveAccount(arg);
|
const ethoAccounts = new Accounts();
|
||||||
event.returnValue = true;
|
ethoAccounts.saveAccount(arg);
|
||||||
|
event.returnValue = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on("deteteAccount", (event, arg) => {
|
ipcMain.on("deteteAccount", (event, arg) => {
|
||||||
EthoAccounts.deteteAccount(arg)
|
const ethoAccounts = new Accounts();
|
||||||
.then((res) => {
|
ethoAccounts.deteteAccount(arg)
|
||||||
event.returnValue = res;
|
.then((res) => {
|
||||||
})
|
event.returnValue = res;
|
||||||
.catch((err) => {
|
})
|
||||||
event.returnValue = err;
|
.catch((err) => {
|
||||||
});
|
event.returnValue = err;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
EthoAccounts = new Accounts();
|
|
||||||
|
|||||||
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();
|
||||||
@@ -44,6 +44,8 @@ class Geth {
|
|||||||
if (this.logGethEvents) {
|
if (this.logGethEvents) {
|
||||||
this.logStream.write(text);
|
this.logStream.write(text);
|
||||||
}
|
}
|
||||||
|
// Print log messages to the console
|
||||||
|
//console.log("geth..."+text);
|
||||||
}
|
}
|
||||||
|
|
||||||
startGeth() {
|
startGeth() {
|
||||||
|
|||||||
365
modules/paperclip-rpc.js
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
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`);
|
||||||
|
}
|
||||||
|
// Suppress console logging in production
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Staking Methods
|
||||||
|
|
||||||
|
async getValidators() {
|
||||||
|
try {
|
||||||
|
const result = await this._makeRPCCall("abci_query", {
|
||||||
|
path: `"validators"`
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.result && result.result.response && result.result.response.value) {
|
||||||
|
const validators = Buffer.from(result.result.response.value, 'base64').toString();
|
||||||
|
return JSON.parse(validators);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
} catch (error) {
|
||||||
|
this._writeLog(`Failed to get validators: ${error.message}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStakingInfo(address) {
|
||||||
|
try {
|
||||||
|
const result = await this._makeRPCCall("abci_query", {
|
||||||
|
path: `"staker:${address}"`
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.result && result.result.response && result.result.response.value) {
|
||||||
|
const stakingInfo = Buffer.from(result.result.response.value, 'base64').toString();
|
||||||
|
return JSON.parse(stakingInfo);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
this._writeLog(`Failed to get staking info for ${address}: ${error.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getValidatorInfo(validatorAddress) {
|
||||||
|
try {
|
||||||
|
const result = await this._makeRPCCall("abci_query", {
|
||||||
|
path: `"validator:${validatorAddress}"`
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.result && result.result.response && result.result.response.value) {
|
||||||
|
const validatorInfo = Buffer.from(result.result.response.value, 'base64').toString();
|
||||||
|
return JSON.parse(validatorInfo);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
this._writeLog(`Failed to get validator info for ${validatorAddress}: ${error.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStakingRewards(address) {
|
||||||
|
try {
|
||||||
|
const result = await this._makeRPCCall("abci_query", {
|
||||||
|
path: `"rewards:${address}"`
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.result && result.result.response && result.result.response.value) {
|
||||||
|
const rewards = Buffer.from(result.result.response.value, 'base64').toString();
|
||||||
|
return parseInt(rewards) || 0;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
} catch (error) {
|
||||||
|
this._writeLog(`Failed to get staking rewards for ${address}: ${error.message}`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTotalStaked() {
|
||||||
|
try {
|
||||||
|
const result = await this._makeRPCCall("abci_query", {
|
||||||
|
path: `"total_staked"`
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.result && result.result.response && result.result.response.value) {
|
||||||
|
const totalStaked = Buffer.from(result.result.response.value, 'base64').toString();
|
||||||
|
return parseInt(totalStaked) || 0;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
} catch (error) {
|
||||||
|
this._writeLog(`Failed to get total staked: ${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("Connected to PaperclipChain node");
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this._writeLog(`Connection failed: ${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);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Staking IPC handlers
|
||||||
|
ipcMain.handle("paperclip-get-validators", async () => {
|
||||||
|
return await PaperclipNode.getValidators();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("paperclip-get-staking-info", async (event, address) => {
|
||||||
|
return await PaperclipNode.getStakingInfo(address);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("paperclip-get-validator-info", async (event, validatorAddress) => {
|
||||||
|
return await PaperclipNode.getValidatorInfo(validatorAddress);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("paperclip-get-staking-rewards", async (event, address) => {
|
||||||
|
return await PaperclipNode.getStakingRewards(address);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("paperclip-get-total-staked", async () => {
|
||||||
|
return await PaperclipNode.getTotalStaked();
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = PaperclipNode;
|
||||||
18156
package-lock.json
generated
Normal file
36
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "EthoWallet",
|
"name": "PaperclipWallet",
|
||||||
"version": "2.4.7",
|
"version": "1.0.0",
|
||||||
"description": "Desktop wallet for Etho Protocol ($ETHO)",
|
"description": "Desktop wallet for PaperclipChain with DeFi staking capabilities",
|
||||||
"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,36 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repository": "https://github.com/Ether1Project/Ether1DesktopWallet",
|
"repository": "https://git.takoyaki.cool/matt/paperclip-wallet",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"Etho",
|
"PaperclipChain",
|
||||||
|
"CLIPS",
|
||||||
"Desktop",
|
"Desktop",
|
||||||
"Wallet"
|
"Wallet",
|
||||||
|
"Staking",
|
||||||
|
"DeFi",
|
||||||
|
"Cryptocurrency",
|
||||||
|
"Blockchain"
|
||||||
],
|
],
|
||||||
"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",
|
||||||
"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": "^2.6.7",
|
||||||
"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;
|
||||||
clbSuccess(block);
|
} catch (error) {
|
||||||
}
|
this.isConnected = false;
|
||||||
});
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getAccounts(clbError, clbSuccess) {
|
async getBlock(blockHeight, clbError, clbSuccess) {
|
||||||
web3Local.eth.getAccounts(function(err, res) {
|
try {
|
||||||
if (err) {
|
const block = await ipcRenderer.invoke("paperclip-get-block", blockHeight);
|
||||||
clbError(err);
|
clbSuccess(block);
|
||||||
} else {
|
} catch (error) {
|
||||||
clbSuccess(res);
|
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) {
|
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);
|
||||||
clbError(error);
|
clbSuccess(transaction);
|
||||||
} else {
|
} catch (error) {
|
||||||
clbSuccess(result);
|
clbError(error);
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getAddressListData(clbError, clbSuccess) {
|
async getTransactionFee(fromAddress, toAddress, amount, txType = 'transfer', clbError, clbSuccess) {
|
||||||
var rendererData = {};
|
try {
|
||||||
rendererData.addressData = [];
|
// 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();
|
async prepareTransaction(privateKey, fromAddress, toAddress, amount, txType = 'transfer', data = '', clbError, clbSuccess) {
|
||||||
var counter = 0;
|
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) {
|
async sendTransaction(transaction, clbError, clbSuccess) {
|
||||||
if (err) {
|
try {
|
||||||
clbError(err);
|
// 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 {
|
||||||
for (var i = 0; i < res.length; i++) {
|
clbError(result.error || "Transaction failed");
|
||||||
var walletName = vsprintf("Account %d", [i + 1]);
|
}
|
||||||
if (wallets) {
|
} catch (error) {
|
||||||
walletName = wallets.names[res[i]] || walletName;
|
clbError(error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var addressInfo = {};
|
async getAccountsData(clbError, clbSuccess) {
|
||||||
addressInfo.address = res[i];
|
try {
|
||||||
addressInfo.name = walletName;
|
const rendererData = {
|
||||||
rendererData.addressData.push(addressInfo);
|
sumBalance: 0,
|
||||||
}
|
addressData: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get stored wallets from database
|
||||||
|
const wallets = PaperclipDatabase.getWallets();
|
||||||
|
|
||||||
|
if (!wallets || !wallets.addresses || wallets.addresses.length === 0) {
|
||||||
clbSuccess(rendererData);
|
clbSuccess(rendererData);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
createNewAccount(password, clbError, clbSuccess) {
|
// Get balance for each address
|
||||||
web3Local.eth.personal.newAccount(password, function(error, account) {
|
let processedCount = 0;
|
||||||
if (error) {
|
const totalAddresses = wallets.addresses.length;
|
||||||
clbError(error);
|
|
||||||
} else {
|
|
||||||
clbSuccess(account);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
importFromPrivateKey(privateKey, keyPassword, clbError, clbSuccess) {
|
for (let i = 0; i < wallets.addresses.length; i++) {
|
||||||
web3Local.eth.personal.importRawKey(privateKey, keyPassword, function(error, account) {
|
const address = wallets.addresses[i];
|
||||||
if (error) {
|
const walletName = wallets.names[address] || `Account ${i + 1}`;
|
||||||
clbError(error);
|
|
||||||
} else {
|
|
||||||
clbSuccess(account);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
subsribePendingTransactions(clbError, clbSuccess, clbData) {
|
try {
|
||||||
this.txSubscribe = web3Local.eth.subscribe("pendingTransactions", function(error, result) {
|
const balance = await ipcRenderer.invoke("paperclip-get-balance", address);
|
||||||
if (error) {
|
|
||||||
clbError(error);
|
rendererData.addressData.push({
|
||||||
} else {
|
address: address,
|
||||||
clbSuccess(result);
|
name: walletName,
|
||||||
}
|
balance: balance,
|
||||||
}).on("data", function(transaction) {
|
balanceFormatted: this.formatBalance(balance)
|
||||||
if (clbData) {
|
});
|
||||||
clbData(transaction);
|
|
||||||
}
|
rendererData.sumBalance += balance;
|
||||||
});
|
|
||||||
}
|
} catch (error) {
|
||||||
|
console.error(`Failed to get balance for ${address}:`, error);
|
||||||
unsubsribePendingTransactions(clbError, clbSuccess) {
|
|
||||||
if (this.txSubscribe) {
|
rendererData.addressData.push({
|
||||||
this.txSubscribe.unsubscribe(function(error, success) {
|
address: address,
|
||||||
if (error) {
|
name: walletName,
|
||||||
clbError(error);
|
balance: 0,
|
||||||
} else {
|
balanceFormatted: "0 CLIPS",
|
||||||
clbSuccess(success);
|
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) {
|
formatBalance(balance) {
|
||||||
this.bhSubscribe = web3Local.eth.subscribe("newBlockHeaders", function(error, result) {
|
if (balance === 0) {
|
||||||
if (error) {
|
return "0 CLIPS";
|
||||||
clbError(error);
|
}
|
||||||
} else {
|
|
||||||
clbSuccess(result);
|
// Format balance with appropriate decimal places
|
||||||
}
|
if (balance >= 1000000) {
|
||||||
}).on("data", function(blockHeader) {
|
return (balance / 1000000).toFixed(2) + "M CLIPS";
|
||||||
if (clbData) {
|
} else if (balance >= 1000) {
|
||||||
clbData(blockHeader);
|
return (balance / 1000).toFixed(2) + "K CLIPS";
|
||||||
}
|
} else {
|
||||||
});
|
return balance.toLocaleString() + " CLIPS";
|
||||||
}
|
|
||||||
|
|
||||||
unsubsribeNewBlockHeaders(clbError, clbSuccess) {
|
|
||||||
if (this.bhSubscribe) {
|
|
||||||
this.bhSubscribe.unsubscribe(function(error, success) {
|
|
||||||
if (error) {
|
|
||||||
clbError(error);
|
|
||||||
} else {
|
|
||||||
clbSuccess(success);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
closeConnection() {
|
async getNetworkInfo(clbError, clbSuccess) {
|
||||||
web3Local.currentProvider.connection.close();
|
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
|
// Create global instance
|
||||||
EthoBlockchain = new Blockchain();
|
const PaperclipChain = new PaperclipBlockchain();
|
||||||
|
|
||||||
|
// Make it available globally (maintaining compatibility with existing code)
|
||||||
|
window.PaperclipChain = PaperclipChain;
|
||||||
@@ -25,6 +25,9 @@ class MainGUI {
|
|||||||
case "transactions":
|
case "transactions":
|
||||||
$("#mainNavBtnTransactionsWrapper").addClass("iconSelected");
|
$("#mainNavBtnTransactionsWrapper").addClass("iconSelected");
|
||||||
break;
|
break;
|
||||||
|
case "staking":
|
||||||
|
$("#mainNavBtnStakingWrapper").addClass("iconSelected");
|
||||||
|
break;
|
||||||
case "markets":
|
case "markets":
|
||||||
$("#mainNavBtnMarketsWrapper").addClass("iconSelected");
|
$("#mainNavBtnMarketsWrapper").addClass("iconSelected");
|
||||||
break;
|
break;
|
||||||
@@ -122,6 +125,11 @@ $("#mainNavBtnTransactions").click(function () {
|
|||||||
EthoTransactions.renderTransactions();
|
EthoTransactions.renderTransactions();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#mainNavBtnStaking").click(function () {
|
||||||
|
EthoMainGUI.changeAppState("staking");
|
||||||
|
PaperclipStaking.showStakingPage();
|
||||||
|
});
|
||||||
|
|
||||||
$("#mainNavBtnAddressBoook").click(function () {
|
$("#mainNavBtnAddressBoook").click(function () {
|
||||||
EthoMainGUI.changeAppState("addressBook");
|
EthoMainGUI.changeAppState("addressBook");
|
||||||
EthoAddressBook.renderAddressBook();
|
EthoAddressBook.renderAddressBook();
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ class Markets {
|
|||||||
$("#marketcap").html(data.market_data.market_cap.usd.toFixed(0) + " $ (" + data.market_cap_rank + ")");
|
$("#marketcap").html(data.market_data.market_cap.usd.toFixed(0) + " $ (" + data.market_cap_rank + ")");
|
||||||
$("#dailyVolume").html(data.market_data.total_volume.usd.toFixed(0) + " $");
|
$("#dailyVolume").html(data.market_data.total_volume.usd.toFixed(0) + " $");
|
||||||
|
|
||||||
$("#changeUSD").html("7 days change: " + data.market_data.price_change_percentage_7d_in_currency.usd.toFixed(2) + "%");
|
$("#changeUSD").html(data.market_data.price_change_percentage_7d_in_currency.usd.toFixed(2) + "%");
|
||||||
$("#changeBTC").html("7 days change: " + data.market_data.price_change_percentage_7d_in_currency.btc.toFixed(2) + "%");
|
$("#changeBTC").html(data.market_data.price_change_percentage_7d_in_currency.btc.toFixed(2) + "%");
|
||||||
$("#changeMarketcap").html("high 24h: " + data.market_data.high_24h.usd.toFixed(5) + " $");
|
$("#changeMarketcap").html(data.market_data.high_24h.usd.toFixed(5) + "$");
|
||||||
$("#changeVolume").html("all time high: " + data.market_data.ath.usd.toFixed(5) + " $");
|
$("#changeVolume").html(data.market_data.ath.usd.toFixed(5) + "$");
|
||||||
|
|
||||||
new Chart(document.getElementById("chartMarketPriceCanvas"), {
|
new Chart(document.getElementById("chartMarketPriceCanvas"), {
|
||||||
type: "line",
|
type: "line",
|
||||||
@@ -29,7 +29,7 @@ class Markets {
|
|||||||
fill: true,
|
fill: true,
|
||||||
borderWidth: 3,
|
borderWidth: 3,
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
borderColor: "#7A1336"
|
borderColor: "#25D4DC"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -55,7 +55,7 @@ class Markets {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
gridLines: {
|
gridLines: {
|
||||||
color: "rgba(255,255,255,.08)"
|
color: "rgba(255,255,255,.35)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
298
renderer/paperclip-database.js
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
const storage = require('electron-storage');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
class PaperclipDatabase {
|
||||||
|
constructor() {
|
||||||
|
this.walletFile = 'paperclip-wallets.json';
|
||||||
|
this.settingsFile = 'paperclip-settings.json';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wallet operations
|
||||||
|
getWallets() {
|
||||||
|
try {
|
||||||
|
const data = storage.getSync(this.walletFile);
|
||||||
|
return data || { addresses: [], names: {}, keys: {} };
|
||||||
|
} catch (error) {
|
||||||
|
return { addresses: [], names: {}, keys: {} };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveWallet(wallet) {
|
||||||
|
try {
|
||||||
|
const wallets = this.getWallets();
|
||||||
|
|
||||||
|
// Add address if not exists
|
||||||
|
if (!wallets.addresses.includes(wallet.address)) {
|
||||||
|
wallets.addresses.push(wallet.address);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save wallet data
|
||||||
|
wallets.names[wallet.address] = wallet.name;
|
||||||
|
wallets.keys[wallet.address] = {
|
||||||
|
publicKey: wallet.publicKey,
|
||||||
|
privateKey: wallet.privateKey, // TODO: Encrypt with password
|
||||||
|
created: wallet.created
|
||||||
|
};
|
||||||
|
|
||||||
|
storage.setSync(this.walletFile, wallets);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save wallet:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getWallet(address) {
|
||||||
|
try {
|
||||||
|
const wallets = this.getWallets();
|
||||||
|
|
||||||
|
if (!wallets.addresses.includes(address)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
address: address,
|
||||||
|
name: wallets.names[address] || 'Account',
|
||||||
|
publicKey: wallets.keys[address]?.publicKey,
|
||||||
|
privateKey: wallets.keys[address]?.privateKey,
|
||||||
|
created: wallets.keys[address]?.created
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get wallet:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeWallet(address) {
|
||||||
|
try {
|
||||||
|
const wallets = this.getWallets();
|
||||||
|
|
||||||
|
// Remove from addresses array
|
||||||
|
const index = wallets.addresses.indexOf(address);
|
||||||
|
if (index > -1) {
|
||||||
|
wallets.addresses.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove wallet data
|
||||||
|
delete wallets.names[address];
|
||||||
|
delete wallets.keys[address];
|
||||||
|
|
||||||
|
storage.setSync(this.walletFile, wallets);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to remove wallet:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWalletName(address, newName) {
|
||||||
|
try {
|
||||||
|
const wallets = this.getWallets();
|
||||||
|
|
||||||
|
if (wallets.addresses.includes(address)) {
|
||||||
|
wallets.names[address] = newName;
|
||||||
|
storage.setSync(this.walletFile, wallets);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update wallet name:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Address book operations
|
||||||
|
getAddressBook() {
|
||||||
|
try {
|
||||||
|
const data = storage.getSync('address-book.json');
|
||||||
|
return data || [];
|
||||||
|
} catch (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveAddressBookEntry(entry) {
|
||||||
|
try {
|
||||||
|
const addressBook = this.getAddressBook();
|
||||||
|
|
||||||
|
// Check if address already exists
|
||||||
|
const existingIndex = addressBook.findIndex(item => item.address === entry.address);
|
||||||
|
|
||||||
|
if (existingIndex > -1) {
|
||||||
|
// Update existing entry
|
||||||
|
addressBook[existingIndex] = entry;
|
||||||
|
} else {
|
||||||
|
// Add new entry
|
||||||
|
addressBook.push(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
storage.setSync('address-book.json', addressBook);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save address book entry:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAddressBookEntry(address) {
|
||||||
|
try {
|
||||||
|
const addressBook = this.getAddressBook();
|
||||||
|
const filtered = addressBook.filter(item => item.address !== address);
|
||||||
|
|
||||||
|
storage.setSync('address-book.json', filtered);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to remove address book entry:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings operations
|
||||||
|
getSettings() {
|
||||||
|
try {
|
||||||
|
const data = storage.getSync(this.settingsFile);
|
||||||
|
return data || {
|
||||||
|
rpcUrl: 'http://localhost:26657',
|
||||||
|
autoConnect: true,
|
||||||
|
notifications: true
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
rpcUrl: 'http://localhost:26657',
|
||||||
|
autoConnect: true,
|
||||||
|
notifications: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSetting(key, value) {
|
||||||
|
try {
|
||||||
|
const settings = this.getSettings();
|
||||||
|
settings[key] = value;
|
||||||
|
storage.setSync(this.settingsFile, settings);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save setting:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSettings(settingsObj) {
|
||||||
|
try {
|
||||||
|
storage.setSync(this.settingsFile, settingsObj);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save settings:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction history (local cache)
|
||||||
|
getTransactionHistory(address) {
|
||||||
|
try {
|
||||||
|
const data = storage.getSync(`tx-history-${address}.json`);
|
||||||
|
return data || [];
|
||||||
|
} catch (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveTransaction(address, transaction) {
|
||||||
|
try {
|
||||||
|
const history = this.getTransactionHistory(address);
|
||||||
|
|
||||||
|
// Check if transaction already exists
|
||||||
|
const exists = history.some(tx => tx.hash === transaction.hash);
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
history.unshift(transaction); // Add to beginning
|
||||||
|
|
||||||
|
// Keep only last 100 transactions
|
||||||
|
if (history.length > 100) {
|
||||||
|
history.splice(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
storage.setSync(`tx-history-${address}.json`, history);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save transaction:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility methods
|
||||||
|
clearAllData() {
|
||||||
|
try {
|
||||||
|
storage.removeSync(this.walletFile);
|
||||||
|
storage.removeSync(this.settingsFile);
|
||||||
|
storage.removeSync('address-book.json');
|
||||||
|
|
||||||
|
// Clear transaction histories
|
||||||
|
const wallets = this.getWallets();
|
||||||
|
wallets.addresses.forEach(address => {
|
||||||
|
try {
|
||||||
|
storage.removeSync(`tx-history-${address}.json`);
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore errors for non-existent files
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to clear data:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exportWallets() {
|
||||||
|
try {
|
||||||
|
const wallets = this.getWallets();
|
||||||
|
const settings = this.getSettings();
|
||||||
|
const addressBook = this.getAddressBook();
|
||||||
|
|
||||||
|
return {
|
||||||
|
wallets: wallets,
|
||||||
|
settings: settings,
|
||||||
|
addressBook: addressBook,
|
||||||
|
exportDate: new Date().toISOString()
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to export data:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
importWallets(data) {
|
||||||
|
try {
|
||||||
|
if (data.wallets) {
|
||||||
|
storage.setSync(this.walletFile, data.wallets);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.settings) {
|
||||||
|
storage.setSync(this.settingsFile, data.settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.addressBook) {
|
||||||
|
storage.setSync('address-book.json', data.addressBook);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to import data:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create global instance
|
||||||
|
const paperclipDB = new PaperclipDatabase();
|
||||||
|
|
||||||
|
// Make it available globally
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.PaperclipDatabase = paperclipDB;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = paperclipDB;
|
||||||
271
renderer/paperclip-wallets.js
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
const {ipcRenderer} = require("electron");
|
||||||
|
|
||||||
|
class PaperclipWallets {
|
||||||
|
constructor() {
|
||||||
|
this.addressList = [];
|
||||||
|
this.price = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getPrice() {
|
||||||
|
return this.price;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setPrice(price) {
|
||||||
|
this.price = price;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAddressList() {
|
||||||
|
return this.addressList;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearAddressList() {
|
||||||
|
this.addressList = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
getAddressExists(address) {
|
||||||
|
if (address) {
|
||||||
|
return this.addressList.indexOf(address) > -1;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
addAddressToList(address) {
|
||||||
|
if (address) {
|
||||||
|
this.addressList.push(address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createNewWallet(password) {
|
||||||
|
try {
|
||||||
|
const ClipsCrypto = require('../modules/clips-crypto.js');
|
||||||
|
const keyPair = ClipsCrypto.generateKeyPair();
|
||||||
|
|
||||||
|
// Store wallet in database
|
||||||
|
const wallet = {
|
||||||
|
address: keyPair.address,
|
||||||
|
publicKey: keyPair.publicKey,
|
||||||
|
privateKey: keyPair.privateKey, // In production, encrypt this with password
|
||||||
|
name: `Account ${this.addressList.length + 1}`,
|
||||||
|
created: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
PaperclipDatabase.saveWallet(wallet);
|
||||||
|
this.addAddressToList(keyPair.address);
|
||||||
|
|
||||||
|
return wallet;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to create wallet: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async importFromPrivateKey(privateKeyHex, password) {
|
||||||
|
try {
|
||||||
|
const ClipsCrypto = require('../modules/clips-crypto.js');
|
||||||
|
const keyPair = ClipsCrypto.importPrivateKey(privateKeyHex);
|
||||||
|
|
||||||
|
const wallet = {
|
||||||
|
address: keyPair.address,
|
||||||
|
publicKey: keyPair.publicKey,
|
||||||
|
privateKey: keyPair.privateKey,
|
||||||
|
name: `Imported Account`,
|
||||||
|
created: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
PaperclipDatabase.saveWallet(wallet);
|
||||||
|
this.addAddressToList(keyPair.address);
|
||||||
|
|
||||||
|
return wallet;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to import wallet: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendTransaction(fromAddress, toAddress, amount, password) {
|
||||||
|
try {
|
||||||
|
// Get wallet data
|
||||||
|
const wallet = PaperclipDatabase.getWallet(fromAddress);
|
||||||
|
if (!wallet) {
|
||||||
|
throw new Error("Wallet not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate address format
|
||||||
|
const ClipsCrypto = require('../modules/clips-crypto.js');
|
||||||
|
if (!ClipsCrypto.isValidAddress(toAddress)) {
|
||||||
|
throw new Error("Invalid recipient address");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current nonce
|
||||||
|
const nonce = await ipcRenderer.invoke("paperclip-get-nonce", fromAddress);
|
||||||
|
|
||||||
|
// Create and sign transaction
|
||||||
|
const transaction = ClipsCrypto.createTransaction({
|
||||||
|
sender: fromAddress,
|
||||||
|
receiver: toAddress,
|
||||||
|
amount: parseInt(amount),
|
||||||
|
nonce: nonce,
|
||||||
|
type: 'transfer',
|
||||||
|
data: '',
|
||||||
|
privateKey: wallet.privateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
// Broadcast transaction
|
||||||
|
const result = await ipcRenderer.invoke("paperclip-broadcast-tx",
|
||||||
|
ClipsCrypto.transactionToHex(transaction));
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
return {
|
||||||
|
hash: result.hash,
|
||||||
|
height: result.height
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error || "Transaction failed");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Transaction failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getWalletBalance(address) {
|
||||||
|
try {
|
||||||
|
const balance = await ipcRenderer.invoke("paperclip-get-balance", address);
|
||||||
|
return balance;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to get balance for ${address}:`, error);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshWalletData() {
|
||||||
|
try {
|
||||||
|
this.clearAddressList();
|
||||||
|
|
||||||
|
const wallets = PaperclipDatabase.getWallets();
|
||||||
|
if (!wallets || !wallets.addresses) {
|
||||||
|
return {
|
||||||
|
sumBalance: 0,
|
||||||
|
addressData: [],
|
||||||
|
sumBalanceFormatted: "0 CLIPS"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
sumBalance: 0,
|
||||||
|
addressData: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get balance for each wallet
|
||||||
|
for (const address of wallets.addresses) {
|
||||||
|
const wallet = PaperclipDatabase.getWallet(address);
|
||||||
|
const balance = await this.getWalletBalance(address);
|
||||||
|
|
||||||
|
data.addressData.push({
|
||||||
|
address: address,
|
||||||
|
name: wallet ? wallet.name : `Account`,
|
||||||
|
balance: balance,
|
||||||
|
balanceFormatted: this.formatBalance(balance)
|
||||||
|
});
|
||||||
|
|
||||||
|
data.sumBalance += balance;
|
||||||
|
this.addAddressToList(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.sumBalanceFormatted = this.formatBalance(data.sumBalance);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to refresh wallet data: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formatBalance(balance) {
|
||||||
|
if (balance === 0) {
|
||||||
|
return "0 CLIPS";
|
||||||
|
}
|
||||||
|
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validateAddress(address) {
|
||||||
|
const ClipsCrypto = require('../modules/clips-crypto.js');
|
||||||
|
return ClipsCrypto.isValidAddress(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
validateAmount(amount) {
|
||||||
|
const num = parseFloat(amount);
|
||||||
|
return !isNaN(num) && num > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI Event Handlers
|
||||||
|
async handleCreateNewWallet() {
|
||||||
|
const password = $("#walletPasswordFirst").val();
|
||||||
|
|
||||||
|
if (!password) {
|
||||||
|
throw new Error("Password cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password !== $("#walletPasswordSecond").val()) {
|
||||||
|
throw new Error("Passwords do not match");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.createNewWallet(password);
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleImportPrivateKey() {
|
||||||
|
const privateKey = $("#inputPrivateKey").val();
|
||||||
|
const password = $("#keyPasswordFirst").val();
|
||||||
|
|
||||||
|
if (!privateKey) {
|
||||||
|
throw new Error("Private key cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!password) {
|
||||||
|
throw new Error("Password cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password !== $("#keyPasswordSecond").val()) {
|
||||||
|
throw new Error("Passwords do not match");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.importFromPrivateKey(privateKey, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleSendTransaction() {
|
||||||
|
const fromAddress = $("#sendFromAddress").val();
|
||||||
|
const toAddress = $("#sendToAddress").val();
|
||||||
|
const amount = $("#sendAmount").val();
|
||||||
|
const password = $("#sendPassword").val();
|
||||||
|
|
||||||
|
if (!fromAddress) {
|
||||||
|
throw new Error("From address is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!toAddress) {
|
||||||
|
throw new Error("To address is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.validateAddress(toAddress)) {
|
||||||
|
throw new Error("Invalid recipient address format");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.validateAmount(amount)) {
|
||||||
|
throw new Error("Invalid amount");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!password) {
|
||||||
|
throw new Error("Password is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.sendTransaction(fromAddress, toAddress, amount, password);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create global instance
|
||||||
|
const ClipsWallet = new PaperclipWallets();
|
||||||
|
|
||||||
|
// Make it available globally
|
||||||
|
window.ClipsWallet = ClipsWallet;
|
||||||
@@ -57,17 +57,29 @@ class SendTransaction {
|
|||||||
|
|
||||||
$(document).on("render_send", function () {
|
$(document).on("render_send", function () {
|
||||||
//$("select").formSelect({classes: "fromAddressSelect"});
|
//$("select").formSelect({classes: "fromAddressSelect"});
|
||||||
|
setTimeout(() => {
|
||||||
|
var optionText = $("#sendFromAddress").find("option:selected").text();
|
||||||
|
var selectedAddressBalance = optionText.substr(0, optionText.indexOf("|"));
|
||||||
|
|
||||||
|
console.log("selectedAddressBalance", selectedAddressBalance);
|
||||||
|
$("#sendMaxAmmount").html(parseFloat(selectedAddressBalance));
|
||||||
|
//$("#sendAmmount").val(selectedAddressBalance);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
$("#sendFromAddress").on("change", function () {
|
$("#sendFromAddress").on("change", function () {
|
||||||
var optionText = $(this).find("option:selected").text();
|
var optionText = $(this).find("option:selected").text();
|
||||||
var addrName = optionText.substr(0, optionText.indexOf("-"));
|
|
||||||
var addrValue = optionText.substr(optionText.indexOf("-") + 1);
|
|
||||||
$(".fromAddressSelect input").val(addrValue.trim());
|
|
||||||
$("#sendFromAddressName").html(addrName.trim());
|
|
||||||
|
|
||||||
web3Local.eth.getBalance(this.value, function (error, balance) {
|
var addrBalance = optionText.substr(0, optionText.indexOf("|"));
|
||||||
$("#sendMaxAmmount").html(parseFloat(web3Local.utils.fromWei(balance, "ether")));
|
var addrName = optionText.substr(optionText.indexOf("|")+1);
|
||||||
});
|
var addrValue = addrName.substr(addrName.indexOf("|")+1);
|
||||||
|
|
||||||
|
|
||||||
|
$(".fromAddressSelect input").val(addrValue.trim());
|
||||||
|
|
||||||
|
$("#sendMaxAmmount").html(parseFloat(addrBalance));
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#btnSendAll").off("click").on("click", function () {
|
$("#btnSendAll").off("click").on("click", function () {
|
||||||
@@ -98,9 +110,27 @@ $(document).on("render_send", function () {
|
|||||||
var adddressObject = {};
|
var adddressObject = {};
|
||||||
adddressObject.address = key;
|
adddressObject.address = key;
|
||||||
adddressObject.name = addressBook[key];
|
adddressObject.name = addressBook[key];
|
||||||
|
adddressObject.balance = 0;
|
||||||
|
|
||||||
addressList.addressData.push(adddressObject);
|
addressList.addressData.push(adddressObject);
|
||||||
|
|
||||||
|
web3Local.eth.getBalance(key, function (error, balance) {
|
||||||
|
if (error) {
|
||||||
|
console.error("Error fetching balance:", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the balance display
|
||||||
|
var etherBalance = web3Local.utils.fromWei(balance, "ether");
|
||||||
|
//$("#sendFromAddressBalance").text("Balance: " + etherBalance + " ETHO");
|
||||||
|
adddressObject.balance = parseFloat(etherBalance)
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
$("#dlgAddressList").iziModal({width: "800px"});
|
$("#dlgAddressList").iziModal({width: "800px"});
|
||||||
EthoMainGUI.renderTemplate("addresslist.html", addressList, $("#dlgAddressListBody"));
|
EthoMainGUI.renderTemplate("addresslist.html", addressList, $("#dlgAddressListBody"));
|
||||||
|
|||||||
520
renderer/staking.js
Normal file
@@ -0,0 +1,520 @@
|
|||||||
|
// PaperclipChain Staking Interface
|
||||||
|
// Handles all staking-related functionality in the wallet
|
||||||
|
|
||||||
|
const { ipcRenderer } = require('electron');
|
||||||
|
|
||||||
|
class StakingManager {
|
||||||
|
constructor() {
|
||||||
|
this.validators = [];
|
||||||
|
this.stakingInfo = null;
|
||||||
|
this.totalStaked = 0;
|
||||||
|
this.pendingRewards = 0;
|
||||||
|
this.refreshInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
console.log('Initializing staking manager...');
|
||||||
|
await this.loadStakingData();
|
||||||
|
this.setupEventHandlers();
|
||||||
|
this.startAutoRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadStakingData() {
|
||||||
|
try {
|
||||||
|
// Load validators
|
||||||
|
this.validators = await ipcRenderer.invoke('paperclip-get-validators');
|
||||||
|
|
||||||
|
// Load total staked
|
||||||
|
this.totalStaked = await ipcRenderer.invoke('paperclip-get-total-staked');
|
||||||
|
|
||||||
|
// Load user staking info if wallet is selected
|
||||||
|
if (ClipsWallet.addressList.length > 0) {
|
||||||
|
const currentAddress = ClipsWallet.addressList[0]; // Use first wallet as current
|
||||||
|
this.stakingInfo = await ipcRenderer.invoke('paperclip-get-staking-info', currentAddress);
|
||||||
|
this.pendingRewards = await ipcRenderer.invoke('paperclip-get-staking-rewards', currentAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateUI();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load staking data:', error);
|
||||||
|
this.showError('Failed to load staking data: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUI() {
|
||||||
|
// Update overview cards
|
||||||
|
$('#totalStakedAmount').text(this.formatClips(this.totalStaked));
|
||||||
|
$('#pendingRewards').text(this.formatClips(this.pendingRewards));
|
||||||
|
$('#activeValidators').text(this.validators.filter(v => v.active).length);
|
||||||
|
|
||||||
|
// Update validators table
|
||||||
|
this.updateValidatorsTable();
|
||||||
|
|
||||||
|
// Update progress bars
|
||||||
|
this.updateProgressBars();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateValidatorsTable() {
|
||||||
|
const tbody = $('#validatorsTableBody');
|
||||||
|
tbody.empty();
|
||||||
|
|
||||||
|
this.validators.forEach(validator => {
|
||||||
|
const apy = this.calculateValidatorAPY(validator);
|
||||||
|
const userStake = this.getUserStakeForValidator(validator.address);
|
||||||
|
const status = validator.active ?
|
||||||
|
'<span class="badge badge-success">Active</span>' :
|
||||||
|
'<span class="badge badge-danger">Inactive</span>';
|
||||||
|
|
||||||
|
const row = `
|
||||||
|
<tr data-validator="${validator.address}">
|
||||||
|
<td>
|
||||||
|
<div class="validator-info">
|
||||||
|
<strong>${validator.address.substring(0, 12)}...</strong>
|
||||||
|
<br><small class="text-muted">${validator.address}</small>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>${status}</td>
|
||||||
|
<td>${this.formatClips(validator.stakedAmount || 0)}</td>
|
||||||
|
<td>${this.formatClips(validator.delegatedAmount || 0)}</td>
|
||||||
|
<td>${(validator.commission / 100).toFixed(2)}%</td>
|
||||||
|
<td>~${apy.toFixed(2)}%</td>
|
||||||
|
<td>${this.formatClips(userStake)}</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group-sm">
|
||||||
|
<button class="btn btn-sm btn-clips btn-delegate" data-validator="${validator.address}" ${!validator.active ? 'disabled' : ''}>
|
||||||
|
Delegate
|
||||||
|
</button>
|
||||||
|
${userStake > 0 ? `<button class="btn btn-sm btn-warning btn-undelegate" data-validator="${validator.address}">Undelegate</button>` : ''}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
tbody.append(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize DataTable if not already initialized
|
||||||
|
if (!$.fn.DataTable.isDataTable('#validatorsTable')) {
|
||||||
|
$('#validatorsTable').DataTable({
|
||||||
|
pageLength: 10,
|
||||||
|
responsive: true,
|
||||||
|
order: [[2, 'desc']] // Sort by self stake desc
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$('#validatorsTable').DataTable().draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProgressBars() {
|
||||||
|
// Calculate total network stake percentage (mock calculation)
|
||||||
|
const networkSupply = 1000000000; // 1B CLIPS total supply (mock)
|
||||||
|
const stakedPercent = (this.totalStaked / networkSupply) * 100;
|
||||||
|
$('#totalStakedProgress').css('width', Math.min(stakedPercent, 100) + '%');
|
||||||
|
|
||||||
|
// Update rewards progress (mock)
|
||||||
|
const rewardsPercent = Math.min((this.pendingRewards / 1000) * 100, 100);
|
||||||
|
$('#rewardsProgress').css('width', rewardsPercent + '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventHandlers() {
|
||||||
|
// Navigation
|
||||||
|
$('#mainNavBtnStaking').off('click').on('click', () => {
|
||||||
|
this.showStakingPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refresh button
|
||||||
|
$('#btnRefreshValidators').off('click').on('click', () => {
|
||||||
|
this.loadStakingData();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Quick action buttons
|
||||||
|
$('#btnDelegate').off('click').on('click', () => {
|
||||||
|
this.showDelegateModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#btnUndelegate').off('click').on('click', () => {
|
||||||
|
this.showUndelegateModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#btnClaimRewards').off('click').on('click', () => {
|
||||||
|
this.claimRewards();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#btnCreateValidator').off('click').on('click', () => {
|
||||||
|
this.showCreateValidatorModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Table action buttons (delegated event handling)
|
||||||
|
$(document).off('click', '.btn-delegate').on('click', '.btn-delegate', (e) => {
|
||||||
|
const validatorAddress = $(e.target).data('validator');
|
||||||
|
this.showDelegateModal(validatorAddress);
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).off('click', '.btn-undelegate').on('click', '.btn-undelegate', (e) => {
|
||||||
|
const validatorAddress = $(e.target).data('validator');
|
||||||
|
this.showUndelegateModal(validatorAddress);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Modal handlers
|
||||||
|
$('#btnDelegateConfirm').off('click').on('click', () => {
|
||||||
|
this.performDelegation();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#btnUndelegateConfirm').off('click').on('click', () => {
|
||||||
|
this.performUndelegation();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#btnCreateValidatorConfirm').off('click').on('click', () => {
|
||||||
|
this.createValidator();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Modal cancel buttons
|
||||||
|
$('#btnDelegateCancel, #btnUndelegateCancel, #btnCreateValidatorCancel').off('click').on('click', function() {
|
||||||
|
$(this).closest('.modalDialog').iziModal('close');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showStakingPage() {
|
||||||
|
EthoMainGUI.renderTemplate('staking.html', {});
|
||||||
|
this.loadStakingData();
|
||||||
|
this.setupEventHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
showDelegateModal(validatorAddress = null) {
|
||||||
|
// Populate validator dropdown
|
||||||
|
const validatorSelect = $('#delegateValidator');
|
||||||
|
validatorSelect.empty().append('<option value="">Select a validator...</option>');
|
||||||
|
|
||||||
|
this.validators.filter(v => v.active).forEach(validator => {
|
||||||
|
const option = `<option value="${validator.address}">${validator.address.substring(0, 20)}... (${(validator.commission / 100).toFixed(2)}% commission)</option>`;
|
||||||
|
validatorSelect.append(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (validatorAddress) {
|
||||||
|
validatorSelect.val(validatorAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate wallet dropdown
|
||||||
|
this.populateWalletDropdown('#delegateWallet');
|
||||||
|
|
||||||
|
// Update available balance
|
||||||
|
this.updateAvailableBalance('#availableBalance');
|
||||||
|
|
||||||
|
$('#dlgDelegate').iziModal('open');
|
||||||
|
}
|
||||||
|
|
||||||
|
showUndelegateModal(validatorAddress = null) {
|
||||||
|
// Populate validator dropdown with only validators user has stake with
|
||||||
|
const validatorSelect = $('#undelegateValidator');
|
||||||
|
validatorSelect.empty().append('<option value="">Select a validator...</option>');
|
||||||
|
|
||||||
|
this.validators.forEach(validator => {
|
||||||
|
const userStake = this.getUserStakeForValidator(validator.address);
|
||||||
|
if (userStake > 0) {
|
||||||
|
const option = `<option value="${validator.address}">${validator.address.substring(0, 20)}... (${this.formatClips(userStake)} staked)</option>`;
|
||||||
|
validatorSelect.append(option);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (validatorAddress) {
|
||||||
|
validatorSelect.val(validatorAddress);
|
||||||
|
this.updateYourStakeAmount(validatorAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate wallet dropdown
|
||||||
|
this.populateWalletDropdown('#undelegateWallet');
|
||||||
|
|
||||||
|
// Event handler for validator selection change
|
||||||
|
validatorSelect.off('change').on('change', (e) => {
|
||||||
|
this.updateYourStakeAmount(e.target.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#dlgUndelegate').iziModal('open');
|
||||||
|
}
|
||||||
|
|
||||||
|
showCreateValidatorModal() {
|
||||||
|
// Populate wallet dropdown
|
||||||
|
this.populateWalletDropdown('#validatorWallet');
|
||||||
|
|
||||||
|
$('#dlgCreateValidator').iziModal('open');
|
||||||
|
}
|
||||||
|
|
||||||
|
async performDelegation() {
|
||||||
|
try {
|
||||||
|
const validatorAddress = $('#delegateValidator').val();
|
||||||
|
const amount = parseFloat($('#delegateAmount').val());
|
||||||
|
const walletAddress = $('#delegateWallet').val();
|
||||||
|
|
||||||
|
if (!validatorAddress || !amount || !walletAddress || amount < 100) {
|
||||||
|
this.showError('Please fill all fields correctly. Minimum delegation is 100 CLIPS.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ClipsWallet.getAddressExists(walletAddress)) {
|
||||||
|
this.showError('Selected wallet not found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create delegation transaction
|
||||||
|
const txData = {
|
||||||
|
sender: walletAddress,
|
||||||
|
receiver: validatorAddress,
|
||||||
|
amount: Math.floor(amount * 1000000), // Convert to micro-CLIPS
|
||||||
|
txType: 'delegate',
|
||||||
|
gas: 30000,
|
||||||
|
gasPrice: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sign and broadcast transaction
|
||||||
|
const result = await this.sendStakingTransaction(txData, { address: walletAddress });
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
this.showSuccess('Delegation successful!');
|
||||||
|
$('#dlgDelegate').iziModal('close');
|
||||||
|
await this.loadStakingData();
|
||||||
|
} else {
|
||||||
|
this.showError('Delegation failed: ' + result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Delegation failed:', error);
|
||||||
|
this.showError('Delegation failed: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async performUndelegation() {
|
||||||
|
try {
|
||||||
|
const validatorAddress = $('#undelegateValidator').val();
|
||||||
|
const amount = parseFloat($('#undelegateAmount').val());
|
||||||
|
const walletAddress = $('#undelegateWallet').val();
|
||||||
|
|
||||||
|
if (!validatorAddress || !amount || !walletAddress || amount <= 0) {
|
||||||
|
this.showError('Please fill all fields correctly.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ClipsWallet.getAddressExists(walletAddress)) {
|
||||||
|
this.showError('Selected wallet not found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create undelegation transaction
|
||||||
|
const txData = {
|
||||||
|
sender: walletAddress,
|
||||||
|
receiver: validatorAddress,
|
||||||
|
amount: Math.floor(amount * 1000000), // Convert to micro-CLIPS
|
||||||
|
txType: 'undelegate',
|
||||||
|
gas: 30000,
|
||||||
|
gasPrice: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sign and broadcast transaction
|
||||||
|
const result = await this.sendStakingTransaction(txData, { address: walletAddress });
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
this.showSuccess('Undelegation successful!');
|
||||||
|
$('#dlgUndelegate').iziModal('close');
|
||||||
|
await this.loadStakingData();
|
||||||
|
} else {
|
||||||
|
this.showError('Undelegation failed: ' + result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Undelegation failed:', error);
|
||||||
|
this.showError('Undelegation failed: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createValidator() {
|
||||||
|
try {
|
||||||
|
const stakeAmount = parseFloat($('#validatorStake').val());
|
||||||
|
const commission = parseFloat($('#validatorCommission').val());
|
||||||
|
const walletAddress = $('#validatorWallet').val();
|
||||||
|
|
||||||
|
if (!stakeAmount || !walletAddress || stakeAmount < 10000 || commission < 0 || commission > 50) {
|
||||||
|
this.showError('Please fill all fields correctly. Minimum stake is 10,000 CLIPS, commission must be 0-50%.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ClipsWallet.getAddressExists(walletAddress)) {
|
||||||
|
this.showError('Selected wallet not found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create validator creation transaction
|
||||||
|
const txData = {
|
||||||
|
sender: walletAddress,
|
||||||
|
receiver: '',
|
||||||
|
amount: Math.floor(stakeAmount * 1000000), // Convert to micro-CLIPS
|
||||||
|
txType: 'create_validator',
|
||||||
|
gas: 30000,
|
||||||
|
gasPrice: 1,
|
||||||
|
data: JSON.stringify({ commission: Math.floor(commission * 100) }) // Convert to basis points
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sign and broadcast transaction
|
||||||
|
const result = await this.sendStakingTransaction(txData, { address: walletAddress });
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
this.showSuccess('Validator created successfully!');
|
||||||
|
$('#dlgCreateValidator').iziModal('close');
|
||||||
|
await this.loadStakingData();
|
||||||
|
} else {
|
||||||
|
this.showError('Validator creation failed: ' + result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Validator creation failed:', error);
|
||||||
|
this.showError('Validator creation failed: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async claimRewards() {
|
||||||
|
try {
|
||||||
|
if (ClipsWallet.addressList.length === 0) {
|
||||||
|
this.showError('No wallet available.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.pendingRewards <= 0) {
|
||||||
|
this.showError('No rewards to claim.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentAddress = ClipsWallet.addressList[0];
|
||||||
|
|
||||||
|
// Create claim rewards transaction
|
||||||
|
const txData = {
|
||||||
|
sender: currentAddress,
|
||||||
|
receiver: '',
|
||||||
|
amount: 0,
|
||||||
|
txType: 'claim_rewards',
|
||||||
|
gas: 30000,
|
||||||
|
gasPrice: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sign and broadcast transaction
|
||||||
|
const result = await this.sendStakingTransaction(txData, { address: currentAddress });
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
this.showSuccess('Rewards claimed successfully!');
|
||||||
|
await this.loadStakingData();
|
||||||
|
} else {
|
||||||
|
this.showError('Claim failed: ' + result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Claim failed:', error);
|
||||||
|
this.showError('Claim failed: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendStakingTransaction(txData, wallet) {
|
||||||
|
try {
|
||||||
|
// Get current nonce
|
||||||
|
const nonce = await ipcRenderer.invoke('paperclip-get-nonce', wallet.address);
|
||||||
|
txData.nonce = nonce + 1;
|
||||||
|
|
||||||
|
// Sign transaction (this would use the actual wallet signing)
|
||||||
|
const signedTx = await ClipsWallet.signTransaction(txData, wallet);
|
||||||
|
|
||||||
|
// Broadcast transaction
|
||||||
|
const result = await ipcRenderer.invoke('paperclip-broadcast-tx', signedTx);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility functions
|
||||||
|
populateWalletDropdown(selector) {
|
||||||
|
const walletSelect = $(selector);
|
||||||
|
walletSelect.empty();
|
||||||
|
|
||||||
|
ClipsWallet.addressList.forEach((address, index) => {
|
||||||
|
const option = `<option value="${address}">Wallet ${index + 1} (${address.substring(0, 12)}...)</option>`;
|
||||||
|
walletSelect.append(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateAvailableBalance(selector) {
|
||||||
|
if (ClipsWallet.addressList.length > 0) {
|
||||||
|
try {
|
||||||
|
const currentAddress = ClipsWallet.addressList[0];
|
||||||
|
const balance = await ipcRenderer.invoke('paperclip-get-balance', currentAddress);
|
||||||
|
$(selector).text(this.formatClips(balance));
|
||||||
|
} catch (error) {
|
||||||
|
$(selector).text('0 CLIPS');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$(selector).text('0 CLIPS');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateYourStakeAmount(validatorAddress) {
|
||||||
|
const userStake = this.getUserStakeForValidator(validatorAddress);
|
||||||
|
$('#yourStakeAmount').text(this.formatClips(userStake));
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserStakeForValidator(validatorAddress) {
|
||||||
|
if (!this.stakingInfo || !this.stakingInfo.delegations) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const delegation = this.stakingInfo.delegations.find(d => d.validator === validatorAddress);
|
||||||
|
return delegation ? delegation.amount : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateValidatorAPY(validator) {
|
||||||
|
// Simplified APY calculation
|
||||||
|
const baseAPY = 8.0; // 8% base APY
|
||||||
|
const totalStake = (validator.stakedAmount || 0) + (validator.delegatedAmount || 0);
|
||||||
|
if (totalStake === 0) return 0;
|
||||||
|
|
||||||
|
// Adjust APY based on commission
|
||||||
|
const adjustedAPY = baseAPY * (1 - (validator.commission || 0) / 10000);
|
||||||
|
return Math.max(adjustedAPY, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
formatClips(microClips) {
|
||||||
|
return (microClips / 1000000).toFixed(6) + ' CLIPS';
|
||||||
|
}
|
||||||
|
|
||||||
|
startAutoRefresh() {
|
||||||
|
// Refresh staking data every 30 seconds
|
||||||
|
this.refreshInterval = setInterval(() => {
|
||||||
|
this.loadStakingData();
|
||||||
|
}, 30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
stopAutoRefresh() {
|
||||||
|
if (this.refreshInterval) {
|
||||||
|
clearInterval(this.refreshInterval);
|
||||||
|
this.refreshInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showError(message) {
|
||||||
|
iziToast.error({
|
||||||
|
title: 'Staking Error',
|
||||||
|
message: message,
|
||||||
|
position: 'topRight'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuccess(message) {
|
||||||
|
iziToast.success({
|
||||||
|
title: 'Staking Success',
|
||||||
|
message: message,
|
||||||
|
position: 'topRight'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize staking manager
|
||||||
|
const PaperclipStaking = new StakingManager();
|
||||||
|
|
||||||
|
// Export for use in other modules
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
module.exports = PaperclipStaking;
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ SyncProgress = new ProgressBar.Line("#syncProgress", {
|
|||||||
strokeWidth: 6,
|
strokeWidth: 6,
|
||||||
easing: "easeInOut",
|
easing: "easeInOut",
|
||||||
duration: 1400,
|
duration: 1400,
|
||||||
color: "#7A1336",
|
color: "#25D4DC",
|
||||||
trailColor: "#eee",
|
trailColor: "#eee",
|
||||||
trailWidth: 1,
|
trailWidth: 1,
|
||||||
text: {
|
text: {
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
const {ipcRenderer} = require("electron");
|
const {ipcRenderer} = require("electron");
|
||||||
|
|
||||||
class Wallets {
|
class PaperclipWallets {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.addressList = [];
|
this.addressList = [];
|
||||||
|
this.price = 0; // CLIPS price placeholder
|
||||||
$.getJSON("https://min-api.cryptocompare.com/data/price?fsym=ETHO&tsyms=USD", function (price) {
|
|
||||||
EthoWallets._setPrice(price.USD);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_getPrice() {
|
_getPrice() {
|
||||||
@@ -35,32 +32,32 @@ class Wallets {
|
|||||||
|
|
||||||
addAddressToList(address) {
|
addAddressToList(address) {
|
||||||
if (address) {
|
if (address) {
|
||||||
this.addressList.push(address.toLowerCase());
|
this.addressList.push(address); // CLIPS addresses are case-sensitive
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enableButtonTooltips() {
|
enableButtonTooltips() {
|
||||||
EthoUtils.createToolTip("#btnNewAddress", "Create New Address");
|
PaperclipUtils.createToolTip("#btnNewAddress", "Create New Address");
|
||||||
EthoUtils.createToolTip("#btnRefreshAddress", "Refresh Address List");
|
PaperclipUtils.createToolTip("#btnRefreshAddress", "Refresh Address List");
|
||||||
EthoUtils.createToolTip("#btnExportAccounts", "Export Accounts");
|
PaperclipUtils.createToolTip("#btnExportAccounts", "Export Accounts");
|
||||||
EthoUtils.createToolTip("#btnImportAccounts", "Import Accounts");
|
PaperclipUtils.createToolTip("#btnImportAccounts", "Import Accounts");
|
||||||
EthoUtils.createToolTip("#btnImportFromPrivateKey", "Import From Private Key");
|
PaperclipUtils.createToolTip("#btnImportFromPrivateKey", "Import From Private Key");
|
||||||
}
|
}
|
||||||
|
|
||||||
validateNewAccountForm() {
|
validateNewAccountForm() {
|
||||||
if (EthoMainGUI.getAppState() == "account") {
|
if (PaperclipMainGUI.getAppState() == "account") {
|
||||||
if (!$("#walletPasswordFirst").val()) {
|
if (!$("#walletPasswordFirst").val()) {
|
||||||
EthoMainGUI.showGeneralError("Password cannot be empty!");
|
PaperclipMainGUI.showGeneralError("Password cannot be empty!");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$("#walletPasswordSecond").val()) {
|
if (!$("#walletPasswordSecond").val()) {
|
||||||
EthoMainGUI.showGeneralError("Password cannot be empty!");
|
PaperclipMainGUI.showGeneralError("Password cannot be empty!");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($("#walletPasswordFirst").val() !== $("#walletPasswordSecond").val()) {
|
if ($("#walletPasswordFirst").val() !== $("#walletPasswordSecond").val()) {
|
||||||
EthoMainGUI.showGeneralError("Passwords do not match!");
|
PaperclipMainGUI.showGeneralError("Passwords do not match!");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +111,7 @@ class Wallets {
|
|||||||
$(document).trigger("render_wallets");
|
$(document).trigger("render_wallets");
|
||||||
EthoWallets.enableButtonTooltips();
|
EthoWallets.enableButtonTooltips();
|
||||||
|
|
||||||
$("#labelSumDollars").html(vsprintf("/ %.2f $ / %.4f $ per ETHO", [
|
$("#labelSumDollars").html(vsprintf("= %.2f 💵 | %.4f 💵 per", [
|
||||||
data.sumBalance * EthoWallets._getPrice(),
|
data.sumBalance * EthoWallets._getPrice(),
|
||||||
EthoWallets._getPrice()
|
EthoWallets._getPrice()
|
||||||
]));
|
]));
|
||||||
@@ -231,15 +228,23 @@ $(document).on("render_wallets", function () {
|
|||||||
ipcRenderer.send("exportAccounts", {});
|
ipcRenderer.send("exportAccounts", {});
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#btnImportAccounts").off("click").on("click", function () {
|
$("#btnImportAccounts").off("click").on("click", function () {
|
||||||
var ImportResult = ipcRenderer.sendSync("importAccounts", {});
|
ipcRenderer.send("importAccounts");
|
||||||
|
|
||||||
|
ipcRenderer.once("importAccountsReply", (event, importResult) => {
|
||||||
|
if (importResult.success) {
|
||||||
|
iziToast.success({ title: "Imported", message: importResult.text, position: "topRight", timeout: 2000 });
|
||||||
|
// Reload wallets after successful import
|
||||||
|
setTimeout(() => {
|
||||||
|
EthoWallets.renderWalletsState();
|
||||||
|
}, 500);
|
||||||
|
} else {
|
||||||
|
EthoMainGUI.showGeneralError(importResult.text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
if (ImportResult.success) {
|
|
||||||
iziToast.success({title: "Imported", message: ImportResult.text, position: "topRight", timeout: 2000});
|
|
||||||
} else if (ImportResult.success == false) {
|
|
||||||
EthoMainGUI.showGeneralError(ImportResult.text);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#btnImportFromPrivateKey").off("click").on("click", function () {
|
$("#btnImportFromPrivateKey").off("click").on("click", function () {
|
||||||
$("#dlgImportFromPrivateKey").iziModal();
|
$("#dlgImportFromPrivateKey").iziModal();
|
||||||
|
|||||||
85
test-wallet.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
// Simple test of wallet functionality outside Electron
|
||||||
|
const ClipsCrypto = require('./modules/clips-crypto.js');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
console.log('Testing PaperclipWallet core functionality...\n');
|
||||||
|
|
||||||
|
// Test 1: Key generation
|
||||||
|
console.log('1. Testing key generation:');
|
||||||
|
const keyPair = ClipsCrypto.generateKeyPair();
|
||||||
|
console.log(' Generated address:', keyPair.address);
|
||||||
|
console.log(' Address format valid:', ClipsCrypto.isValidAddress(keyPair.address));
|
||||||
|
console.log(' Private key length:', keyPair.privateKey.length);
|
||||||
|
console.log(' Public key length:', keyPair.publicKey.length);
|
||||||
|
|
||||||
|
// Test 2: Address validation
|
||||||
|
console.log('\n2. Testing address validation:');
|
||||||
|
const validAddresses = [
|
||||||
|
keyPair.address,
|
||||||
|
'CLIP-1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF'
|
||||||
|
];
|
||||||
|
const invalidAddresses = [
|
||||||
|
'CLIP-123',
|
||||||
|
'ETH-1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF',
|
||||||
|
'CLIP-GGGG567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF',
|
||||||
|
'CLIP1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF'
|
||||||
|
];
|
||||||
|
|
||||||
|
validAddresses.forEach(addr => {
|
||||||
|
console.log(` ${addr}: ${ClipsCrypto.isValidAddress(addr) ? 'VALID' : 'INVALID'}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
invalidAddresses.forEach(addr => {
|
||||||
|
console.log(` ${addr}: ${ClipsCrypto.isValidAddress(addr) ? 'VALID' : 'INVALID'}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test 3: Transaction creation and signing
|
||||||
|
console.log('\n3. Testing transaction creation:');
|
||||||
|
const transaction = ClipsCrypto.createTransaction({
|
||||||
|
sender: keyPair.address,
|
||||||
|
receiver: 'CLIP-1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF',
|
||||||
|
amount: 1000,
|
||||||
|
nonce: 0,
|
||||||
|
type: 'transfer',
|
||||||
|
data: '',
|
||||||
|
privateKey: keyPair.privateKey
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(' Transaction created successfully:', !!transaction);
|
||||||
|
console.log(' Transaction signed:', !!transaction.signature);
|
||||||
|
console.log(' Signature length:', transaction.signature ? transaction.signature.length : 0);
|
||||||
|
|
||||||
|
// Test 4: Transaction verification
|
||||||
|
console.log('\n4. Testing transaction verification:');
|
||||||
|
const isValid = ClipsCrypto.verifyTransaction(transaction, keyPair.publicKey);
|
||||||
|
console.log(' Transaction signature valid:', isValid);
|
||||||
|
|
||||||
|
// Test 5: Transaction serialization
|
||||||
|
console.log('\n5. Testing transaction serialization:');
|
||||||
|
const txHex = ClipsCrypto.transactionToHex(transaction);
|
||||||
|
console.log(' Transaction hex length:', txHex.length);
|
||||||
|
console.log(' Transaction hex (first 100 chars):', txHex.substring(0, 100) + '...');
|
||||||
|
|
||||||
|
const deserializedTx = ClipsCrypto.hexToTransaction(txHex);
|
||||||
|
console.log(' Deserialization successful:', deserializedTx.sender === transaction.sender);
|
||||||
|
|
||||||
|
// Test 6: Private key import
|
||||||
|
console.log('\n6. Testing private key import:');
|
||||||
|
try {
|
||||||
|
const importedWallet = ClipsCrypto.importPrivateKey(keyPair.privateKey);
|
||||||
|
console.log(' Import successful:', importedWallet.address === keyPair.address);
|
||||||
|
console.log(' Imported address:', importedWallet.address);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(' Import failed:', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 7: Seed phrase generation
|
||||||
|
console.log('\n7. Testing seed phrase generation:');
|
||||||
|
const seedPhrase = ClipsCrypto.generateSeedPhrase();
|
||||||
|
console.log(' Seed phrase:', seedPhrase);
|
||||||
|
console.log(' Word count:', seedPhrase.split(' ').length);
|
||||||
|
|
||||||
|
console.log('\nAll core wallet functionality tests completed!');
|
||||||
|
console.log('\nThe wallet crypto layer is working correctly.');
|
||||||
|
console.log('Next step: Test the full wallet with Electron interface.');
|
||||||