Иконка ресурса

FREE Orangemart : 0.7.3 0.7.3

Нет прав для скачивания
C#:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Libraries;

namespace Oxide.Plugins
{
    [Info("Orangemart", "RustySats Orangemart", "0.7.3")]
    [Description("Allows players to buy and sell in-game units and VIP status using Bitcoin Lightning Network payments via LNbits with Fiat/BTC pricing and comprehensive protection features")]
    public class Orangemart : CovalencePlugin
    {
        // Configuration sections and keys
        private static class ConfigSections
        {
            public const string Commands = "Commands";
            public const string CurrencySettings = "CurrencySettings";
            public const string Discord = "Discord";
            public const string InvoiceSettings = "InvoiceSettings";
            public const string VIPSettings = "VIPSettings";
            public const string ExternalApi = "ExternalApi"; // NEW
        }

        private static class ConfigKeys
        {
            // Commands
            public const string BuyCurrencyCommandName = "BuyCurrencyCommandName";
            public const string SendCurrencyCommandName = "SendCurrencyCommandName";
            public const string BankCommandName = "BankCommandName";
            public const string BuyVipCommandName = "BuyVipCommandName";

            // CurrencySettings
            public const string CurrencyItemID = "CurrencyItemID";
            public const string CurrencyName = "CurrencyName";
            public const string CurrencySkinID = "CurrencySkinID";
            public const string PricePerCurrencyUnit = "PricePerCurrencyUnit";
            public const string CurrencyPriceCurrency = "CurrencyPriceCurrency"; 
            public const string SatsPerCurrencyUnit = "SatsPerCurrencyUnit";
            
            // Protection Settings
            public const string MaxPurchaseAmount = "MaxPurchaseAmount";
            public const string MaxSendAmount = "MaxSendAmount";
            public const string CommandCooldownSeconds = "CommandCooldownSeconds";
            public const string MaxPendingInvoicesPerPlayer = "MaxPendingInvoicesPerPlayer";

            // Discord
            public const string DiscordChannelName = "DiscordChannelName";
            public const string DiscordWebhookUrl = "DiscordWebhookUrl";
            public const string AdminDiscordWebhookUrl = "AdminDiscordWebhookUrl";

            // InvoiceSettings
            public const string BlacklistedDomains = "BlacklistedDomains";
            public const string WhitelistedDomains = "WhitelistedDomains";
            public const string CheckIntervalSeconds = "CheckIntervalSeconds";
            public const string InvoiceTimeoutSeconds = "InvoiceTimeoutSeconds";
            public const string LNbitsApiKey = "LNbitsApiKey";
            public const string LNbitsBaseUrl = "LNbitsBaseUrl";
            public const string MaxRetries = "MaxRetries";
            public const string UseWebSockets = "UseWebSockets";
            public const string WebSocketReconnectDelay = "WebSocketReconnectDelay";

            // VIPSettings
            public const string VipPrice = "VipPrice";
            public const string VipPriceCurrency = "VipPriceCurrency"; 
            public const string VipCommand = "VipCommand";

            // ExternalApi
            public const string AddressLookupApiUrl = "AddressLookupApiUrl";
            public const string AddressLookupApiKey = "AddressLookupApiKey";
        }

        // Configuration variables
        private int currencyItemID;
        private string buyCurrencyCommandName;
        private string sendCurrencyCommandName;
        private string bankCommandName;
        private string buyVipCommandName;
        private double vipPrice; 
        private string vipPriceCurrency; 
        private string vipCommand;
        private string currencyName;
        private int satsPerCurrencyUnit;
        private double pricePerCurrencyUnit; 
        private string currencyPriceCurrency; 
        private string discordChannelName;
        private string adminDiscordWebhookUrl;
        private ulong currencySkinID;
        private int checkIntervalSeconds;
        private int invoiceTimeoutSeconds;
        private int maxRetries;
        private bool useWebSockets;
        private int webSocketReconnectDelay;
        private List<string> blacklistedDomains = new List<string>();
        private List<string> whitelistedDomains = new List<string>();
        private string addressLookupApiUrl; // NEW
        private string addressLookupApiKey; // NEW
        
        // Protection and rate limiting variables
        private int maxPurchaseAmount;
        private int maxSendAmount;
        private int commandCooldownSeconds;
        private int maxPendingInvoicesPerPlayer;
        private Dictionary<string, DateTime> lastCommandTime = new Dictionary<string, DateTime>();

        private const string SellLogFile = "Orangemart/send_bitcoin.json";
        private const string BuyInvoiceLogFile = "Orangemart/buy_invoices.json";
        private const string OfflineQueueFile = "Orangemart/offline_rewards.json"; 
        
        private LNbitsConfig config;
        private List<PendingInvoice> pendingInvoices = new List<PendingInvoice>();
        private Dictionary<string, int> retryCounts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
        private Dictionary<string, int> offlineCurrencyQueue = new Dictionary<string, int>(); 
        
        // WebSocket tracking
        private Dictionary<string, WebSocketConnection> activeWebSockets = new Dictionary<string, WebSocketConnection>();
        private readonly object webSocketLock = new object();

        // BTC Price Cache
        private double cachedBtcPriceUsd = 0;
        private DateTime lastBtcPriceFetch = DateTime.MinValue;

        // Transaction status constants
        private static class TransactionStatus
        {
            public const string INITIATED = "INITIATED";
            public const string PROCESSING = "PROCESSING";
            public const string COMPLETED = "COMPLETED";
            public const string FAILED = "FAILED";
            public const string EXPIRED = "EXPIRED";
            public const string REFUNDED = "REFUNDED";
        }

        // External API Response Model
        private class AddressLookupResponse
        {
            [JsonProperty("lightningAddress")]
            public string LightningAddress { get; set; }
            [JsonProperty("error")]
            public string Error { get; set; }
        }

        // Price Fetching Models
        private class MempoolPriceResponse
        {
            [JsonProperty("USD")]
            public double USD { get; set; }
        }

        // WebSocket connection wrapper
        private class WebSocketConnection
        {
            public ClientWebSocket WebSocket { get; set; }
            public CancellationTokenSource CancellationTokenSource { get; set; }
            public string InvoiceKey { get; set; }
            public PendingInvoice Invoice { get; set; }
            public DateTime ConnectedAt { get; set; }
            public int ReconnectAttempts { get; set; }
            public Task ListenTask { get; set; }
        }

        // WebSocket response structure
        private class WebSocketPaymentUpdate
        {
            [JsonProperty("balance")]
            public long Balance { get; set; }
            
            [JsonProperty("payment")]
            public WebSocketPayment Payment { get; set; }
        }

        private class WebSocketPayment
        {
            [JsonProperty("checking_id")]
            public string CheckingId { get; set; }
            
            [JsonProperty("pending")]
            public bool Pending { get; set; }
            
            [JsonProperty("amount")]
            public long Amount { get; set; }
            
            [JsonProperty("payment_hash")]
            public string PaymentHash { get; set; }
            
            [JsonProperty("preimage")]
            public string Preimage { get; set; }
        }

        // LNbits Configuration
        private class LNbitsConfig
        {
            public string BaseUrl { get; set; }
            public string ApiKey { get; set; }
            public string DiscordWebhookUrl { get; set; }
            public string WebSocketUrl { get; set; }

            public static LNbitsConfig ParseLNbitsConnection(string baseUrl, string apiKey, string discordWebhookUrl)
            {
                var trimmedBaseUrl = baseUrl.TrimEnd('/');
                if (!Uri.IsWellFormedUriString(trimmedBaseUrl, UriKind.Absolute))
                    throw new Exception("Invalid base URL in connection string.");

                var wsUrl = trimmedBaseUrl.Replace("https://", "wss://").Replace("http://", "ws://");

                return new LNbitsConfig
                {
                    BaseUrl = trimmedBaseUrl,
                    ApiKey = apiKey,
                    DiscordWebhookUrl = discordWebhookUrl,
                    WebSocketUrl = wsUrl
                };
            }
        }

        // Invoice and Payment Classes
        private class InvoiceResponse
        {
            [JsonProperty("bolt11")]
            public string PaymentRequest { get; set; }

            [JsonProperty("payment_hash")]
            public string PaymentHash { get; set; }
        }

        private class InvoiceResponseWrapper
        {
            [JsonProperty("data")]
            public InvoiceResponse Data { get; set; }
        }

        private class SellInvoiceLogEntry
        {
            public string TransactionId { get; set; }
            public string SteamID { get; set; }
            public string LightningAddress { get; set; }
            public string Status { get; set; }
            public bool Success { get; set; }
            public int SatsAmount { get; set; }
            public string PaymentHash { get; set; }
            public bool CurrencyReturned { get; set; }
            public DateTime Timestamp { get; set; }
            public DateTime? CompletedTimestamp { get; set; }
            public int RetryCount { get; set; }
            public string FailureReason { get; set; }
        }

        private class BuyInvoiceLogEntry
        {
            public string TransactionId { get; set; }
            public string SteamID { get; set; }
            public string InvoiceID { get; set; }
            public string Status { get; set; }
            public bool IsPaid { get; set; }
            public DateTime Timestamp { get; set; }
            public DateTime? CompletedTimestamp { get; set; }
            public int Amount { get; set; }
            public bool CurrencyGiven { get; set; }
            public bool VipGranted { get; set; }
            public int RetryCount { get; set; }
            public string PurchaseType { get; set; }
        }

        private class PendingInvoice
        {
            public string TransactionId { get; set; }
            public string RHash { get; set; }
            public IPlayer Player { get; set; }
            public int Amount { get; set; }
            public int SatsAmount { get; set; }
            public string Memo { get; set; }
            public DateTime CreatedAt { get; set; }
            public PurchaseType Type { get; set; }
            public string DiscordMessageId { get; set; } 
        }

        private enum PurchaseType
        {
            Currency,
            Vip,
            SendBitcoin
        }

        private class PaymentStatusResponse
        {
            [JsonProperty("paid")]
            public bool Paid { get; set; }

            [JsonProperty("preimage")]
            public string Preimage { get; set; }
        }

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                bool configChanged = false;

                config = LNbitsConfig.ParseLNbitsConnection(
                    GetConfigValue(ConfigSections.InvoiceSettings, ConfigKeys.LNbitsBaseUrl, "https://your-lnbits-instance.com", ref configChanged),
                    GetConfigValue(ConfigSections.InvoiceSettings, ConfigKeys.LNbitsApiKey, "your-lnbits-admin-api-key", ref configChanged),
                    GetConfigValue(ConfigSections.Discord, ConfigKeys.DiscordWebhookUrl, "https://discord.com/api/webhooks/your_webhook_url", ref configChanged)
                );

                // External API Settings
                addressLookupApiUrl = GetConfigValue(ConfigSections.ExternalApi, ConfigKeys.AddressLookupApiUrl, "https://theorangemart.com/api/server/resolve-address", ref configChanged);
                addressLookupApiKey = GetConfigValue(ConfigSections.ExternalApi, ConfigKeys.AddressLookupApiKey, "your_super_secret_api_key_here", ref configChanged);

                // Currency Settings
                currencyItemID = GetConfigValue(ConfigSections.CurrencySettings, ConfigKeys.CurrencyItemID, 1776460938, ref configChanged);
                currencyName = GetConfigValue(ConfigSections.CurrencySettings, ConfigKeys.CurrencyName, "blood", ref configChanged);
                satsPerCurrencyUnit = GetConfigValue(ConfigSections.CurrencySettings, ConfigKeys.SatsPerCurrencyUnit, 1, ref configChanged);
                pricePerCurrencyUnit = GetConfigValue(ConfigSections.CurrencySettings, ConfigKeys.PricePerCurrencyUnit, 1.0, ref configChanged);
                currencyPriceCurrency = GetConfigValue(ConfigSections.CurrencySettings, ConfigKeys.CurrencyPriceCurrency, "SATS", ref configChanged);
                currencySkinID = GetConfigValue(ConfigSections.CurrencySettings, ConfigKeys.CurrencySkinID, 0UL, ref configChanged);

                // Protection Settings
                maxPurchaseAmount = GetConfigValue(ConfigSections.CurrencySettings, ConfigKeys.MaxPurchaseAmount, 10000, ref configChanged);
                maxSendAmount = GetConfigValue(ConfigSections.CurrencySettings, ConfigKeys.MaxSendAmount, 10000, ref configChanged);
                commandCooldownSeconds = GetConfigValue(ConfigSections.CurrencySettings, ConfigKeys.CommandCooldownSeconds, 0, ref configChanged);
                maxPendingInvoicesPerPlayer = GetConfigValue(ConfigSections.CurrencySettings, ConfigKeys.MaxPendingInvoicesPerPlayer, 1, ref configChanged);

                if (maxPurchaseAmount < 0) maxPurchaseAmount = 0;
                if (maxSendAmount < 0) maxSendAmount = 0;
                if (commandCooldownSeconds < 0) commandCooldownSeconds = 0;
                if (maxPendingInvoicesPerPlayer < 0) maxPendingInvoicesPerPlayer = 0;

                // Command Names
                buyCurrencyCommandName = GetConfigValue(ConfigSections.Commands, ConfigKeys.BuyCurrencyCommandName, "buyblood", ref configChanged);
                sendCurrencyCommandName = GetConfigValue(ConfigSections.Commands, ConfigKeys.SendCurrencyCommandName, "sendblood", ref configChanged);
                bankCommandName = GetConfigValue(ConfigSections.Commands, ConfigKeys.BankCommandName, "bank", ref configChanged);
                buyVipCommandName = GetConfigValue(ConfigSections.Commands, ConfigKeys.BuyVipCommandName, "buyvip", ref configChanged);

                // VIP Settings
                vipPrice = GetConfigValue(ConfigSections.VIPSettings, ConfigKeys.VipPrice, 1000.0, ref configChanged);
                vipPriceCurrency = GetConfigValue(ConfigSections.VIPSettings, ConfigKeys.VipPriceCurrency, "SATS", ref configChanged);
                vipCommand = GetConfigValue(ConfigSections.VIPSettings, ConfigKeys.VipCommand, "oxide.usergroup add {player} vip", ref configChanged);

                // Discord Settings
                discordChannelName = GetConfigValue(ConfigSections.Discord, ConfigKeys.DiscordChannelName, "mart", ref configChanged);
                adminDiscordWebhookUrl = GetConfigValue(ConfigSections.Discord, ConfigKeys.AdminDiscordWebhookUrl, "", ref configChanged);

                // Invoice Settings
                checkIntervalSeconds = GetConfigValue(ConfigSections.InvoiceSettings, ConfigKeys.CheckIntervalSeconds, 10, ref configChanged);
                invoiceTimeoutSeconds = GetConfigValue(ConfigSections.InvoiceSettings, ConfigKeys.InvoiceTimeoutSeconds, 300, ref configChanged);
                maxRetries = GetConfigValue(ConfigSections.InvoiceSettings, ConfigKeys.MaxRetries, 25, ref configChanged);
                useWebSockets = GetConfigValue(ConfigSections.InvoiceSettings, ConfigKeys.UseWebSockets, true, ref configChanged);
                webSocketReconnectDelay = GetConfigValue(ConfigSections.InvoiceSettings, ConfigKeys.WebSocketReconnectDelay, 5, ref configChanged);

                blacklistedDomains = GetConfigValue(ConfigSections.InvoiceSettings, ConfigKeys.BlacklistedDomains, new List<string> { "example.com", "blacklisted.net" }, ref configChanged).Select(d => d.ToLower()).ToList();

                whitelistedDomains = GetConfigValue(ConfigSections.InvoiceSettings, ConfigKeys.WhitelistedDomains, new List<string>(), ref configChanged).Select(d => d.ToLower()).ToList();

                if (configChanged)
                {
                    SaveConfig();
                }

                Puts($"Protection Settings: MaxPurchase={maxPurchaseAmount}, MaxSend={maxSendAmount}, Cooldown={commandCooldownSeconds}s, MaxPending={maxPendingInvoicesPerPlayer}");
                Puts($"Pricing Denominations: VIP={vipPriceCurrency}, Currency={currencyPriceCurrency}");
            }
            catch (Exception ex)
            {
                PrintError($"Failed to load configuration: {ex.Message}");
            }
        }

        private T GetConfigValue<T>(string section, string key, T defaultValue, ref bool configChanged)
        {
            if (!(Config[section] is Dictionary<string, object> data))
            {
                data = new Dictionary<string, object>();
                Config[section] = data;
                configChanged = true;
            }

            if (!data.TryGetValue(key, out var value))
            {
                value = defaultValue;
                data[key] = value;
                configChanged = true;
            }

            try
            {
                if (value is T tValue) return tValue;
                if (typeof(T) == typeof(List<string>))
                {
                    if (value is IEnumerable<object> enumerable)
                        return (T)(object)enumerable.Select(item => item.ToString()).ToList();
                    return (T)(object)new List<string> { value.ToString() };
                }
                if (typeof(T) == typeof(ulong)) return (T)(object)Convert.ToUInt64(value);
                if (typeof(T) == typeof(double)) return (T)(object)Convert.ToDouble(value);
                return (T)Convert.ChangeType(value, typeof(T));
            }
            catch
            {
                data[key] = defaultValue;
                configChanged = true;
                return defaultValue;
            }
        }

        protected override void LoadDefaultConfig()
        {
            Config[ConfigSections.Commands] = new Dictionary<string, object>
            {
                [ConfigKeys.BuyCurrencyCommandName] = "buyblood",
                [ConfigKeys.BuyVipCommandName] = "buyvip",
                [ConfigKeys.BankCommandName] = "bank",
                [ConfigKeys.SendCurrencyCommandName] = "sendblood"
            };

            Config[ConfigSections.ExternalApi] = new Dictionary<string, object>
            {
                [ConfigKeys.AddressLookupApiUrl] = "https://theorangemart.com/api/server/resolve-address",
                [ConfigKeys.AddressLookupApiKey] = "your_super_secret_api_key_here"
            };

            Config[ConfigSections.CurrencySettings] = new Dictionary<string, object>
            {
                [ConfigKeys.CurrencyItemID] = 1776460938,
                [ConfigKeys.CurrencyName] = "blood",
                [ConfigKeys.CurrencySkinID] = 0UL,
                [ConfigKeys.PricePerCurrencyUnit] = 1.0,
                [ConfigKeys.CurrencyPriceCurrency] = "SATS",
                [ConfigKeys.SatsPerCurrencyUnit] = 1,
                [ConfigKeys.MaxPurchaseAmount] = 10000,
                [ConfigKeys.MaxSendAmount] = 10000,
                [ConfigKeys.CommandCooldownSeconds] = 0,
                [ConfigKeys.MaxPendingInvoicesPerPlayer] = 1
            };

            Config[ConfigSections.Discord] = new Dictionary<string, object>
            {
                [ConfigKeys.DiscordChannelName] = "mart",
                [ConfigKeys.DiscordWebhookUrl] = "https://discord.com/api/webhooks/your_webhook_url",
                [ConfigKeys.AdminDiscordWebhookUrl] = "" 
            };

            Config[ConfigSections.InvoiceSettings] = new Dictionary<string, object>
            {
                [ConfigKeys.BlacklistedDomains] = new List<string> { "example.com", "blacklisted.net" },
                [ConfigKeys.WhitelistedDomains] = new List<string>(),
                [ConfigKeys.CheckIntervalSeconds] = 10,
                [ConfigKeys.InvoiceTimeoutSeconds] = 300,
                [ConfigKeys.LNbitsApiKey] = "your-lnbits-admin-api-key",
                [ConfigKeys.LNbitsBaseUrl] = "https://your-lnbits-instance.com",
                [ConfigKeys.MaxRetries] = 25,
                [ConfigKeys.UseWebSockets] = true,
                [ConfigKeys.WebSocketReconnectDelay] = 5
            };

            Config[ConfigSections.VIPSettings] = new Dictionary<string, object>
            {
                [ConfigKeys.VipCommand] = "oxide.usergroup add {steamid} vip",
                [ConfigKeys.VipPrice] = 1000.0,
                [ConfigKeys.VipPriceCurrency] = "SATS"
            };
        }

        private void Init()
        {
            permission.RegisterPermission("orangemart.buycurrency", this);
            permission.RegisterPermission("orangemart.sendcurrency", this);
            permission.RegisterPermission("orangemart.buyvip", this);
            permission.RegisterPermission("orangemart.admin", this);
        }

        private void OnServerInitialized()
        {
            if (config == null)
            {
                PrintError("Plugin configuration is not properly set up.");
                return;
            }

            LoadOfflineQueue(); 

            AddCovalenceCommand(buyCurrencyCommandName, nameof(CmdBuyCurrency), "orangemart.buycurrency");
            AddCovalenceCommand(sendCurrencyCommandName, nameof(CmdSendCurrency), "orangemart.sendcurrency");
            AddCovalenceCommand(bankCommandName, nameof(CmdBank), "orangemart.sendcurrency");
            AddCovalenceCommand(buyVipCommandName, nameof(CmdBuyVip), "orangemart.buyvip");
            AddCovalenceCommand("giveblood", nameof(CmdGiveBlood), "orangemart.admin");

            RecoverInterruptedTransactions();

            timer.Every(checkIntervalSeconds, CheckPendingInvoices);
            timer.Every(300f, CleanupOldCooldowns);

            Puts($"Orangemart initialized.WebSockets: {(useWebSockets ? "Enabled" : "Disabled")}");
        }

        private void Unload()
        {
            CleanupAllWebSockets();
            SaveOfflineQueue(); 
            pendingInvoices.Clear();
            retryCounts.Clear();
            lastCommandTime.Clear();
        }

        // --- Offline Queue Methods ---
        private void LoadOfflineQueue()
        {
            var path = Path.Combine(Interface.Oxide.DataDirectory, OfflineQueueFile);
            if (File.Exists(path))
            {
                offlineCurrencyQueue = JsonConvert.DeserializeObject<Dictionary<string, int>>(File.ReadAllText(path)) ?? new Dictionary<string, int>();
            }
        }

        private void SaveOfflineQueue()
        {
            var path = Path.Combine(Interface.Oxide.DataDirectory, OfflineQueueFile);
            if (!Directory.Exists(Path.GetDirectoryName(path))) Directory.CreateDirectory(Path.GetDirectoryName(path));
            File.WriteAllText(path, JsonConvert.SerializeObject(offlineCurrencyQueue, Formatting.Indented));
        }

        private void AddOfflineCurrency(string playerId, int amount)
        {
            if (offlineCurrencyQueue.ContainsKey(playerId))
                offlineCurrencyQueue[playerId] += amount;
            else
                offlineCurrencyQueue[playerId] = amount;
                
            SaveOfflineQueue();
        }

        private void OnPlayerSleepEnded(BasePlayer player)
        {
            if (player == null || !offlineCurrencyQueue.ContainsKey(player.UserIDString)) return;

            int pendingAmount = offlineCurrencyQueue[player.UserIDString];
            if (pendingAmount > 0)
            {
                var currencyItem = ItemManager.CreateByItemID(currencyItemID, pendingAmount);
                if (currencyItem != null)
                {
                    if (currencySkinID > 0) currencyItem.skin = currencySkinID;
                    player.GiveItem(currencyItem);
                    
                    player.ChatMessage($"Welcome back! You received {pendingAmount} {currencyName} from pending Orangemart transactions.");
                    Puts($"Delivered {pendingAmount} offline {currencyName} to {player.displayName} ({player.UserIDString}).");
                }
            }
            
            offlineCurrencyQueue.Remove(player.UserIDString);
            SaveOfflineQueue();
        }

        // --- Pricing Methods ---
        private void GetBtcPriceUsd(Action<double> callback)
        {
            if ((DateTime.UtcNow - lastBtcPriceFetch).TotalMinutes < 5 && cachedBtcPriceUsd > 0)
            {
                callback(cachedBtcPriceUsd);
                return;
            }

            webrequest.Enqueue("https://mempool.space/api/v1/prices", null, (code, response) =>
            {
                if (code == 200 && !string.IsNullOrEmpty(response))
                {
                    try
                    {
                        var data = JsonConvert.DeserializeObject<MempoolPriceResponse>(response);
                        if (data != null && data.USD > 0)
                        {
                            cachedBtcPriceUsd = data.USD;
                            lastBtcPriceFetch = DateTime.UtcNow;
                            callback(cachedBtcPriceUsd);
                            return;
                        }
                    }
                    catch (Exception ex)
                    {
                        PrintError($"Failed to parse BTC price from mempool.space: {ex.Message}");
                    }
                }
                
                if (cachedBtcPriceUsd > 0) callback(cachedBtcPriceUsd);
                else callback(0);

            }, this, RequestMethod.GET);
        }

        private void CalculateSatsAmount(double fiatOrSatsPrice, string currencyType, Action<int> callback)
        {
            if (currencyType.Equals("USD", StringComparison.OrdinalIgnoreCase))
            {
                GetBtcPriceUsd(btcPrice => {
                    if (btcPrice <= 0) {
                        callback(-1); 
                        return;
                    }
                    int sats = (int)Math.Ceiling((fiatOrSatsPrice / btcPrice) * 100_000_000);
                    callback(sats);
                });
            }
            else 
            {
                callback((int)Math.Ceiling(fiatOrSatsPrice));
            }
        }

        // Protection Methods
        private bool IsOnCooldown(IPlayer player, string commandType)
        {
            if (commandCooldownSeconds <= 0) return false;
            
            string key = $"{GetPlayerId(player)}:{commandType}";
            
            if (lastCommandTime.TryGetValue(key, out DateTime lastTime))
            {
                double secondsSince = (DateTime.UtcNow - lastTime).TotalSeconds;
                if (secondsSince < commandCooldownSeconds)
                {
                    double remaining = commandCooldownSeconds - secondsSince;
                    player.Reply(Lang("CommandOnCooldown", player.Id, commandType, Math.Ceiling(remaining)));
                    return true;
                }
            }
            
            lastCommandTime[key] = DateTime.UtcNow;
            return false;
        }

        private bool HasTooManyPendingInvoices(IPlayer player)
        {
            if (maxPendingInvoicesPerPlayer == 0) return false;
            
            string playerId = GetPlayerId(player);
            int pendingCount = pendingInvoices.Count(inv => GetPlayerId(inv.Player) == playerId);
            
            if (pendingCount >= maxPendingInvoicesPerPlayer)
            {
                player.Reply(Lang("TooManyPendingInvoices", player.Id, pendingCount, maxPendingInvoicesPerPlayer));
                return true;
            }
            
            return false;
        }

        private bool ValidateSendAmount(IPlayer player, int amount, out int safeSats)
        {
            safeSats = 0;
            if (amount <= 0)
            {
                player.Reply(Lang("InvalidAmount", player.Id));
                return false;
            }
            if (maxSendAmount > 0 && amount > maxSendAmount)
            {
                player.Reply(Lang("SendAmountTooLarge", player.Id, amount, maxSendAmount, currencyName));
                return false;
            }
            
            long amountSatsLong = (long)amount * satsPerCurrencyUnit;
            if (amountSatsLong > int.MaxValue)
            {
                player.Reply(Lang("AmountCausesOverflow", player.Id));
                return false;
            }
            
            safeSats = (int)amountSatsLong;
            return true;
        }

        private void CleanupOldCooldowns()
        {
            var expiredKeys = lastCommandTime.Where(kvp => (DateTime.UtcNow - kvp.Value).TotalSeconds > commandCooldownSeconds * 2).Select(kvp => kvp.Key).ToList();
            
            foreach (var key in expiredKeys)
                lastCommandTime.Remove(key);
        }

        private void CleanupAllWebSockets()
        {
            lock (webSocketLock)
            {
                foreach (var kvp in activeWebSockets)
                {
                    try
                    {
                        kvp.Value.CancellationTokenSource?.Cancel();
                        kvp.Value.WebSocket?.Dispose();
                    }
                    catch { }
                }
                activeWebSockets.Clear();
            }
        }

        protected override void LoadDefaultMessages()
        {
            lang.RegisterMessages(new Dictionary<string, string>
            {
                ["UsageSendCurrency"] = "Usage: /{0} <amount> [optional_lightning_address]",
                ["NeedMoreCurrency"] = "You need more {0}.You currently have {1}.",
                ["FailedToReserveCurrency"] = "Failed to reserve currency.Please try again.",
                ["FailedToQueryLightningAddress"] = "Failed to query Lightning address for an invoice.",
                ["FailedToAuthenticate"] = "Failed to authenticate with LNbits.",
                ["InvoiceCreatedCheckDiscord"] = "Invoice created! Please check the #{0} channel on Discord.",
                ["FailedToCreateInvoice"] = "Failed to create an invoice.Please try again later.",
                ["FailedToProcessPayment"] = "Failed to process payment.Please try again later.",
                ["CurrencySentSuccess"] = "You have successfully sent {0} {1}!",
                ["PurchaseSuccess"] = "You have successfully purchased {0} {1}!",
                ["PurchaseVipSuccess"] = "You have successfully purchased VIP status!",
                ["InvalidCommandUsage"] = "Usage: /{0} <amount>",
                ["NoPermission"] = "You do not have permission to use this command.",
                ["FailedToFindBasePlayer"] = "Failed to find base player object for player {0}.",
                ["FailedToCreateCurrencyItem"] = "Failed to create {0} item for player {1}.",
                ["AddedToVipGroup"] = "Player {0} added to VIP group '{1}'.",
                ["InvoiceExpired"] = "Your invoice for ₿{0} has expired.Please try again.",
                ["BlacklistedDomain"] = "The domain '{0}' is currently blacklisted.",
                ["NotWhitelistedDomain"] = "The domain '{0}' is not whitelisted.Allowed: {1}.",
                ["InvalidLightningAddress"] = "The Lightning Address provided is invalid.",
                ["PaymentProcessing"] = "Your payment is being processed...",
                ["TransactionInitiated"] = "Transaction initiated.Processing your payment...",
                ["InvalidAmount"] = "Invalid amount.Please enter a positive number.",
                ["AmountTooLarge"] = "Amount {0} exceeds maximum limit of {1} {2}.",
                ["SendAmountTooLarge"] = "Send amount {0} exceeds maximum limit of {1} {2}.",
                ["AmountCausesOverflow"] = "Amount too large.Please use a smaller amount.",
                ["CommandOnCooldown"] = "Command '{0}' is on cooldown.Wait {1}s.",
                ["TooManyPendingInvoices"] = "You have {0} pending invoices (max: {1}).",
                ["VipPriceTooHigh"] = "VIP price is configured too high.",
                ["ProtectionLimits"] = "Orangemart Limits: Purchase max {0}, Send max {1}, Cooldown {2}s",
                ["FailedToFetchPrice"] = "Failed to fetch live exchange rate.Please try again.",
                ["ApiNotConfigured"] = "The server has not configured an automatic address lookup.Usage: /{0} <amount> <lightning_address>",
                ["FailedToResolveAddressApi"] = "Could not find your linked Lightning Address ({0}).Please link one at theorangemart.com or provide an address manually."
            }, this);
        }

        private string Lang(string key, string userId = null, params object[] args)
        {
            return string.Format(lang.GetMessage(key, this, userId), args);
        }

        private string GenerateTransactionId()
        {
            return $"{DateTime.UtcNow.Ticks}-{Guid.NewGuid().ToString("N").Substring(0, 8)}";
        }

        // WebSocket connection management
        private async Task ConnectWebSocket(PendingInvoice invoice)
        {
            if (!useWebSockets) return;

            if (invoice.Type == PurchaseType.SendBitcoin) return;

            var wsConnection = new WebSocketConnection
            {
                WebSocket = new ClientWebSocket(),
                CancellationTokenSource = new CancellationTokenSource(),
                InvoiceKey = invoice.RHash,
                Invoice = invoice,
                ConnectedAt = DateTime.UtcNow,
                ReconnectAttempts = 0
            };

            wsConnection.WebSocket.Options.SetRequestHeader("X-Api-Key", config.ApiKey);

            lock (webSocketLock)
            {
                if (activeWebSockets.ContainsKey(invoice.RHash))
                {
                    var existing = activeWebSockets[invoice.RHash];
                    existing.CancellationTokenSource?.Cancel();
                    existing.WebSocket?.Dispose();
                }
                activeWebSockets[invoice.RHash] = wsConnection;
            }

            try
            {
                var wsUrl = $"{config.WebSocketUrl}/api/v1/ws/{invoice.RHash}";
                await wsConnection.WebSocket.ConnectAsync(new Uri(wsUrl), wsConnection.CancellationTokenSource.Token);
                
                wsConnection.ListenTask = Task.Run(async () => await ListenToWebSocket(wsConnection), wsConnection.CancellationTokenSource.Token);
            }
            catch (Exception ex)
            {
                PrintError($"Failed to connect WebSocket for invoice {invoice.RHash}: {ex.Message}");
                lock (webSocketLock) { activeWebSockets.Remove(invoice.RHash); }
            }
        }

        private async Task ListenToWebSocket(WebSocketConnection connection)
        {
            var buffer = new ArraySegment<byte>(new byte[4096]);
            var messageBuilder = new StringBuilder();

            try
            {
                while (connection.WebSocket.State == WebSocketState.Open && !connection.CancellationTokenSource.Token.IsCancellationRequested)
                {
                    WebSocketReceiveResult result;
                    messageBuilder.Clear();

                    do
                    {
                        result = await connection.WebSocket.ReceiveAsync(buffer, connection.CancellationTokenSource.Token);
                        if (result.MessageType == WebSocketMessageType.Text)
                            messageBuilder.Append(Encoding.UTF8.GetString(buffer.Array, 0, result.Count));
                        else if (result.MessageType == WebSocketMessageType.Close)
                        {
                            await connection.WebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);
                            break;
                        }
                    }
                    while (!result.EndOfMessage);

                    if (messageBuilder.Length > 0)
                        ProcessWebSocketMessage(connection, messageBuilder.ToString());
                }
            }
            catch { }
            finally
            {
                lock (webSocketLock)
                {
                    if (activeWebSockets.ContainsKey(connection.InvoiceKey))
                        activeWebSockets.Remove(connection.InvoiceKey);
                }
                connection.WebSocket?.Dispose();
            }
        }

        private void ProcessWebSocketMessage(WebSocketConnection connection, string message)
        {
            try
            {
                bool confirmed = false;

                try
                {
                    var simpleUpdate = JsonConvert.DeserializeObject<Dictionary<string, object>>(message);
                    if (simpleUpdate != null && simpleUpdate.ContainsKey("pending") && simpleUpdate.ContainsKey("status"))
                    {
                        bool isPending = Convert.ToBoolean(simpleUpdate["pending"]);
                        string status = simpleUpdate["status"]?.ToString();
                        if (!isPending && status == "success") confirmed = true;
                    }
                }
                catch { }
                
                if (!confirmed)
                {
                    try
                    {
                        var update = JsonConvert.DeserializeObject<WebSocketPaymentUpdate>(message);
                        if (update?.Payment != null)
                        {
                            if (!update.Payment.Pending && !string.IsNullOrEmpty(update.Payment.Preimage)) confirmed = true;
                            else if (!update.Payment.Pending && update.Payment.PaymentHash?.ToLower() == connection.InvoiceKey.ToLower()) confirmed = true;
                        }
                    }
                    catch { }
                }

                if (confirmed)
                {
                    Interface.Oxide.NextTick(() => {
                        ProcessPaymentConfirmation(connection.Invoice);
                    });
                    
                    connection.CancellationTokenSource?.Cancel();
                }
            }
            catch (Exception ex)
            {
                PrintError($"Error processing WebSocket message: {ex.Message}");
            }
        }

        private void ProcessPaymentConfirmation(PendingInvoice invoice)
        {
            if (!pendingInvoices.Contains(invoice)) return;

            pendingInvoices.Remove(invoice);
            
            string logMsg = $"Processing payment confirmation for {invoice.Player.Name} (Amount: ₿{invoice.SatsAmount}).Hash: {invoice.RHash}, Type: {invoice.Type}";
            Puts($"[ProcessPayment] {logMsg}");
            SendAdminNotification("Payment Confirmed", logMsg, 3066993); 

            switch (invoice.Type)
            {
                case PurchaseType.Currency:
                    RewardPlayer(invoice.Player, invoice.Amount);
                    UpdateBuyTransactionStatus(invoice.TransactionId, TransactionStatus.COMPLETED, true);
                    break;
                case PurchaseType.Vip:
                    GrantVip(invoice.Player);
                    UpdateBuyTransactionStatus(invoice.TransactionId, TransactionStatus.COMPLETED, true);
                    break;
                case PurchaseType.SendBitcoin:
                    invoice.Player.Reply(Lang("CurrencySentSuccess", invoice.Player.Id, invoice.Amount / satsPerCurrencyUnit, currencyName));
                    UpdateSellTransactionStatus(invoice.TransactionId, TransactionStatus.COMPLETED, true);
                    break;
            }

            retryCounts.Remove(invoice.RHash);
            
            lock (webSocketLock)
            {
                if (activeWebSockets.ContainsKey(invoice.RHash))
                {
                    activeWebSockets[invoice.RHash].CancellationTokenSource?.Cancel();
                    activeWebSockets.Remove(invoice.RHash);
                }
            }
        }

        private void RecoverInterruptedTransactions()
        {
            Puts("Checking for interrupted transactions...");

            var sellLogs = LoadSellLogData();
            foreach (var log in sellLogs.Where(l => l.Status == TransactionStatus.INITIATED || l.Status == TransactionStatus.PROCESSING))
            {
                if (!string.IsNullOrEmpty(log.PaymentHash))
                {
                    CheckInvoicePaid(log.PaymentHash, isPaid =>
                    {
                        if (isPaid) UpdateSellTransactionStatus(log.TransactionId, TransactionStatus.COMPLETED, true);
                        else UpdateSellTransactionStatus(log.TransactionId, TransactionStatus.FAILED, false, "Server interrupted");
                    });
                }
                else
                {
                    UpdateSellTransactionStatus(log.TransactionId, TransactionStatus.FAILED, false, "Interrupted before init");
                }
            }

            var buyLogs = LoadBuyLogData();
            foreach (var log in buyLogs.Where(l => l.Status == TransactionStatus.INITIATED || l.Status == TransactionStatus.PROCESSING))
            {
                if (!string.IsNullOrEmpty(log.InvoiceID))
                {
                    CheckInvoicePaid(log.InvoiceID, isPaid =>
                    {
                        if (isPaid) UpdateBuyTransactionStatus(log.TransactionId, TransactionStatus.COMPLETED, true);
                        else UpdateBuyTransactionStatus(log.TransactionId, TransactionStatus.EXPIRED, false);
                    });
                }
            }
        }

        private void CmdBuyCurrency(IPlayer player, string command, string[] args)
        {
            if (!player.HasPermission("orangemart.buycurrency")) { player.Reply(Lang("NoPermission", player.Id)); return; }
            if (IsOnCooldown(player, "buy")) return;
            if (HasTooManyPendingInvoices(player)) return;

            if (args.Length != 1 || !int.TryParse(args[0], out int amount) || amount <= 0)
            {
                player.Reply(Lang("InvalidCommandUsage", player.Id, buyCurrencyCommandName));
                return;
            }

            if (maxPurchaseAmount > 0 && amount > maxPurchaseAmount)
            {
                player.Reply(Lang("AmountTooLarge", player.Id, amount, maxPurchaseAmount, currencyName));
                return;
            }

            player.Reply(Lang("PaymentProcessing", player.Id));

            // Calculate total cost before converting
            double totalCost = amount * pricePerCurrencyUnit;

            CalculateSatsAmount(totalCost, currencyPriceCurrency, amountSats => 
            {
                if (amountSats <= 0)
                {
                    player.Reply(Lang("FailedToFetchPrice", player.Id));
                    return;
                }

                string transactionId = GenerateTransactionId();
                LogBuyInvoice(CreateBuyInvoiceLogEntry(player, null, false, amountSats, PurchaseType.Currency, 0));

                string memo = $"[Orangemart] {player.Name} buying {amount} {currencyName} " + 
                              (currencyPriceCurrency == "USD" ? $"(${totalCost:F2})" : "");

                CreateInvoice(amountSats, memo, invoiceResponse =>
                {
                    if (invoiceResponse != null)
                    {
                        UpdateBuyTransactionInvoiceId(transactionId, invoiceResponse.PaymentHash);
                        player.Reply(Lang("InvoiceCreatedCheckDiscord", player.Id, discordChannelName));

                        var pendingInvoice = new PendingInvoice
                        {
                            TransactionId = transactionId,
                            RHash = invoiceResponse.PaymentHash.ToLower(),
                            Player = player,
                            Amount = amount,
                            SatsAmount = amountSats,
                            Memo = memo,
                            CreatedAt = DateTime.UtcNow,
                            Type = PurchaseType.Currency
                        };
                        pendingInvoices.Add(pendingInvoice);
                        
                        SendInvoiceToDiscord(player, invoiceResponse.PaymentRequest, amountSats, memo, pendingInvoice);

                        Task.Run(async () => await ConnectWebSocket(pendingInvoice));
                        ScheduleInvoiceExpiry(pendingInvoice);
                    }
                    else
                    {
                        player.Reply(Lang("FailedToCreateInvoice", player.Id));
                        UpdateBuyTransactionStatus(transactionId, TransactionStatus.FAILED, false);
                    }
                });
            });
        }

        private void FetchPlayerLightningAddress(string steamId, Action<string, string> callback)
        {
            if (string.IsNullOrEmpty(addressLookupApiUrl))
            {
                callback(null, "API not configured");
                return;
            }

            string url = $"{addressLookupApiUrl}?steamId={steamId}";
            var headers = new Dictionary<string, string>();
            
            if (!string.IsNullOrEmpty(addressLookupApiKey))
            {
                headers["Authorization"] = $"Bearer {addressLookupApiKey}";
            }

            MakeWebRequest(url, null, (code, response) =>
            {
                if (code == 200 && !string.IsNullOrEmpty(response))
                {
                    try
                    {
                        var data = JsonConvert.DeserializeObject<AddressLookupResponse>(response);
                        if (data != null && !string.IsNullOrEmpty(data.LightningAddress))
                        {
                            callback(data.LightningAddress, null);
                            return;
                        }
                        if (data != null && !string.IsNullOrEmpty(data.Error))
                        {
                            callback(null, data.Error);
                            return;
                        }
                    }
                    catch { }
                }
                callback(null, "Server returned invalid response or player not found.");
        
// ... [Code preview truncated due to length. Full source in attached file] ...

Автор
15FPS
Скачиваний
0
Просмотры
1
Первый выпуск
Обновление
Оценка
0.00 звёзд 0 оценок
Сверху