diff --git a/WpfApp1TRC20.sln b/WpfApp1TRC20.sln new file mode 100644 index 0000000..a3d3a8b --- /dev/null +++ b/WpfApp1TRC20.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33815.320 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WpfApp1TRC20", "WpfApp1TRC20\WpfApp1TRC20.csproj", "{B5C8DC98-1B86-4718-B4D2-CE2EF908249E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B5C8DC98-1B86-4718-B4D2-CE2EF908249E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B5C8DC98-1B86-4718-B4D2-CE2EF908249E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B5C8DC98-1B86-4718-B4D2-CE2EF908249E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B5C8DC98-1B86-4718-B4D2-CE2EF908249E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {CE428E59-6FEF-42AE-BF99-6493814BA044} + EndGlobalSection +EndGlobal diff --git a/WpfApp1TRC20/App.xaml b/WpfApp1TRC20/App.xaml new file mode 100644 index 0000000..f012783 --- /dev/null +++ b/WpfApp1TRC20/App.xaml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/WpfApp1TRC20/App.xaml.cs b/WpfApp1TRC20/App.xaml.cs new file mode 100644 index 0000000..a773f64 --- /dev/null +++ b/WpfApp1TRC20/App.xaml.cs @@ -0,0 +1,71 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Windows; +using Serilog; +using System; +using WpfApp1TRC20; +using WpfApp1TRC20.Views; +using WpfApp1TRC20.Data; +using WpfApp1TRC20.Services; +using WpfApp1TRC20.ViewModels; + +namespace WpfApp1TRC20 +{ + public partial class App : Application + { + private IHost _host; + + protected override async void OnStartup(StartupEventArgs e) + { + // Configure Serilog + Log.Logger = new LoggerConfiguration() + .WriteTo.File("logs/app-.txt", rollingInterval: RollingInterval.Day) + .CreateLogger(); + + _host = Host.CreateDefaultBuilder() + .ConfigureServices((context, services) => + { + // Database + services.AddDbContext(); + + // Services + services.AddSingleton(); + services.AddScoped(); + + // ViewModels + services.AddTransient(); + + // Views + services.AddTransient(); + + // Logging + services.AddLogging(a => + a.AddProvider( )); + }) + .Build(); + + await _host.StartAsync(); + + // Ensure database is created + using (var scope = _host.Services.CreateScope()) + { + var context = scope.ServiceProvider.GetRequiredService(); + await context.Database.EnsureCreatedAsync(); + } + + var mainWindow = _host.Services.GetRequiredService(); + mainWindow.Show(); + + base.OnStartup(e); + } + + protected override async void OnExit(ExitEventArgs e) + { + await _host?.StopAsync(); + _host?.Dispose(); + Log.CloseAndFlush(); + base.OnExit(e); + } + } +} \ No newline at end of file diff --git a/WpfApp1TRC20/AssemblyInfo.cs b/WpfApp1TRC20/AssemblyInfo.cs new file mode 100644 index 0000000..8b5504e --- /dev/null +++ b/WpfApp1TRC20/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/WpfApp1TRC20/Contract/Ganatch.cs b/WpfApp1TRC20/Contract/Ganatch.cs new file mode 100644 index 0000000..518de05 --- /dev/null +++ b/WpfApp1TRC20/Contract/Ganatch.cs @@ -0,0 +1,116 @@ +namespace TRC20TokenManager.Contracts +{ + public static class TimedTRC20Contract + { + public const string Bytecode = @" +pragma solidity ^0.8.0; + +interface IERC20 { + function totalSupply() external view returns (uint256); + function balanceOf(address account) external view returns (uint256); + function transfer(address recipient, uint256 amount) external returns (bool); + function allowance(address owner, address spender) external view returns (uint256); + function approve(address spender, uint256 amount) external returns (bool); + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); +} + +contract TimedTRC20 is IERC20 { + mapping(address => uint256) private _balances; + mapping(address => mapping(address => uint256)) private _allowances; + + uint256 private _totalSupply; + string private _name; + string private _symbol; + uint8 private _decimals; + uint256 public immutable expiryTimestamp; + address private _owner; + + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); + + constructor( + string memory name_, + string memory symbol_, + uint8 decimals_, + uint256 totalSupply_, + uint256 expiryDays + ) { + _name = name_; + _symbol = symbol_; + _decimals = decimals_; + _totalSupply = totalSupply_ * 10**decimals_; + _owner = msg.sender; + _balances[msg.sender] = _totalSupply; + expiryTimestamp = block.timestamp + (expiryDays * 1 days); + } + + modifier notExpired() { + require(block.timestamp < expiryTimestamp, ""Token has expired""); + _; + } + + function name() public view returns (string memory) { return _name; } + function symbol() public view returns (string memory) { return _symbol; } + function decimals() public view returns (uint8) { return _decimals; } + function totalSupply() public view override returns (uint256) { return _totalSupply; } + + function balanceOf(address account) public view override returns (uint256) { + return _balances[account]; + } + + function transfer(address recipient, uint256 amount) public override notExpired returns (bool) { + _transfer(msg.sender, recipient, amount); + return true; + } + + function allowance(address owner, address spender) public view override returns (uint256) { + return _allowances[owner][spender]; + } + + function approve(address spender, uint256 amount) public override notExpired returns (bool) { + _approve(msg.sender, spender, amount); + return true; + } + + function transferFrom(address sender, address recipient, uint256 amount) public override notExpired returns (bool) { + uint256 currentAllowance = _allowances[sender][msg.sender]; + require(currentAllowance >= amount, ""ERC20: transfer amount exceeds allowance""); + + _transfer(sender, recipient, amount); + _approve(sender, msg.sender, currentAllowance - amount); + + return true; + } + + function _transfer(address sender, address recipient, uint256 amount) internal { + require(sender != address(0), ""ERC20: transfer from the zero address""); + require(recipient != address(0), ""ERC20: transfer to the zero address""); + + uint256 senderBalance = _balances[sender]; + require(senderBalance >= amount, ""ERC20: transfer amount exceeds balance""); + + _balances[sender] = senderBalance - amount; + _balances[recipient] += amount; + + emit Transfer(sender, recipient, amount); + } + + function _approve(address owner, address spender, uint256 amount) internal { + require(owner != address(0), ""ERC20: approve from the zero address""); + require(spender != address(0), ""ERC20: approve to the zero address""); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + function isExpired() public view returns (bool) { + return block.timestamp >= expiryTimestamp; + } + + function timeRemaining() public view returns (uint256) { + if (block.timestamp >= expiryTimestamp) return 0; + return expiryTimestamp - block.timestamp; + } +}"; + } +} \ No newline at end of file diff --git a/WpfApp1TRC20/Data/AppDbContext.cs b/WpfApp1TRC20/Data/AppDbContext.cs new file mode 100644 index 0000000..3fa41aa --- /dev/null +++ b/WpfApp1TRC20/Data/AppDbContext.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace WpfApp1TRC20.Data +{ + public class AppDbContext : DbContext + { + public DbSet Tokens { get; set; } + public DbSet Wallets { get; set; } + public DbSet Transactions { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + var dbPath = Path.Combine(appDataPath, "TRC20TokenManager", "app.db"); + Directory.CreateDirectory(Path.GetDirectoryName(dbPath)); + optionsBuilder.UseSqlite($"Data Source={dbPath}"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .Property(e => e.TotalSupply) + .HasConversion(); + + modelBuilder.Entity() + .Property(e => e.Amount) + .HasConversion(); + + modelBuilder.Entity() + .Property(e => e.TrxBalance) + .HasConversion(); + } + } +} diff --git a/WpfApp1TRC20/Data/TokenBalance.cs b/WpfApp1TRC20/Data/TokenBalance.cs new file mode 100644 index 0000000..c57f043 --- /dev/null +++ b/WpfApp1TRC20/Data/TokenBalance.cs @@ -0,0 +1,11 @@ +namespace WpfApp1TRC20.Data +{ + public class TokenBalance + { + public string TokenSymbol { get; set; } + public string ContractAddress { get; set; } + public decimal Balance { get; set; } + public int Decimals { get; set; } + public string WalletAddress { get; set; } + } +} diff --git a/WpfApp1TRC20/Data/TokenCreationParams.cs b/WpfApp1TRC20/Data/TokenCreationParams.cs new file mode 100644 index 0000000..3c0bca4 --- /dev/null +++ b/WpfApp1TRC20/Data/TokenCreationParams.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace WpfApp1TRC20.Data +{ + + public class TokenCreationParams + { + [Required] + public string Name { get; set; } + [Required] + public string Symbol { get; set; } + [Range(0, 18)] + public int Decimals { get; set; } = 18; + [Range(1, double.MaxValue)] + public decimal TotalSupply { get; set; } + [Range(10, 90)] + public int ExpiryDays { get; set; } = 30; + public string OwnerAddress { get; set; } + } +} diff --git a/WpfApp1TRC20/Data/TokenInfo.cs b/WpfApp1TRC20/Data/TokenInfo.cs new file mode 100644 index 0000000..aa960eb --- /dev/null +++ b/WpfApp1TRC20/Data/TokenInfo.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace WpfApp1TRC20.Data +{ + public class TokenInfo + { + public int Id { get; set; } + [Required] + public string Name { get; set; } + [Required] + public string Symbol { get; set; } + public int Decimals { get; set; } + public decimal TotalSupply { get; set; } + public string ContractAddress { get; set; } + public string OwnerAddress { get; set; } + public DateTime CreationDate { get; set; } + public DateTime ExpiryDate { get; set; } + public int ExpiryDays { get; set; } + public bool IsExpired => DateTime.UtcNow > ExpiryDate; + public string TransactionHash { get; set; } + public TokenStatus Status { get; set; } + } +} diff --git a/WpfApp1TRC20/Data/TokenStatus.cs b/WpfApp1TRC20/Data/TokenStatus.cs new file mode 100644 index 0000000..297ae41 --- /dev/null +++ b/WpfApp1TRC20/Data/TokenStatus.cs @@ -0,0 +1,10 @@ +namespace WpfApp1TRC20.Data +{ + public enum TokenStatus + { + Creating, + Active, + Expired, + Failed + } +} diff --git a/WpfApp1TRC20/Data/TransactionRecord.cs b/WpfApp1TRC20/Data/TransactionRecord.cs new file mode 100644 index 0000000..b243faf --- /dev/null +++ b/WpfApp1TRC20/Data/TransactionRecord.cs @@ -0,0 +1,19 @@ +using System; + +namespace WpfApp1TRC20.Data +{ + public class TransactionRecord + { + public int Id { get; set; } + public string TransactionHash { get; set; } + public string FromAddress { get; set; } + public string ToAddress { get; set; } + public decimal Amount { get; set; } + public string TokenSymbol { get; set; } + public string ContractAddress { get; set; } + public DateTime Timestamp { get; set; } + public TransactionStatus Status { get; set; } + public string ErrorMessage { get; set; } + public decimal GasFee { get; set; } + } +} diff --git a/WpfApp1TRC20/Data/TransactionStatus.cs b/WpfApp1TRC20/Data/TransactionStatus.cs new file mode 100644 index 0000000..705baa0 --- /dev/null +++ b/WpfApp1TRC20/Data/TransactionStatus.cs @@ -0,0 +1,9 @@ +namespace WpfApp1TRC20.Data +{ + public enum TransactionStatus + { + Pending, + Confirmed, + Failed + } +} diff --git a/WpfApp1TRC20/Data/WalletAccount.cs b/WpfApp1TRC20/Data/WalletAccount.cs new file mode 100644 index 0000000..ef05922 --- /dev/null +++ b/WpfApp1TRC20/Data/WalletAccount.cs @@ -0,0 +1,18 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace WpfApp1TRC20.Data +{ + public class WalletAccount + { + public int Id { get; set; } + [Required] + public string Name { get; set; } + [Required] + public string Address { get; set; } + public string EncryptedPrivateKey { get; set; } + public DateTime CreatedDate { get; set; } + public decimal TrxBalance { get; set; } + public bool IsImported { get; set; } + } +} diff --git a/WpfApp1TRC20/Services/DataService.cs b/WpfApp1TRC20/Services/DataService.cs new file mode 100644 index 0000000..fb0f825 --- /dev/null +++ b/WpfApp1TRC20/Services/DataService.cs @@ -0,0 +1,147 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using WpfApp1TRC20.Data; + +namespace WpfApp1TRC20.Services +{ + public class DataService : IDataService + { + private readonly AppDbContext _context; + private readonly ITronService _tronService; + + public DataService(AppDbContext context, ITronService tronService) + { + _context = context; + _tronService = tronService; + } + + public async Task> GetTokensAsync() + { + return await _context.Tokens.ToListAsync(); + } + + public async Task> GetWalletsAsync() + { + var wallets = await _context.Wallets.ToListAsync(); + + // Update TRX balances + foreach (var wallet in wallets) + { + try + { + wallet.TrxBalance = await _tronService.GetTrxBalanceAsync(wallet.Address); + } + catch + { + // Continue if balance check fails + } + } + + return wallets; + } + + public async Task> GetTransactionsAsync() + { + return await _context.Transactions + .OrderByDescending(t => t.Timestamp) + .ToListAsync(); + } + + public async Task SaveTokenAsync(TokenInfo token) + { + if (token.Id == 0) + { + _context.Tokens.Add(token); + } + else + { + _context.Tokens.Update(token); + } + + await _context.SaveChangesAsync(); + return token; + } + + public async Task SaveWalletAsync(WalletAccount wallet) + { + if (wallet.Id == 0) + { + _context.Wallets.Add(wallet); + } + else + { + _context.Wallets.Update(wallet); + } + + await _context.SaveChangesAsync(); + return wallet; + } + + public async Task SaveTransactionAsync(TransactionRecord transaction) + { + if (transaction.Id == 0) + { + _context.Transactions.Add(transaction); + } + else + { + _context.Transactions.Update(transaction); + } + + await _context.SaveChangesAsync(); + return transaction; + } + + public async Task DeleteTokenAsync(int tokenId) + { + var token = await _context.Tokens.FindAsync(tokenId); + if (token != null) + { + _context.Tokens.Remove(token); + await _context.SaveChangesAsync(); + } + } + + public async Task DeleteWalletAsync(int walletId) + { + var wallet = await _context.Wallets.FindAsync(walletId); + if (wallet != null) + { + _context.Wallets.Remove(wallet); + await _context.SaveChangesAsync(); + } + } + + public async Task> GetTokenBalancesAsync(string walletAddress) + { + var tokens = await _context.Tokens.Where(t => t.Status == TokenStatus.Active).ToListAsync(); + var balances = new List(); + + foreach (var token in tokens) + { + try + { + var balance = await _tronService.GetTokenBalanceAsync(token.ContractAddress, walletAddress); + balances.Add(new TokenBalance + { + TokenSymbol = token.Symbol, + ContractAddress = token.ContractAddress, + Balance = balance, + Decimals = token.Decimals, + WalletAddress = walletAddress + }); + } + catch + { + // Continue if balance check fails + } + } + + return balances; + } + } +} diff --git a/WpfApp1TRC20/Services/IDataService.cs b/WpfApp1TRC20/Services/IDataService.cs new file mode 100644 index 0000000..a995768 --- /dev/null +++ b/WpfApp1TRC20/Services/IDataService.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using WpfApp1TRC20.Data; + +namespace WpfApp1TRC20.Services +{ + public interface IDataService + { + Task> GetTokensAsync(); + Task> GetWalletsAsync(); + Task> GetTransactionsAsync(); + Task SaveTokenAsync(TokenInfo token); + Task SaveWalletAsync(WalletAccount wallet); + Task SaveTransactionAsync(TransactionRecord transaction); + Task DeleteTokenAsync(int tokenId); + Task DeleteWalletAsync(int walletId); + Task> GetTokenBalancesAsync(string walletAddress); + } +} diff --git a/WpfApp1TRC20/Services/ITronService.cs b/WpfApp1TRC20/Services/ITronService.cs new file mode 100644 index 0000000..4ff297d --- /dev/null +++ b/WpfApp1TRC20/Services/ITronService.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using WpfApp1TRC20.Data; + +namespace WpfApp1TRC20.Services +{ + public interface ITronService + { + Task CreateTokenAsync(TokenCreationParams parameters, string ownerPrivateKey); + Task TransferTokenAsync(string contractAddress, string fromPrivateKey, string toAddress, decimal amount); + Task GetTokenBalanceAsync(string contractAddress, string address); + Task GetTrxBalanceAsync(string address); + Task GetTransactionAsync(string txHash); + Task IsTokenExpiredAsync(string contractAddress); + WalletAccount CreateWallet(string name); + WalletAccount ImportWallet(string name, string privateKey); + string DecryptPrivateKey(string encryptedPrivateKey); + } +} diff --git a/WpfApp1TRC20/Services/TronService.cs b/WpfApp1TRC20/Services/TronService.cs new file mode 100644 index 0000000..5c199ee --- /dev/null +++ b/WpfApp1TRC20/Services/TronService.cs @@ -0,0 +1,255 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Net.Http; +using System.Numerics; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using System.Transactions; +using TronNet; +using TronNet.Accounts; +using WpfApp1TRC20.Data; +using TransactionStatus = WpfApp1TRC20.Data.TransactionStatus; + +namespace WpfApp1TRC20.Services +{ + public class TronService : ITronService + { + private readonly ITronClient _tronClient; + private readonly ILogger _logger; + private const string TRON_MAINNET = "https://api.trongrid.io"; + private const string TRON_TESTNET = "https://api.shasta.trongrid.io"; + + public TronService(ILogger logger) + { + _logger = logger; + // Use testnet for development, mainnet for production + var httpClient = new HttpClient(); + _tronClient = new TronClient(httpClient, TRON_TESTNET); + } + + public async Task CreateTokenAsync(TokenCreationParams parameters, string ownerPrivateKey) + { + try + { + var account = new TronAccount(ownerPrivateKey); + + // Compile and deploy the smart contract + var contractData = CompileContract(parameters); + + var transaction = await _tronClient.CreateTransactionAsync( + account.Address, + contractData, + 0, + "Smart Contract Creation" + ); + + var signedTransaction = transaction.Sign(account); + var result = await _tronClient.BroadcastTransactionAsync(signedTransaction); + + _logger.LogInformation($"Token creation transaction broadcast: {result.Txid}"); + return result.Txid; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating token"); + throw; + } + } + + public async Task TransferTokenAsync(string contractAddress, string fromPrivateKey, string toAddress, decimal amount) + { + try + { + var fromAccount = new TronAccount(fromPrivateKey); + + // Check if token is expired first + if (await IsTokenExpiredAsync(contractAddress)) + { + throw new InvalidOperationException("Token has expired and cannot be transferred"); + } + + var contract = _tronClient.GetContract(contractAddress); + var transferFunction = contract.GetFunction("transfer"); + + var transaction = await transferFunction.CreateTransactionAsync( + fromAccount.Address, + toAddress, + (BigInteger)(amount * (decimal)Math.Pow(10, 18)) // Assuming 18 decimals + ); + + var signedTransaction = transaction.Sign(fromAccount); + var result = await _tronClient.BroadcastTransactionAsync(signedTransaction); + + _logger.LogInformation($"Token transfer transaction broadcast: {result.Txid}"); + return result.Txid; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error transferring tokens"); + throw; + } + } + + public async Task GetTokenBalanceAsync(string contractAddress, string address) + { + try + { + var contract = _tronClient.GetContract(contractAddress); + var balanceFunction = contract.GetFunction("balanceOf"); + + var balance = await balanceFunction.CallAsync(address); + return (decimal)balance / (decimal)Math.Pow(10, 18); // Assuming 18 decimals + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting token balance"); + return 0; + } + } + + public async Task GetTrxBalanceAsync(string address) + { + try + { + var account = await _tronClient.GetAccountAsync(address); + return (decimal)account.Balance / 1_000_000; // TRX has 6 decimals + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting TRX balance"); + return 0; + } + } + + public async Task GetTransactionAsync(string txHash) + { + try + { + var transaction = await _tronClient.GetTransactionByIdAsync(txHash); + var transactionInfo = await _tronClient.GetTransactionInfoByIdAsync(txHash); + + return new TransactionRecord + { + TransactionHash = txHash, + Status = transactionInfo.Receipt.Result == "SUCCESS" ? + TransactionStatus.Confirmed : TransactionStatus.Failed, + Timestamp = DateTimeOffset.FromUnixTimeMilliseconds(transaction.RawData.Timestamp).DateTime, + GasFee = (decimal)transactionInfo.Fee / 1_000_000 + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting transaction info"); + throw; + } + } + + public async Task IsTokenExpiredAsync(string contractAddress) + { + try + { + var contract = _tronClient.GetContract(contractAddress); + var isExpiredFunction = contract.GetFunction("isExpired"); + + var isExpired = await isExpiredFunction.CallAsync(); + return isExpired; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error checking token expiry"); + return false; + } + } + + public WalletAccount CreateWallet(string name) + { + var account = TronAccount.GenerateAccount(); + + return new WalletAccount + { + Name = name, + Address = account.Address, + EncryptedPrivateKey = EncryptPrivateKey(account.PrivateKey), + CreatedDate = DateTime.UtcNow, + IsImported = false + }; + } + + public WalletAccount ImportWallet(string name, string privateKey) + { + try + { + var account = new TronAccount(privateKey); + + return new WalletAccount + { + Name = name, + Address = account.Address, + EncryptedPrivateKey = EncryptPrivateKey(privateKey), + CreatedDate = DateTime.UtcNow, + IsImported = true + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error importing wallet"); + throw new ArgumentException("Invalid private key"); + } + } + + public string DecryptPrivateKey(string encryptedPrivateKey) + { + try + { + var encryptedBytes = Convert.FromBase64String(encryptedPrivateKey); + var decryptedBytes = ProtectedData.Unprotect( + encryptedBytes, + null, + DataProtectionScope.CurrentUser + ); + return Encoding.UTF8.GetString(decryptedBytes); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error decrypting private key"); + throw; + } + } + + private string EncryptPrivateKey(string privateKey) + { + var dataBytes = Encoding.UTF8.GetBytes(privateKey); + var encryptedBytes = ProtectedData.Protect( + dataBytes, + null, + DataProtectionScope.CurrentUser + ); + return Convert.ToBase64String(encryptedBytes); + } + + private string CompileContract(TokenCreationParams parameters) + { + // In a real implementation, you would use a Solidity compiler + // For now, we'll use pre-compiled bytecode with constructor parameters + // This is a simplified approach - in production, use tools like Nethereum.Contracts + + var constructorParams = EncodeConstructorParameters( + parameters.Name, + parameters.Symbol, + (byte)parameters.Decimals, + parameters.TotalSupply, + parameters.ExpiryDays + ); + + return TRC20TokenManager.Contracts.TimedTRC20Contract.Bytecode + constructorParams; + } + + private string EncodeConstructorParameters(string name, string symbol, byte decimals, decimal totalSupply, int expiryDays) + { + // Simplified ABI encoding - in production use proper ABI encoding library + // This would need to be implemented properly using ABI encoding standards + return ""; + } + } +} diff --git a/WpfApp1TRC20/Utils/InverseBooleanConverter.cs b/WpfApp1TRC20/Utils/InverseBooleanConverter.cs new file mode 100644 index 0000000..1cb9c87 --- /dev/null +++ b/WpfApp1TRC20/Utils/InverseBooleanConverter.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Data; + +namespace WpfApp1TRC20.Utils +{ + public class InverseBooleanConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool boolValue) + return !boolValue; + return true; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool boolValue) + return !boolValue; + return false; + } + } +} diff --git a/WpfApp1TRC20/Utils/StatusToColorConverter.cs b/WpfApp1TRC20/Utils/StatusToColorConverter.cs new file mode 100644 index 0000000..e0d01e4 --- /dev/null +++ b/WpfApp1TRC20/Utils/StatusToColorConverter.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Data; +using WpfApp1TRC20.Data; + +namespace WpfApp1TRC20.Utils +{ + public class StatusToColorConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is TransactionStatus status) + { + return status switch + { + TransactionStatus.Confirmed => "Green", + TransactionStatus.Pending => "Orange", + TransactionStatus.Failed => "Red", + _ => "Gray" + }; + } + return "Gray"; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/WpfApp1TRC20/ViewModels/MainViewModel.cs b/WpfApp1TRC20/ViewModels/MainViewModel.cs new file mode 100644 index 0000000..68c48b9 --- /dev/null +++ b/WpfApp1TRC20/ViewModels/MainViewModel.cs @@ -0,0 +1,445 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Input; +using WpfApp1TRC20.Data; +using WpfApp1TRC20.Services; +using TransactionStatus = WpfApp1TRC20.Data.TransactionStatus; + +namespace WpfApp1TRC20.ViewModels +{ + + public class MainViewModel : ViewModelBase + { + private readonly IDataService _dataService; + private readonly ITronService _tronService; + private readonly ILogger _logger; + + private TokenInfo _selectedToken; + private WalletAccount _selectedWallet; + private string _selectedTabItem = "Dashboard"; + + public MainViewModel(IDataService dataService, ITronService tronService, ILogger logger) + { + _dataService = dataService; + _tronService = tronService; + _logger = logger; + + Tokens = new ObservableCollection(); + Wallets = new ObservableCollection(); + Transactions = new ObservableCollection(); + TokenBalances = new ObservableCollection(); + + // Commands + CreateTokenCommand = new RelayCommand(async () => await CreateTokenAsync()); + CreateWalletCommand = new RelayCommand(async () => await CreateWalletAsync()); + ImportWalletCommand = new RelayCommand(async () => await ImportWalletAsync()); + TransferTokenCommand = new RelayCommand(async () => await TransferTokenAsync(), CanTransferToken); + RefreshDataCommand = new RelayCommand(async () => await RefreshDataAsync()); + + // Load initial data + _ = Task.Run(RefreshDataAsync); + } + + // Properties + public ObservableCollection Tokens { get; } + public ObservableCollection Wallets { get; } + public ObservableCollection Transactions { get; } + public ObservableCollection TokenBalances { get; } + + public TokenInfo SelectedToken + { + get => _selectedToken; + set => SetProperty(ref _selectedToken, value); + } + + public WalletAccount SelectedWallet + { + get => _selectedWallet; + set + { + SetProperty(ref _selectedWallet, value); + if (value != null) + _ = Task.Run(() => LoadTokenBalancesAsync(value.Address)); + } + } + + public string SelectedTabItem + { + get => _selectedTabItem; + set => SetProperty(ref _selectedTabItem, value); + } + + // Token Creation Properties + private string _newTokenName; + public string NewTokenName + { + get => _newTokenName; + set => SetProperty(ref _newTokenName, value); + } + + private string _newTokenSymbol; + public string NewTokenSymbol + { + get => _newTokenSymbol; + set => SetProperty(ref _newTokenSymbol, value); + } + + private int _newTokenDecimals = 18; + public int NewTokenDecimals + { + get => _newTokenDecimals; + set => SetProperty(ref _newTokenDecimals, value); + } + + private decimal _newTokenSupply = 1000000; + public decimal NewTokenSupply + { + get => _newTokenSupply; + set => SetProperty(ref _newTokenSupply, value); + } + + private int _newTokenExpiryDays = 30; + public int NewTokenExpiryDays + { + get => _newTokenExpiryDays; + set => SetProperty(ref _newTokenExpiryDays, value); + } + + // Transfer Properties + private string _transferToAddress; + public string TransferToAddress + { + get => _transferToAddress; + set => SetProperty(ref _transferToAddress, value); + } + + private decimal _transferAmount; + public decimal TransferAmount + { + get => _transferAmount; + set => SetProperty(ref _transferAmount, value); + } + + private TokenBalance _selectedTokenBalance; + public TokenBalance SelectedTokenBalance + { + get => _selectedTokenBalance; + set => SetProperty(ref _selectedTokenBalance, value); + } + + private string _newWalletName; + public string NewWalletName + { + get => _newWalletName; + set => SetProperty(ref _newWalletName, value); + } + + private string _importPrivateKey; + public string ImportPrivateKey + { + get => _importPrivateKey; + set => SetProperty(ref _importPrivateKey, value); + } + + private bool _isLoading; + public bool IsLoading + { + get => _isLoading; + set => SetProperty(ref _isLoading, value); + } + + private string _statusMessage; + public string StatusMessage + { + get => _statusMessage; + set => SetProperty(ref _statusMessage, value); + } + + // Commands + public ICommand CreateTokenCommand { get; } + public ICommand CreateWalletCommand { get; } + public ICommand ImportWalletCommand { get; } + public ICommand TransferTokenCommand { get; } + public ICommand RefreshDataCommand { get; } + + // Methods + private async Task CreateTokenAsync() + { + if (string.IsNullOrWhiteSpace(NewTokenName) || string.IsNullOrWhiteSpace(NewTokenSymbol) || SelectedWallet == null) + { + StatusMessage = "Please fill all required fields and select a wallet"; + return; + } + + IsLoading = true; + StatusMessage = "Creating token..."; + + try + { + var parameters = new TokenCreationParams + { + Name = NewTokenName, + Symbol = NewTokenSymbol, + Decimals = NewTokenDecimals, + TotalSupply = NewTokenSupply, + ExpiryDays = NewTokenExpiryDays, + OwnerAddress = SelectedWallet.Address + }; + + var privateKey = _tronService.DecryptPrivateKey(SelectedWallet.EncryptedPrivateKey); + var txHash = await _tronService.CreateTokenAsync(parameters, privateKey); + + var token = new TokenInfo + { + Name = parameters.Name, + Symbol = parameters.Symbol, + Decimals = parameters.Decimals, + TotalSupply = parameters.TotalSupply, + OwnerAddress = parameters.OwnerAddress, + CreationDate = DateTime.UtcNow, + ExpiryDate = DateTime.UtcNow.AddDays(parameters.ExpiryDays), + ExpiryDays = parameters.ExpiryDays, + TransactionHash = txHash, + Status = TokenStatus.Creating + }; + + await _dataService.SaveTokenAsync(token); + Tokens.Add(token); + + // Clear form + NewTokenName = ""; + NewTokenSymbol = ""; + NewTokenDecimals = 18; + NewTokenSupply = 1000000; + NewTokenExpiryDays = 30; + + StatusMessage = $"Token creation initiated. Transaction: {txHash}"; + + // Monitor transaction status + _ = Task.Run(() => MonitorTokenCreation(token)); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating token"); + StatusMessage = $"Error creating token: {ex.Message}"; + } + finally + { + IsLoading = false; + } + } + + private async Task CreateWalletAsync() + { + if (string.IsNullOrWhiteSpace(NewWalletName)) + { + StatusMessage = "Please enter a wallet name"; + return; + } + + try + { + var wallet = _tronService.CreateWallet(NewWalletName); + await _dataService.SaveWalletAsync(wallet); + Wallets.Add(wallet); + + NewWalletName = ""; + StatusMessage = $"Wallet '{wallet.Name}' created successfully"; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating wallet"); + StatusMessage = $"Error creating wallet: {ex.Message}"; + } + } + + private async Task ImportWalletAsync() + { + if (string.IsNullOrWhiteSpace(NewWalletName) || string.IsNullOrWhiteSpace(ImportPrivateKey)) + { + StatusMessage = "Please enter wallet name and private key"; + return; + } + + try + { + var wallet = _tronService.ImportWallet(NewWalletName, ImportPrivateKey); + await _dataService.SaveWalletAsync(wallet); + Wallets.Add(wallet); + + NewWalletName = ""; + ImportPrivateKey = ""; + StatusMessage = $"Wallet '{wallet.Name}' imported successfully"; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error importing wallet"); + StatusMessage = $"Error importing wallet: {ex.Message}"; + } + } + + private bool CanTransferToken() + { + return SelectedWallet != null && SelectedTokenBalance != null && + !string.IsNullOrWhiteSpace(TransferToAddress) && TransferAmount > 0; + } + + private async Task TransferTokenAsync() + { + if (!CanTransferToken()) return; + + IsLoading = true; + StatusMessage = "Transferring tokens..."; + + try + { + var privateKey = _tronService.DecryptPrivateKey(SelectedWallet.EncryptedPrivateKey); + var txHash = await _tronService.TransferTokenAsync( + SelectedTokenBalance.ContractAddress, + privateKey, + TransferToAddress, + TransferAmount + ); + + var transaction = new TransactionRecord + { + TransactionHash = txHash, + FromAddress = SelectedWallet.Address, + ToAddress = TransferToAddress, + Amount = TransferAmount, + TokenSymbol = SelectedTokenBalance.TokenSymbol, + ContractAddress = SelectedTokenBalance.ContractAddress, + Timestamp = DateTime.UtcNow, + Status = TransactionStatus.Pending + }; + + await _dataService.SaveTransactionAsync(transaction); + Transactions.Insert(0, transaction); + + // Clear form + TransferToAddress = ""; + TransferAmount = 0; + + StatusMessage = $"Transfer initiated. Transaction: {txHash}"; + + // Refresh balances + await LoadTokenBalancesAsync(SelectedWallet.Address); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error transferring tokens"); + StatusMessage = $"Error transferring tokens: {ex.Message}"; + } + finally + { + IsLoading = false; + } + } + + private async Task RefreshDataAsync() + { + try + { + var tokens = await _dataService.GetTokensAsync(); + var wallets = await _dataService.GetWalletsAsync(); + var transactions = await _dataService.GetTransactionsAsync(); + + Application.Current.Dispatcher.Invoke(() => + { + Tokens.Clear(); + foreach (var token in tokens) Tokens.Add(token); + + Wallets.Clear(); + foreach (var wallet in wallets) Wallets.Add(wallet); + + Transactions.Clear(); + foreach (var transaction in transactions) Transactions.Add(transaction); + }); + + if (SelectedWallet != null) + { + await LoadTokenBalancesAsync(SelectedWallet.Address); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error refreshing data"); + } + } + + private async Task LoadTokenBalancesAsync(string walletAddress) + { + try + { + var balances = await _dataService.GetTokenBalancesAsync(walletAddress); + + Application.Current.Dispatcher.Invoke(() => + { + TokenBalances.Clear(); + foreach (var balance in balances) TokenBalances.Add(balance); + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error loading token balances"); + } + } + + private async Task MonitorTokenCreation(TokenInfo token) + { + // Wait for transaction confirmation + for (int i = 0; i < 30; i++) // Wait up to 5 minutes + { + await Task.Delay(10000); // Wait 10 seconds + + try + { + var txInfo = await _tronService.GetTransactionAsync(token.TransactionHash); + + if (txInfo.Status == TransactionStatus.Confirmed) + { + // Extract contract address from transaction (simplified) + // In real implementation, parse the transaction receipt + token.ContractAddress = GenerateContractAddress(token.TransactionHash); + token.Status = TokenStatus.Active; + + await _dataService.SaveTokenAsync(token); + + Application.Current.Dispatcher.Invoke(() => + { + StatusMessage = $"Token '{token.Symbol}' deployed successfully!"; + }); + + break; + } + else if (txInfo.Status == TransactionStatus.Failed) + { + token.Status = TokenStatus.Failed; + await _dataService.SaveTokenAsync(token); + + Application.Current.Dispatcher.Invoke(() => + { + StatusMessage = $"Token '{token.Symbol}' deployment failed"; + }); + + break; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error monitoring token creation"); + } + } + } + + private string GenerateContractAddress(string transactionHash) + { + // Simplified contract address generation + // In real implementation, calculate from transaction details + return "T" + transactionHash.Substring(0, 33); + } + } +} diff --git a/WpfApp1TRC20/ViewModels/RelayCommand .cs b/WpfApp1TRC20/ViewModels/RelayCommand .cs new file mode 100644 index 0000000..65c8b77 --- /dev/null +++ b/WpfApp1TRC20/ViewModels/RelayCommand .cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Input; + +namespace WpfApp1TRC20.ViewModels +{ + public class RelayCommand : ICommand + { + private readonly Func _execute; + private readonly Func _canExecute; + + public RelayCommand(Func execute, Func canExecute = null) + { + _execute = execute ?? throw new ArgumentNullException(nameof(execute)); + _canExecute = canExecute; + } + + public event EventHandler CanExecuteChanged + { + add { CommandManager.RequerySuggested += value; } + remove { CommandManager.RequerySuggested -= value; } + } + + public bool CanExecute(object parameter) => _canExecute?.Invoke() ?? true; + + public async void Execute(object parameter) => await _execute(); + } +} diff --git a/WpfApp1TRC20/ViewModels/ViewModelBase.cs b/WpfApp1TRC20/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..b3efaf9 --- /dev/null +++ b/WpfApp1TRC20/ViewModels/ViewModelBase.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace WpfApp1TRC20.ViewModels +{ + public abstract class ViewModelBase : INotifyPropertyChanged + { + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + protected virtual bool SetProperty(ref T storage, T value, [CallerMemberName] string propertyName = null) + { + if (EqualityComparer.Default.Equals(storage, value)) + return false; + + storage = value; + OnPropertyChanged(propertyName); + return true; + } + } +} diff --git a/WpfApp1TRC20/Views/MainWindow.xaml b/WpfApp1TRC20/Views/MainWindow.xaml new file mode 100644 index 0000000..41a30db --- /dev/null +++ b/WpfApp1TRC20/Views/MainWindow.xaml @@ -0,0 +1,505 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +