Compare commits

..

10 Commits

Author SHA1 Message Date
4b597ebc07 fix: Remove large binary files from git tracking
- Add bin/ directory to .gitignore to exclude 116MB of binaries
- Remove geth binaries from git tracking (85MB linux + 31MB macos)
- Reduces repository size significantly for faster cloning/pushing
2025-06-17 15:10:27 -07:00
a4db7394b0 docs: Remove excessive formatting from documentation
- Strip bold text and emoji formatting from README.md
- Clean up CHANGELOG.md formatting
- Maintain professional tone without unnecessary emphasis
2025-06-17 14:49:57 -07:00
97a11985e7 docs: Update documentation for staking features
- Comprehensive README update with staking guide and setup instructions
- Add CHANGELOG.md documenting major DeFi staking release
- Update package.json description and keywords for staking capabilities
- Include troubleshooting section and development guidelines
2025-06-17 14:42:54 -07:00
2c3ad62bc4 feat: Add comprehensive staking interface to PaperclipWallet
- Add staking RPC methods for validators, delegation, and rewards
- Implement complete staking UI with validator selection and delegation
- Add reward claiming functionality and validator creation interface
- Include professional staking dashboard with real-time data
- Integrate staking navigation into existing wallet interface
2025-06-17 14:38:53 -07:00
8414593d47 Add comprehensive wallet testing and fix database export
- Create test-wallet.js for standalone functionality testing
- Fix PaperclipDatabase naming conflict for Node.js compatibility
- Verify Ed25519 key generation, signing, and verification
- Test CLIPS address validation and transaction serialization
- All core crypto functionality working correctly
2025-06-15 18:42:02 -07:00
1ceb56c7b8 Update wallet UI to use PaperclipChain
- Remove Ethereum/Web3 references from HTML
- Create PaperclipWallets class with Ed25519 key support
- Add PaperclipDatabase for wallet storage
- Update module loading order in index.html
- Convert button classes from btn-etho to btn-clips
2025-06-15 18:39:48 -07:00
dab604463f Clean up logging and remove excessive styling 2025-06-15 17:49:10 -07:00
f3367b22dc feat: Convert Ether1 wallet to PaperclipWallet
- Update package.json with PaperclipWallet branding
- Replace Ethereum Web3 with Tendermint RPC client
- Implement CLIPS address format (CLIP-{64-hex})
- Add Ed25519 key management with tweetnacl
- Create PaperclipChain blockchain interface
- Support gas system and transaction types
- Add smart contract and multisig support
2025-06-15 16:32:44 -07:00
Exlo
d1c1c835f2 Merge pull request #100 from Ether1Project/RebrandingAndImprovements
rebranding upgrade and various fixes
2024-04-29 07:46:05 +02:00
d0x
6db10719a7 rebranding upgrade and various fixes 2024-04-28 22:20:58 -07:00
44 changed files with 23305 additions and 2979 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
node_modules node_modules
.DS_Store .DS_Store
dist dist
bin/

81
CHANGELOG.md Normal file
View 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
View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 16 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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>

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5a0d13c1c0aea93d72c457d318f4fa3bfb6fcf9b792b25055ea51df2b1047f11
size 23514025

View File

@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ca2e254777323d66dcc483e967d30babeb126f082dddc540b947e1f55be7602c
size 111578699

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -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&amp;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>
@@ -66,23 +66,44 @@
<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 = [
{ text: "Starting PaperclipChain node...", delay: 1250 },
{ text: "Connecting to network...", delay: 6400 },
{ 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);
});
$(document).on("onNodeReady", function () {
setTimeout(() => { setTimeout(() => {
loading_screen.finish(); loading_screen.finish();
}, 4000); }, 5500);
}); });
$(window).on("beforeunload", function () { $(window).on("beforeunload", function () {
EthoBlockchain.closeConnection(); if (window.PaperclipChain) {
}) PaperclipChain.stopConnection();
}
});
</script> </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>

View File

@@ -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()
@@ -74,6 +75,7 @@ locker.lock().then(function() {
// 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) => {
event.returnValue = fs.readFileSync(path.join(app.getAppPath(), "assets/templates/") + arg, "utf8"); event.returnValue = fs.readFileSync(path.join(app.getAppPath(), "assets/templates/") + arg, "utf8");

View File

@@ -8,24 +8,49 @@ class Accounts {
constructor() {} constructor() {}
getKeyStoreLocation() { getKeyStoreLocation() {
switch (os.type()) { const platform = os.type();
let keystorePath;
switch (platform) {
case "Darwin": case "Darwin":
return path.join(os.homedir(), "Library", "Ether1", "keystore"); keystorePath = path.join(os.homedir(), "Library", "Ether1", "keystore");
break;
case "Windows_NT":
if (process.env.APPDATA) {
keystorePath = path.join(process.env.APPDATA.replace('Roaming', 'Local'), "Ether1", "keystore");
} else {
keystorePath = path.join(app.getPath("userData"), "Ether1", "keystore");
}
break;
case "Linux":
keystorePath = path.join(app.getPath("home"), ".ether1", "keystore");
break; break;
default: default:
return path.join(process.env.APPDATA.replace('Roaming', 'Local'), "Ether1", "keystore"); keystorePath = path.join(app.getPath("userData"), "Ether1", "keystore");
break;
} }
return Promise.resolve(keystorePath);
} }
exportAccounts() { exportAccounts() {
var savePath = dialog.showSaveDialog({ dialog.showSaveDialog({
defaultPath: path.join(app.getPath("documents"), "accounts.zip") defaultPath: path.join(app.getPath("documents"), "accounts.zip")
}); }).then(result => {
if (!result.canceled) {
if (savePath) { const savePath = result.filePath;
const accPath = EthoAccounts.getKeyStoreLocation(); const accPathPromise = this.getKeyStoreLocation();
accPathPromise.then(accPath => {
fs.readdir(accPath, function (err, files) { fs.readdir(accPath, function (err, files) {
if (err) {
console.error("Error reading directory:", err);
// Handle the error, e.g., show an error dialog to the user
return;
}
var zip = new admZip(); var zip = new admZip();
for (let filePath of files) { for (let filePath of files) {
@@ -35,95 +60,130 @@ class Accounts {
// store zip to path // store zip to path
zip.writeZip(savePath); zip.writeZip(savePath);
}); });
}).catch(err => {
console.error("Error getting keystore location:", err);
// Handle the error, e.g., show an error dialog to the user
});
} }
}).catch(err => {
console.error("Error showing save dialog:", err);
// Handle the error, e.g., show an error dialog to the user
});
} }
importAccounts(accountsFile) {
var extName = path.extname(accountsFile).toUpperCase();
const accPath = EthoAccounts.getKeyStoreLocation();
if (extName == ".ZIP") { async importAccounts() {
var zip = new admZip(accountsFile);
zip.extractAllTo(accPath, true);
return {success: true, text: "Accounts ware successfully imported."};
} else {
try { try {
fs.copySync(accountsFile, path.join(accPath, path.basename(accountsFile))); const openPath = await dialog.showOpenDialog({
return {success: true, text: "Account was successfully imported."};
} catch (err) {
return {success: false, text: err};
}
}
}
saveAccount(account) {
fs.writeFile(path.join(tEthoAccountshis.getKeyStoreLocation(), "0x" + account.address), JSON.stringify(account), "utf8", function () {
// file was written
});
}
deteteAccount(address) {
return new Promise((resolve, reject) => {
const accPath = EthoAccounts.getKeyStoreLocation();
fs.readdir(accPath, function (err, files) {
let deleteFilePath = null;
if (err) reject(err);
else {
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)
}
});
});
}
}
ipcMain.on("exportAccounts", (event, arg) => {
EthoAccounts.exportAccounts();
});
ipcMain.on("importAccounts", (event, arg) => {
var openPath = dialog.showOpenDialog({
defaultPath: app.getPath("documents"), defaultPath: app.getPath("documents"),
filters: [ filters: [
{ {
name: "archive", name: "archive",
extensions: ["zip"] extensions: ["zip"]
}, { },
{
name: "json", name: "json",
extensions: ["json"] extensions: ["json"]
}, { },
{
name: "All", name: "All",
extensions: ["*"] extensions: ["*"]
} }
] ]
}); });
if (openPath) { if (!openPath.canceled && openPath.filePaths.length > 0) {
event.returnValue = EthoAccounts.importAccounts(openPath[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 { } else {
event.returnValue = {}; 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) => {
const ethoAccounts = new Accounts();
ethoAccounts.exportAccounts();
});
ipcMain.on("importAccounts", async (event, arg) => {
const ethoAccounts = new Accounts();
try {
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();
ethoAccounts.saveAccount(arg);
event.returnValue = true; event.returnValue = true;
}); });
ipcMain.on("deteteAccount", (event, arg) => { ipcMain.on("deteteAccount", (event, arg) => {
EthoAccounts.deteteAccount(arg) const ethoAccounts = new Accounts();
ethoAccounts.deteteAccount(arg)
.then((res) => { .then((res) => {
event.returnValue = res; event.returnValue = res;
}) })
@@ -132,4 +192,3 @@ ipcMain.on("deteteAccount", (event, arg) => {
}); });
}); });
EthoAccounts = new Accounts();

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

@@ -0,0 +1,236 @@
const nacl = require('tweetnacl');
const crypto = require('crypto');
class ClipsCrypto {
constructor() {
this.addressPrefix = 'CLIP-';
}
/**
* Generate a new Ed25519 key pair
* @returns {Object} Contains publicKey, privateKey, and address
*/
generateKeyPair() {
const keyPair = nacl.sign.keyPair();
return {
publicKey: Buffer.from(keyPair.publicKey).toString('hex'),
privateKey: Buffer.from(keyPair.secretKey).toString('hex'),
address: this.publicKeyToAddress(keyPair.publicKey)
};
}
/**
* Convert public key to CLIPS address format
* @param {Uint8Array|Buffer|string} publicKey - The public key
* @returns {string} CLIPS address
*/
publicKeyToAddress(publicKey) {
let pubKeyBuffer;
if (typeof publicKey === 'string') {
pubKeyBuffer = Buffer.from(publicKey, 'hex');
} else if (publicKey instanceof Uint8Array) {
pubKeyBuffer = Buffer.from(publicKey);
} else {
pubKeyBuffer = publicKey;
}
return this.addressPrefix + pubKeyBuffer.toString('hex').toUpperCase();
}
/**
* Validate CLIPS address format
* @param {string} address - The address to validate
* @returns {boolean} True if valid
*/
isValidAddress(address) {
if (!address || typeof address !== 'string') {
return false;
}
// Check prefix
if (!address.startsWith(this.addressPrefix)) {
return false;
}
// Check hex part (should be 64 characters after prefix)
const hexPart = address.slice(this.addressPrefix.length);
if (hexPart.length !== 64) {
return false;
}
// Check if hex is valid
return /^[0-9A-Fa-f]+$/.test(hexPart);
}
/**
* Sign a transaction
* @param {string} privateKeyHex - The private key in hex format
* @param {Object} transaction - Transaction data
* @returns {string} Signature in hex format
*/
signTransaction(privateKeyHex, transaction) {
const privateKey = Buffer.from(privateKeyHex, 'hex');
// Create transaction message to sign
const txMessage = this.createTransactionMessage(transaction);
const messageBuffer = Buffer.from(txMessage, 'utf8');
// Sign the message
const signature = nacl.sign.detached(messageBuffer, privateKey);
return Buffer.from(signature).toString('hex');
}
/**
* Create transaction message for signing
* @param {Object} tx - Transaction object
* @returns {string} Message to sign
*/
createTransactionMessage(tx) {
// Format: sender:receiver:amount:nonce:type:data
const parts = [
tx.sender || '',
tx.receiver || '',
tx.amount || '0',
tx.nonce || '0',
tx.type || 'transfer',
tx.data || ''
];
return parts.join(':');
}
/**
* Create a complete transaction object
* @param {Object} params - Transaction parameters
* @returns {Object} Complete transaction object
*/
createTransaction(params) {
const {
sender,
receiver,
amount,
nonce,
type = 'transfer',
data = '',
privateKey
} = params;
const transaction = {
sender,
receiver,
amount: parseInt(amount),
nonce: parseInt(nonce),
type,
data,
timestamp: Date.now()
};
// Sign the transaction
if (privateKey) {
transaction.signature = this.signTransaction(privateKey, transaction);
}
return transaction;
}
/**
* Verify a transaction signature
* @param {Object} transaction - The transaction object
* @param {string} publicKeyHex - The public key to verify against
* @returns {boolean} True if signature is valid
*/
verifyTransaction(transaction, publicKeyHex) {
if (!transaction.signature) {
return false;
}
try {
const publicKey = Buffer.from(publicKeyHex, 'hex');
const signature = Buffer.from(transaction.signature, 'hex');
// Recreate the message that was signed
const txMessage = this.createTransactionMessage(transaction);
const messageBuffer = Buffer.from(txMessage, 'utf8');
return nacl.sign.detached.verify(messageBuffer, signature, publicKey);
} catch (error) {
console.error('Error verifying transaction:', error);
return false;
}
}
/**
* Convert transaction to hex string for broadcasting
* @param {Object} transaction - The transaction object
* @returns {string} Transaction as hex string
*/
transactionToHex(transaction) {
const txString = JSON.stringify(transaction);
return Buffer.from(txString, 'utf8').toString('hex');
}
/**
* Convert hex string back to transaction object
* @param {string} txHex - Transaction in hex format
* @returns {Object} Transaction object
*/
hexToTransaction(txHex) {
const txString = Buffer.from(txHex, 'hex').toString('utf8');
return JSON.parse(txString);
}
/**
* Import private key and return key pair info
* @param {string} privateKeyHex - Private key in hex format
* @returns {Object} Key pair information
*/
importPrivateKey(privateKeyHex) {
try {
const privateKey = Buffer.from(privateKeyHex, 'hex');
// Generate public key from private key
const keyPair = nacl.sign.keyPair.fromSecretKey(privateKey);
return {
publicKey: Buffer.from(keyPair.publicKey).toString('hex'),
privateKey: privateKeyHex,
address: this.publicKeyToAddress(keyPair.publicKey)
};
} catch (error) {
throw new Error('Invalid private key format');
}
}
/**
* Generate a random seed phrase (12 words)
* @returns {string} Seed phrase
*/
generateSeedPhrase() {
// Simple word list for demo purposes
const wordList = [
'abandon', 'ability', 'able', 'about', 'above', 'absent', 'absorb', 'abstract',
'absurd', 'abuse', 'access', 'accident', 'account', 'accuse', 'achieve', 'acid',
'acoustic', 'acquire', 'across', 'act', 'action', 'actor', 'actress', 'actual',
'adapt', 'add', 'addict', 'address', 'adjust', 'admit', 'adult', 'advance',
'advice', 'aerobic', 'affair', 'afford', 'afraid', 'again', 'against', 'agent',
'agree', 'ahead', 'aim', 'air', 'airport', 'aisle', 'alarm', 'album',
'alcohol', 'alert', 'alien', 'all', 'alley', 'allow', 'almost', 'alone',
'alpha', 'already', 'also', 'alter', 'always', 'amateur', 'amazing', 'among',
'amount', 'amused', 'analyst', 'anchor', 'ancient', 'anger', 'angle', 'angry',
'animal', 'ankle', 'announce', 'annual', 'another', 'answer', 'antenna', 'antique'
];
const words = [];
for (let i = 0; i < 12; i++) {
const randomIndex = crypto.randomInt(0, wordList.length);
words.push(wordList[randomIndex]);
}
return words.join(' ');
}
}
module.exports = new ClipsCrypto();

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -3,284 +3,301 @@ const {
ipcRenderer ipcRenderer
} = require("electron"); } = require("electron");
class Blockchain { class PaperclipBlockchain {
constructor() { constructor() {
this.txSubscribe = null; this.isConnected = false;
this.bhSubscribe = null; this.gasPrice = 1; // 1 CLIP per gas unit
this.gasLimits = {
'transfer': 21000,
'contract_call': 50000,
'contract_deploy': 100000,
'multisig_create': 75000,
'multisig': 35000
};
} }
getBlock(blockToGet, includeData, clbError, clbSuccess) { async getStatus() {
web3Local.eth.getBlock(blockToGet, includeData, function(error, block) { try {
if (error) { const result = await ipcRenderer.invoke("paperclip-get-status");
clbError(error); this.isConnected = true;
} else { return result;
} catch (error) {
this.isConnected = false;
throw error;
}
}
async getBlock(blockHeight, clbError, clbSuccess) {
try {
const block = await ipcRenderer.invoke("paperclip-get-block", blockHeight);
clbSuccess(block); clbSuccess(block);
} catch (error) {
clbError(error);
} }
});
} }
getAccounts(clbError, clbSuccess) { async getBalance(address, clbError, clbSuccess) {
web3Local.eth.getAccounts(function(err, res) { try {
if (err) { const balance = await ipcRenderer.invoke("paperclip-get-balance", address);
clbError(err); clbSuccess(balance);
} else { } catch (error) {
clbSuccess(res); clbError(error);
}
}
async getNonce(address, clbError, clbSuccess) {
try {
const nonce = await ipcRenderer.invoke("paperclip-get-nonce", address);
clbSuccess(nonce);
} catch (error) {
clbError(error);
} }
});
} }
isAddress(address) { isAddress(address) {
return web3Local.utils.isAddress(address); // CLIPS address validation: CLIP- followed by 64 hex characters
if (!address || typeof address !== 'string') {
return false;
}
return /^CLIP-[0-9A-Fa-f]{64}$/.test(address);
} }
getTransaction(thxid, clbError, clbSuccess) { async getTransaction(txHash, clbError, clbSuccess) {
web3Local.eth.getTransaction(thxid, function(error, result) { try {
if (error) { const transaction = await ipcRenderer.invoke("paperclip-get-transaction", txHash);
clbSuccess(transaction);
} catch (error) {
clbError(error); clbError(error);
} else {
clbSuccess(result);
} }
}
async getTransactionFee(fromAddress, toAddress, amount, txType = 'transfer', clbError, clbSuccess) {
try {
// Calculate gas needed based on transaction type
const gasNeeded = this.gasLimits[txType] || this.gasLimits['transfer'];
const fee = gasNeeded * this.gasPrice;
clbSuccess({
gasNeeded: gasNeeded,
gasPrice: this.gasPrice,
totalFee: fee
}); });
} catch (error) {
clbError(error);
}
} }
getTranasctionFee(fromAddress, toAddress, value, clbError, clbSuccess) { async prepareTransaction(privateKey, fromAddress, toAddress, amount, txType = 'transfer', data = '', clbError, clbSuccess) {
web3Local.eth.getTransactionCount(fromAddress, function(error, result) { try {
if (error) { // Get current nonce
const nonce = await ipcRenderer.invoke("paperclip-get-nonce", fromAddress);
// Calculate gas
const gasNeeded = this.gasLimits[txType] || this.gasLimits['transfer'];
// Create transaction object
const ClipsCrypto = require('../modules/clips-crypto.js');
const transaction = ClipsCrypto.createTransaction({
sender: fromAddress,
receiver: toAddress,
amount: parseInt(amount),
nonce: nonce,
type: txType,
data: data,
privateKey: privateKey
});
// Add gas information
transaction.gas = gasNeeded;
transaction.gasPrice = this.gasPrice;
transaction.fee = gasNeeded * this.gasPrice;
clbSuccess(transaction);
} catch (error) {
clbError(error); clbError(error);
}
}
async sendTransaction(transaction, clbError, clbSuccess) {
try {
// Convert transaction to hex for broadcasting
const ClipsCrypto = require('../modules/clips-crypto.js');
const txHex = ClipsCrypto.transactionToHex(transaction);
// Broadcast transaction
const result = await ipcRenderer.invoke("paperclip-broadcast-tx", txHex);
if (result.success) {
clbSuccess({
hash: result.hash,
height: result.height
});
} else { } else {
var amountToSend = web3Local.utils.toWei(value, "ether"); //convert to wei value clbError(result.error || "Transaction failed");
var RawTransaction = { }
from: fromAddress, } catch (error) {
to: toAddress, clbError(error);
value: amountToSend, }
nonce: result }
async getAccountsData(clbError, clbSuccess) {
try {
const rendererData = {
sumBalance: 0,
addressData: []
}; };
web3Local.eth.estimateGas(RawTransaction, function(error, result) { // Get stored wallets from database
if (error) { const wallets = PaperclipDatabase.getWallets();
clbError(error);
} else { if (!wallets || !wallets.addresses || wallets.addresses.length === 0) {
var usedGas = result + 1; clbSuccess(rendererData);
web3Local.eth.getGasPrice(function(error, result) { return;
if (error) {
clbError(error);
} else {
clbSuccess(result * usedGas);
} }
// Get balance for each address
let processedCount = 0;
const totalAddresses = wallets.addresses.length;
for (let i = 0; i < wallets.addresses.length; i++) {
const address = wallets.addresses[i];
const walletName = wallets.names[address] || `Account ${i + 1}`;
try {
const balance = await ipcRenderer.invoke("paperclip-get-balance", address);
rendererData.addressData.push({
address: address,
name: walletName,
balance: balance,
balanceFormatted: this.formatBalance(balance)
}); });
}
}); rendererData.sumBalance += balance;
}
} catch (error) {
console.error(`Failed to get balance for ${address}:`, error);
rendererData.addressData.push({
address: address,
name: walletName,
balance: 0,
balanceFormatted: "0 CLIPS",
error: true
}); });
} }
prepareTransaction(password, fromAddress, toAddress, value, clbError, clbSuccess) { processedCount++;
web3Local.eth.personal.unlockAccount(fromAddress, password, function(error, result) {
if (error) { // If all addresses processed, return data
clbError("Wrong password for the selected address!"); if (processedCount === totalAddresses) {
} else { rendererData.sumBalanceFormatted = this.formatBalance(rendererData.sumBalance);
web3Local.eth.getTransactionCount(fromAddress, "pending", function(error, result) { clbSuccess(rendererData);
if (error) { }
}
} catch (error) {
clbError(error); clbError(error);
}
}
formatBalance(balance) {
if (balance === 0) {
return "0 CLIPS";
}
// Format balance with appropriate decimal places
if (balance >= 1000000) {
return (balance / 1000000).toFixed(2) + "M CLIPS";
} else if (balance >= 1000) {
return (balance / 1000).toFixed(2) + "K CLIPS";
} else { } else {
var amountToSend = web3Local.utils.toWei(value, "ether"); //convert to wei value return balance.toLocaleString() + " CLIPS";
var RawTransaction = { }
from: fromAddress, }
to: toAddress,
value: amountToSend, async getNetworkInfo(clbError, clbSuccess) {
nonce: result try {
const status = await this.getStatus();
const networkInfo = {
nodeInfo: status.result.node_info,
syncInfo: status.result.sync_info,
validatorInfo: status.result.validator_info,
chainId: status.result.node_info.network,
latestBlockHeight: status.result.sync_info.latest_block_height,
catching_up: status.result.sync_info.catching_up
}; };
web3Local.eth.estimateGas(RawTransaction, function(error, result) { clbSuccess(networkInfo);
if (error) { } catch (error) {
clbError(error); clbError(error);
} else { }
RawTransaction.gas = result + 1; }
web3Local.eth.getGasPrice(function(error, result) {
if (error) { async getContractInfo(contractAddress, clbError, clbSuccess) {
try {
const contractInfo = await ipcRenderer.invoke("paperclip-get-contract", contractAddress);
clbSuccess(contractInfo);
} catch (error) {
clbError(error); clbError(error);
} else { }
RawTransaction.gasPrice = result; }
web3Local.eth.signTransaction(RawTransaction, fromAddress, function(error, result) {
if (error) { async getMultisigInfo(multisigAddress, clbError, clbSuccess) {
try {
const multisigInfo = await ipcRenderer.invoke("paperclip-get-multisig", multisigAddress);
clbSuccess(multisigInfo);
} catch (error) {
clbError(error); clbError(error);
} else {
clbSuccess(result);
} }
});
}
});
}
});
}
});
}
});
} }
sendTransaction(rawTransaction, clbError, clbSuccess) { async getFeePool(clbError, clbSuccess) {
web3Local.eth.sendSignedTransaction(rawTransaction, function(error, result) { try {
if (error) { const feePool = await ipcRenderer.invoke("paperclip-get-fee-pool");
clbSuccess({
totalFees: feePool,
formatted: this.formatBalance(feePool)
});
} catch (error) {
clbError(error); clbError(error);
} else {
clbSuccess(result);
}
});
}
getAccountsData(clbError, clbSuccess) {
var rendererData = {};
rendererData.sumBalance = 0;
rendererData.addressData = [];
var wallets = EthoDatatabse.getWallets();
var counter = 0;
web3Local.eth.getAccounts(function(err, res) {
if (err) {
clbError(err);
} else {
for (var i = 0; i < res.length; i++) {
var walletName = vsprintf("Account %d", [i + 1]);
if (wallets) {
walletName = wallets.names[res[i]] || walletName;
}
var addressInfo = {};
addressInfo.balance = 0;
addressInfo.address = res[i];
addressInfo.name = walletName;
rendererData.addressData.push(addressInfo);
}
if (rendererData.addressData.length > 0) {
updateBalance(counter);
} else {
clbSuccess(rendererData);
}
}
});
function updateBalance(index) {
web3Local.eth.getBalance(rendererData.addressData[index].address, function(error, balance) {
rendererData.addressData[index].balance = parseFloat(web3Local.utils.fromWei(balance, "ether")).toFixed(2);
rendererData.sumBalance = rendererData.sumBalance + parseFloat(web3Local.utils.fromWei(balance, "ether"));
if (counter < rendererData.addressData.length - 1) {
counter++;
updateBalance(counter);
} else {
rendererData.sumBalance = parseFloat(rendererData.sumBalance).toFixed(2);
clbSuccess(rendererData);
}
});
} }
} }
getAddressListData(clbError, clbSuccess) { // Utility method to validate transaction before sending
var rendererData = {}; validateTransaction(transaction) {
rendererData.addressData = []; const errors = [];
var wallets = EthoDatatabse.getWallets(); if (!this.isAddress(transaction.sender)) {
var counter = 0; errors.push("Invalid sender address");
web3Local.eth.getAccounts(function(err, res) {
if (err) {
clbError(err);
} else {
for (var i = 0; i < res.length; i++) {
var walletName = vsprintf("Account %d", [i + 1]);
if (wallets) {
walletName = wallets.names[res[i]] || walletName;
} }
var addressInfo = {}; if (!this.isAddress(transaction.receiver)) {
addressInfo.address = res[i]; errors.push("Invalid receiver address");
addressInfo.name = walletName;
rendererData.addressData.push(addressInfo);
} }
clbSuccess(rendererData); if (!transaction.amount || transaction.amount <= 0) {
} errors.push("Invalid amount");
});
} }
createNewAccount(password, clbError, clbSuccess) { if (transaction.nonce < 0) {
web3Local.eth.personal.newAccount(password, function(error, account) { errors.push("Invalid nonce");
if (error) {
clbError(error);
} else {
clbSuccess(account);
}
});
} }
importFromPrivateKey(privateKey, keyPassword, clbError, clbSuccess) { if (!transaction.signature) {
web3Local.eth.personal.importRawKey(privateKey, keyPassword, function(error, account) { errors.push("Transaction not signed");
if (error) {
clbError(error);
} else {
clbSuccess(account);
}
});
} }
subsribePendingTransactions(clbError, clbSuccess, clbData) { return {
this.txSubscribe = web3Local.eth.subscribe("pendingTransactions", function(error, result) { isValid: errors.length === 0,
if (error) { errors: errors
clbError(error); };
} else {
clbSuccess(result);
}
}).on("data", function(transaction) {
if (clbData) {
clbData(transaction);
}
});
}
unsubsribePendingTransactions(clbError, clbSuccess) {
if (this.txSubscribe) {
this.txSubscribe.unsubscribe(function(error, success) {
if (error) {
clbError(error);
} else {
clbSuccess(success);
}
});
} }
} }
subsribeNewBlockHeaders(clbError, clbSuccess, clbData) { // Create global instance
this.bhSubscribe = web3Local.eth.subscribe("newBlockHeaders", function(error, result) { const PaperclipChain = new PaperclipBlockchain();
if (error) {
clbError(error);
} else {
clbSuccess(result);
}
}).on("data", function(blockHeader) {
if (clbData) {
clbData(blockHeader);
}
});
}
unsubsribeNewBlockHeaders(clbError, clbSuccess) { // Make it available globally (maintaining compatibility with existing code)
if (this.bhSubscribe) { window.PaperclipChain = PaperclipChain;
this.bhSubscribe.unsubscribe(function(error, success) {
if (error) {
clbError(error);
} else {
clbSuccess(success);
}
});
}
}
closeConnection() {
web3Local.currentProvider.connection.close();
}
}
// create new blockchain variable
EthoBlockchain = new Blockchain();

View File

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

View File

@@ -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)"
} }
} }
], ],

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

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

View File

@@ -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,10 +110,28 @@ $(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"));
$("#dlgAddressList").iziModal("open"); $("#dlgAddressList").iziModal("open");

520
renderer/staking.js Normal file
View 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;
}

View File

@@ -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: {

View File

@@ -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()
])); ]));
@@ -232,14 +229,22 @@ $(document).on("render_wallets", function () {
}); });
$("#btnImportAccounts").off("click").on("click", function () { $("#btnImportAccounts").off("click").on("click", function () {
var ImportResult = ipcRenderer.sendSync("importAccounts", {}); ipcRenderer.send("importAccounts");
if (ImportResult.success) { ipcRenderer.once("importAccountsReply", (event, importResult) => {
iziToast.success({title: "Imported", message: ImportResult.text, position: "topRight", timeout: 2000}); if (importResult.success) {
} else if (ImportResult.success == false) { iziToast.success({ title: "Imported", message: importResult.text, position: "topRight", timeout: 2000 });
EthoMainGUI.showGeneralError(ImportResult.text); // Reload wallets after successful import
setTimeout(() => {
EthoWallets.renderWalletsState();
}, 500);
} else {
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
View 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.');

4074
yarn.lock

File diff suppressed because it is too large Load Diff